mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
492
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f255c7010 | ||
|
|
4c2e8d4bda | ||
|
|
9f8f1d11be | ||
|
|
9f742634f5 | ||
|
|
6cf3ad78bd | ||
|
|
56e140826f | ||
|
|
ad6ba0a61b | ||
|
|
9e1c1e7c67 | ||
|
|
a36b0b1d1d | ||
|
|
4c1350d8cb | ||
|
|
1aafbc7793 | ||
|
|
3e409bdc91 | ||
|
|
46eb1c22a4 | ||
|
|
f857903c83 | ||
|
|
ee53c01313 | ||
|
|
14a43e6510 | ||
|
|
931e3f9046 | ||
|
|
a6d5e97d45 | ||
|
|
f0ef60e25f | ||
|
|
9fdc66e4a8 | ||
|
|
f2952d7702 | ||
|
|
4dffd11945 | ||
|
|
ab960fd4ed | ||
|
|
34fc1c5ad6 | ||
|
|
ac65aa3708 | ||
|
|
73bf782656 | ||
|
|
a0a303ae89 | ||
|
|
5383bd67b5 | ||
|
|
73ea63fd44 | ||
|
|
3ef04241a1 | ||
|
|
402b5459e6 | ||
|
|
7c56553372 | ||
|
|
a40754eaac | ||
|
|
042bb6bccc | ||
|
|
8bc7273cad | ||
|
|
8e5d2f9dbe | ||
|
|
52864c481f | ||
|
|
2f6fca1eac | ||
|
|
481b5b0534 | ||
|
|
65efd3f2fd | ||
|
|
58c5f33cf5 | ||
|
|
3e20660ab0 | ||
|
|
acb6cd2d77 | ||
|
|
7f82085467 | ||
|
|
1ac72de74d | ||
|
|
94df7a3e58 | ||
|
|
329e44dfd9 | ||
|
|
5d303fca79 | ||
|
|
90f759dac8 | ||
|
|
1fc9e256c0 | ||
|
|
13935b2e68 | ||
|
|
1db7b99685 | ||
|
|
b48e3fc537 | ||
|
|
10ec67d274 | ||
|
|
52f5517c2a | ||
|
|
c4ed103182 | ||
|
|
c04b0884a5 | ||
|
|
893fe4ccda | ||
|
|
b43ef0fabe | ||
|
|
25d2bffb5a | ||
|
|
534763f087 | ||
|
|
ec733974de | ||
|
|
d9811baa7e | ||
|
|
d8b360ecea | ||
|
|
12c55be611 | ||
|
|
3a147f1e01 | ||
|
|
5b03320a32 | ||
|
|
1cc6d35dc6 | ||
|
|
1c5d8ab5a9 | ||
|
|
04a71e6d7d | ||
|
|
bb8b90be56 | ||
|
|
7a2b58ade8 | ||
|
|
854143b593 | ||
|
|
d3068394bf | ||
|
|
d80db8a932 | ||
|
|
179ff728dd | ||
|
|
422044af0d | ||
|
|
7dbdd1931c | ||
|
|
8b7e1b63ab | ||
|
|
ed5c89253c | ||
|
|
06b5a56e74 | ||
|
|
704912d25a | ||
|
|
ea1fe95408 | ||
|
|
8fe54f243e | ||
|
|
4fa6fe86fe | ||
|
|
23c5a0b164 | ||
|
|
7b8dafcf55 | ||
|
|
4e6531fa98 | ||
|
|
6e9b2a9820 | ||
|
|
b11f85d4d7 | ||
|
|
23f1341684 | ||
|
|
e68e163456 | ||
|
|
f8430cecf7 | ||
|
|
276a30fbaa | ||
|
|
3aa8a9f93c | ||
|
|
fc3fb7386a | ||
|
|
6edea618e9 | ||
|
|
e920f2f8ea | ||
|
|
979d5b8809 | ||
|
|
5b4a03c9c3 | ||
|
|
e93fabecd6 | ||
|
|
b2cae511e6 | ||
|
|
2e8e53789f | ||
|
|
6a08fb2f7c | ||
|
|
76d38d4677 | ||
|
|
3f87964397 | ||
|
|
bb19ba0665 | ||
|
|
262851373d | ||
|
|
4afcd5bc9d | ||
|
|
c746a5262c | ||
|
|
e91cdf117f | ||
|
|
1fb671f71b | ||
|
|
40df2701d1 | ||
|
|
c43347886b | ||
|
|
4ef33f389c | ||
|
|
acd52520e4 | ||
|
|
9a373e20f0 | ||
|
|
aad186a632 | ||
|
|
ec9d9b2cc1 | ||
|
|
05d3b2604b | ||
|
|
c3e8a97f66 | ||
|
|
547a56696d | ||
|
|
84bb2397c7 | ||
|
|
503d040885 | ||
|
|
801452eeff | ||
|
|
f6c7d73014 | ||
|
|
228757f656 | ||
|
|
738010e990 | ||
|
|
9dcf9b2d25 | ||
|
|
32ab020dbd | ||
|
|
6fcaf970b1 | ||
|
|
47c83bbaa0 | ||
|
|
7d76febbc9 | ||
|
|
f4c182427f | ||
|
|
836e33ade7 | ||
|
|
0aa7e5bf96 | ||
|
|
b46df4b05e | ||
|
|
cbc9200649 | ||
|
|
a0bf009c06 | ||
|
|
46f516fa3a | ||
|
|
1f82c38e11 | ||
|
|
6ac84ffc11 | ||
|
|
2638b5f384 | ||
|
|
cf19741b9d | ||
|
|
b2c0979b63 | ||
|
|
ea5e1412b2 | ||
|
|
bd948b8ab1 | ||
|
|
937a74d597 | ||
|
|
e747187dda | ||
|
|
ec992dea76 | ||
|
|
544c872881 | ||
|
|
4701b895b7 | ||
|
|
18c56ec3c5 | ||
|
|
4b12c80534 | ||
|
|
a8c6d5e0a3 | ||
|
|
67c6d0d356 | ||
|
|
5e3b432b9b | ||
|
|
fc4d9a1c36 | ||
|
|
55b97df9f7 | ||
|
|
227328fa87 | ||
|
|
071106d0bf | ||
|
|
8c326e6af9 | ||
|
|
5e9ba47bf4 | ||
|
|
0537e285fc | ||
|
|
ca685f5eed | ||
|
|
902999493b | ||
|
|
14363c5d2a | ||
|
|
926b7ae1a7 | ||
|
|
bf815b36ed | ||
|
|
637853e8c2 | ||
|
|
0c022ec722 | ||
|
|
7c83b458fa | ||
|
|
f447a170c6 | ||
|
|
d565592773 | ||
|
|
c21da4dceb | ||
|
|
b08e2ff5c0 | ||
|
|
09b0540e75 | ||
|
|
b4005efda8 | ||
|
|
6d086fd7a7 | ||
|
|
ccebd505b0 | ||
|
|
94c5022dd0 | ||
|
|
b19e383111 | ||
|
|
02ebd99c68 | ||
|
|
83d82e7161 | ||
|
|
c481a4f5db | ||
|
|
eb5ac1b309 | ||
|
|
e73618e0a5 | ||
|
|
0017fd2fe8 | ||
|
|
e44111b6b4 | ||
|
|
aa2dce510a | ||
|
|
5e6acde3c7 | ||
|
|
99a8d3f859 | ||
|
|
fc15a47b1e | ||
|
|
8b32b6525f | ||
|
|
9e0fafb785 | ||
|
|
12ebffb16c | ||
|
|
b539762de7 | ||
|
|
e71af809d4 | ||
|
|
f3ffa3323b | ||
|
|
778331d474 | ||
|
|
8d259577af | ||
|
|
aa108a66a8 | ||
|
|
44e5464950 | ||
|
|
1381200b33 | ||
|
|
f81b250a04 | ||
|
|
8b4ee5f5c5 | ||
|
|
68cb1363e8 | ||
|
|
2344a3ae50 | ||
|
|
8cec5164b7 | ||
|
|
be50c25cb2 | ||
|
|
ee2cc83aa1 | ||
|
|
8e62e2dcd9 | ||
|
|
1347059639 | ||
|
|
29efea561c | ||
|
|
82deca2b0f | ||
|
|
808b3b6408 | ||
|
|
eba10ebbfc | ||
|
|
13903e46a8 | ||
|
|
8aaaa583bd | ||
|
|
4b428065e2 | ||
|
|
578f9a28d0 | ||
|
|
ce2f494f96 | ||
|
|
c1c36491a6 | ||
|
|
a5c3482c6d | ||
|
|
b428274515 | ||
|
|
5dc6adae47 | ||
|
|
264b00f3c7 | ||
|
|
ada79c8ecd | ||
|
|
edd4c3d097 | ||
|
|
38f84a4f6f | ||
|
|
3f1103ef1b | ||
|
|
ec4d561762 | ||
|
|
aee0958e99 | ||
|
|
23c067982a | ||
|
|
6767493833 | ||
|
|
7e917da3ac | ||
|
|
674b49f41f | ||
|
|
4ba04df621 | ||
|
|
3274382bc7 | ||
|
|
069d57b5e5 | ||
|
|
e023211c02 | ||
|
|
a48006640d | ||
|
|
764fd0eddf | ||
|
|
9c9943c94c | ||
|
|
2e9b52371e | ||
|
|
6d6df68972 | ||
|
|
7f863d3172 | ||
|
|
6ecb379bce | ||
|
|
db45c4aaf9 | ||
|
|
198913d6f8 | ||
|
|
cd85824185 | ||
|
|
63797ac79c | ||
|
|
4633409623 | ||
|
|
13c2d33449 | ||
|
|
8cf478d7b0 | ||
|
|
f55215ff93 | ||
|
|
b5d3d0bad6 | ||
|
|
a32d81252c | ||
|
|
a1537535c4 | ||
|
|
61dd34e989 | ||
|
|
2b67cb917b | ||
|
|
ceead92ec5 | ||
|
|
df9ed94635 | ||
|
|
e891613f6f | ||
|
|
528b04427a | ||
|
|
f60a0e91d3 | ||
|
|
2308314529 | ||
|
|
04bc82bca3 | ||
|
|
acdfa33231 | ||
|
|
a03592de81 | ||
|
|
5cf9d088e8 | ||
|
|
db3294b926 | ||
|
|
1847b8c24b | ||
|
|
79e4259081 | ||
|
|
3b7168ac23 | ||
|
|
83ea905ce3 | ||
|
|
42e51c4843 | ||
|
|
9ff9b55352 | ||
|
|
9767e24edd | ||
|
|
bb572f3846 | ||
|
|
ccb5dd5ebb | ||
|
|
01c5b5cee4 | ||
|
|
1049dc4a98 | ||
|
|
d1b0ba13c4 | ||
|
|
2d98f14149 | ||
|
|
19e82064b1 | ||
|
|
3b72374685 | ||
|
|
20fb8c4975 | ||
|
|
68829c0a40 | ||
|
|
2c18062f4d | ||
|
|
f17ce0afbe | ||
|
|
116b12885c | ||
|
|
3d9860db6c | ||
|
|
1a6579e954 | ||
|
|
2659d848d7 | ||
|
|
df6571f63f | ||
|
|
aab1470e08 | ||
|
|
19d43fdc37 | ||
|
|
3b09e5ea80 | ||
|
|
b904d29b25 | ||
|
|
8e2b069a7d | ||
|
|
096c418d27 | ||
|
|
d9e241ccb5 | ||
|
|
086da95525 | ||
|
|
cf6eeaf142 | ||
|
|
27ed557e55 | ||
|
|
af7c823508 | ||
|
|
f2a3d8ee6d | ||
|
|
de7215d2c0 | ||
|
|
2d555b16d9 | ||
|
|
597a2571fc | ||
|
|
409b4c8fdb | ||
|
|
2e72bc96b9 | ||
|
|
fa9557348e | ||
|
|
4768e36074 | ||
|
|
e53f66de72 | ||
|
|
1d33b89ec5 | ||
|
|
7b0f420290 | ||
|
|
00824d1b83 | ||
|
|
99ad05b7b7 | ||
|
|
80273462bd | ||
|
|
2cdefc7369 | ||
|
|
80c752a422 | ||
|
|
8a8a9dc498 | ||
|
|
5f8540b3c4 | ||
|
|
4eef868047 | ||
|
|
5ebfecd822 | ||
|
|
9762560228 | ||
|
|
6310c7d6a0 | ||
|
|
fafe360672 | ||
|
|
d9b6f41940 | ||
|
|
f1ccf0eac3 | ||
|
|
d2aa924bc4 | ||
|
|
46861cd428 | ||
|
|
1c1825f8f0 | ||
|
|
a943995040 | ||
|
|
f0d2a2cf24 | ||
|
|
27ac612c82 | ||
|
|
baedb563f3 | ||
|
|
45c7914602 | ||
|
|
92d85cb8ff | ||
|
|
f5e831844a | ||
|
|
ab2fc7dc4a | ||
|
|
5e377019e4 | ||
|
|
88a4bebca0 | ||
|
|
2daccb5bd3 | ||
|
|
e2a009d4ac | ||
|
|
eda51f826a | ||
|
|
f83dc42a64 | ||
|
|
17e8c996a6 | ||
|
|
2b667f0e7e | ||
|
|
73bcf40bfd | ||
|
|
b635adcb3c | ||
|
|
2b92397e07 | ||
|
|
c23067a0e0 | ||
|
|
3aefc39c08 | ||
|
|
abd8c83df8 | ||
|
|
6cce9426f2 | ||
|
|
2e742863a1 | ||
|
|
dc13a5bb93 | ||
|
|
1f2946eb87 | ||
|
|
5eeda4c02b | ||
|
|
55a3386313 | ||
|
|
b34589a002 | ||
|
|
4486b17b49 | ||
|
|
377ee5ca7c | ||
|
|
4765f40854 | ||
|
|
e239d85821 | ||
|
|
dec419b284 | ||
|
|
6d51a23cc9 | ||
|
|
3d6eec47be | ||
|
|
464fb7008c | ||
|
|
ffa5f89f5f | ||
|
|
f19cbdc957 | ||
|
|
83bfc8d8e6 | ||
|
|
365bb3596e | ||
|
|
9ee96c2ce6 | ||
|
|
e776e266d0 | ||
|
|
d080c8a646 | ||
|
|
cb6402245e | ||
|
|
e4357277ec | ||
|
|
5dc2ab1490 | ||
|
|
9f034a9956 | ||
|
|
f700535997 | ||
|
|
31bca034d8 | ||
|
|
fac4ad5fb7 | ||
|
|
c128d33514 | ||
|
|
4a37262bcf | ||
|
|
a2295112bd | ||
|
|
834dff0f6e | ||
|
|
f4a0431fdd | ||
|
|
d072b328c0 | ||
|
|
435bbcbd7e | ||
|
|
84dc28e584 | ||
|
|
639592cb02 | ||
|
|
8c7a1a6b40 | ||
|
|
06274c4061 | ||
|
|
593925b4a5 | ||
|
|
255477e572 | ||
|
|
0acc6911d8 | ||
|
|
4ca026906b | ||
|
|
97a249e28e | ||
|
|
9f331687d3 | ||
|
|
14d68d6fcd | ||
|
|
05c5334fea | ||
|
|
d277a14c57 | ||
|
|
a7b5dbe6ee | ||
|
|
c188dc1e69 | ||
|
|
9fc9d1762e | ||
|
|
4ef86448d0 | ||
|
|
02117a6072 | ||
|
|
c272c8abac | ||
|
|
cdf2412b39 | ||
|
|
40f52791d3 | ||
|
|
a5d8bd16b9 | ||
|
|
bf113f2151 | ||
|
|
4cd010afb9 | ||
|
|
04b0bdd199 | ||
|
|
9682db93b3 | ||
|
|
1d7ffb4ac2 | ||
|
|
c5e7cd6354 | ||
|
|
b20ba7284b | ||
|
|
49498210f8 | ||
|
|
14e9121302 | ||
|
|
8bd0257dd9 | ||
|
|
93fdfb3add | ||
|
|
e02b03cc70 | ||
|
|
6831fe50a6 | ||
|
|
405108866d | ||
|
|
4d551b71e7 | ||
|
|
a7edd9838d | ||
|
|
9ef54ca6b1 | ||
|
|
4622c50288 | ||
|
|
b1ee0355af | ||
|
|
01d9b213f2 | ||
|
|
f56886bce5 | ||
|
|
88176dfd00 | ||
|
|
45dc3ea09b | ||
|
|
682b3c2409 | ||
|
|
3a93f312b4 | ||
|
|
2ca95f7a7c | ||
|
|
d2319432ab | ||
|
|
253f1a8806 | ||
|
|
2c025d5970 | ||
|
|
f0034683f9 | ||
|
|
8e8398b654 | ||
|
|
aa02293f05 | ||
|
|
f967a32308 | ||
|
|
99ec812455 | ||
|
|
eeef542eeb | ||
|
|
6d7a10b392 | ||
|
|
ca40c0a1e7 | ||
|
|
4a5ad6ea92 | ||
|
|
266dabf6c3 | ||
|
|
a4c8968539 | ||
|
|
613c030ea3 | ||
|
|
9f6bfb6714 | ||
|
|
a764814a8e | ||
|
|
0c9ce138a0 | ||
|
|
6c1dcf6a06 | ||
|
|
fffe93819d | ||
|
|
635ee421e3 | ||
|
|
3ce6d69474 | ||
|
|
cd523c7f77 | ||
|
|
1f1b759914 | ||
|
|
b78915fcbd | ||
|
|
fa0e13a5cc | ||
|
|
924bcddfd2 | ||
|
|
affbf61452 | ||
|
|
7c6b2f70e4 | ||
|
|
c46f2938de | ||
|
|
b355fe70d6 | ||
|
|
10b1079286 | ||
|
|
9c042e6977 | ||
|
|
62583cac8f | ||
|
|
95764945b8 | ||
|
|
26abda1c15 | ||
|
|
01dd3cac72 | ||
|
|
f8faa1f767 | ||
|
|
a9e48e84cd | ||
|
|
9d6704d925 | ||
|
|
ce5157e2e9 | ||
|
|
7d19051df8 | ||
|
|
a0d3bd579f | ||
|
|
82c309632a | ||
|
|
5c0b8ec460 | ||
|
|
7791a86eb4 | ||
|
|
7ac4db3fa0 | ||
|
|
512bc822ef | ||
|
|
f2bb9c3733 | ||
|
|
0bd4f100fd | ||
|
|
3230322fd6 |
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
* @BishopFox/red-team
|
||||
* @BishopFox/bf-red-team
|
||||
@@ -2,8 +2,8 @@ name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -17,34 +17,53 @@ jobs:
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Find Semver Tag
|
||||
- name: Validate Release Tag
|
||||
id: tag
|
||||
run: |
|
||||
git fetch --prune --tags -f
|
||||
tag=$(git tag --points-at "$GITHUB_SHA" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
|
||||
git fetch --prune --tags -f origin
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
|
||||
if ! printf '%s\n' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "Tag $tag is not a release semver tag."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! git merge-base --is-ancestor "$GITHUB_SHA" "origin/master"; then
|
||||
echo "Tag $tag does not point to a commit on origin/master."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
e2e:
|
||||
name: End-to-end test suites
|
||||
needs: tag-check
|
||||
if: needs.tag-check.outputs.tag != ''
|
||||
uses: ./.github/workflows/comprehensive-e2e.yml
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: tag-check
|
||||
if: needs.tag-check.outputs.tag != ''
|
||||
needs:
|
||||
- tag-check
|
||||
- e2e
|
||||
if: needs.tag-check.outputs.tag != '' && needs.e2e.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Go 1.25
|
||||
uses: actions/setup-go@v6
|
||||
- name: Go 1.27.3
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.3"
|
||||
|
||||
- name: Install Minisign
|
||||
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup go environment
|
||||
- name: Go 1.25
|
||||
uses: actions/setup-go@v6
|
||||
- name: Go 1.27.3
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.3"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# Must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head of the pull request.
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
git build-essential zlib1g zlib1g-dev wget zip unzip
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -0,0 +1,551 @@
|
||||
name: Comprehensive e2e Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_scope:
|
||||
description: Test scope to run across the complete platform matrix
|
||||
required: true
|
||||
default: comprehensive
|
||||
type: choice
|
||||
options:
|
||||
- comprehensive
|
||||
- rportfwd
|
||||
workflow_call:
|
||||
inputs:
|
||||
test_scope:
|
||||
description: Test scope to run across the complete platform matrix
|
||||
required: false
|
||||
default: comprehensive
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SLIVER_E2E_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
|
||||
|
||||
concurrency:
|
||||
group: comprehensive-e2e-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
name: Authorize invocation
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check actor repository permission
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
REQUESTING_ACTOR: ${{ github.triggering_actor }}
|
||||
REQUESTED_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
|
||||
with:
|
||||
script: |
|
||||
const testScope = process.env.REQUESTED_SCOPE || "comprehensive";
|
||||
if (!new Set(["comprehensive", "rportfwd"]).has(testScope)) {
|
||||
core.setFailed(`Unsupported E2E test scope: ${JSON.stringify(testScope)}`);
|
||||
return;
|
||||
}
|
||||
core.info(`Validated E2E test scope ${testScope}`);
|
||||
|
||||
const isReleaseTag = context.eventName === "push" && context.ref.startsWith("refs/tags/v");
|
||||
if (isReleaseTag) {
|
||||
core.info(`Authorized release test invocation for ${context.ref}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = process.env.REQUESTING_ACTOR || context.actor;
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: actor,
|
||||
});
|
||||
core.info(`${actor} has repository permission ${data.permission} (${data.role_name})`);
|
||||
if (data.permission !== "admin") {
|
||||
core.setFailed("Comprehensive e2e Tests may only be run by repository administrators");
|
||||
}
|
||||
|
||||
rportfwd-security-regression:
|
||||
name: Reverse-port-forward malicious protocol regression
|
||||
needs: authorize
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
env:
|
||||
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Test malicious destination poisoning and cross-session isolation
|
||||
env:
|
||||
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
|
||||
run: >-
|
||||
go test -buildvcs=false -mod=vendor
|
||||
-tags=server,osusergo,netgo,go_sqlite,sliver_e2e
|
||||
./server/c2
|
||||
-run '^TestMTLSYamux_ReversePortForwardAuthorizationCannotBePoisonedOrReplayed$'
|
||||
-count=1 -v
|
||||
|
||||
darwin:
|
||||
name: comprehensive-e2e-darwin-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-intel
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: macos-15
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'darwin/%s\n' '${{ matrix.goarch }}' > e2e-results/target-darwin-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: darwin
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: darwin
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os darwin \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os darwin
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-darwin-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
linux-native:
|
||||
name: comprehensive-e2e-linux-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'linux/%s\n' '${{ matrix.goarch }}' > e2e-results/target-linux-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: linux
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: linux
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os linux \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os linux
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-linux-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
windows:
|
||||
name: comprehensive-e2e-windows-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: windows-11-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'windows/%s\n' '${{ matrix.goarch }}' > e2e-results/target-windows-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: windows
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server.exe ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: windows
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e.exe ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e.exe \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server.exe \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os windows \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os windows
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-windows-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
aggregate-rportfwd:
|
||||
name: Aggregate reverse-port-forward E2E coverage
|
||||
needs:
|
||||
- authorize
|
||||
- rportfwd-security-regression
|
||||
- darwin
|
||||
- linux-native
|
||||
- windows
|
||||
if: ${{ always() && needs.authorize.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download target coverage
|
||||
id: download
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: comprehensive-e2e-target-*
|
||||
path: rportfwd-coverage-input
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
mkdir -p rportfwd-coverage-input rportfwd-coverage-summary
|
||||
printf 'Reverse-port-forward E2E coverage aggregate\n' > rportfwd-coverage-summary/README.txt
|
||||
|
||||
- name: Aggregate coverage and enforce all 162 required cells
|
||||
id: aggregate
|
||||
if: ${{ always() }}
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input rportfwd-coverage-input
|
||||
-output rportfwd-coverage-summary
|
||||
|
||||
- name: Publish reverse-port-forward coverage table
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
report="rportfwd-coverage-summary/rportfwd-coverage.md"
|
||||
if [[ -s "${report}" ]]; then
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
else
|
||||
printf 'Reverse-port-forward coverage report is missing or empty: %s\n' "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
- name: Upload aggregate reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-rportfwd-coverage-summary
|
||||
path: rportfwd-coverage-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed prerequisites, missing, malformed, or unavailable coverage
|
||||
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.download.outcome != 'success' || steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
aggregate-coverage:
|
||||
name: Aggregate comprehensive E2E coverage
|
||||
needs:
|
||||
- authorize
|
||||
- rportfwd-security-regression
|
||||
- darwin
|
||||
- linux-native
|
||||
- windows
|
||||
if: ${{ always() && needs.authorize.result == 'success' && inputs.test_scope != 'rportfwd' }}
|
||||
outputs:
|
||||
command_coverage_markdown: ${{ steps.publish_command_coverage.outputs.command_coverage_markdown }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download target coverage
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: comprehensive-e2e-target-*
|
||||
path: coverage-input
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
run: |
|
||||
mkdir -p coverage-input coverage-summary
|
||||
printf 'Comprehensive E2E coverage aggregate\n' > coverage-summary/README.txt
|
||||
|
||||
- name: Aggregate coverage and enforce required cells
|
||||
id: aggregate
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/report
|
||||
-input coverage-input
|
||||
-output coverage-summary
|
||||
|
||||
- name: Publish command coverage table
|
||||
id: publish_command_coverage
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
detailed_report="coverage-summary/coverage-summary.md"
|
||||
report="coverage-summary/command-coverage.md"
|
||||
if [[ ! -s "${report}" ]]; then
|
||||
printf 'Command coverage report is missing or empty: %s\n' "${report}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
delimiter="COMPREHENSIVE_E2E_COMMAND_COVERAGE_EOF"
|
||||
if grep -Fxq "${delimiter}" "${report}"; then
|
||||
printf 'Command coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'command_coverage_markdown<<%s\n' "${delimiter}"
|
||||
cat "${report}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
if [[ -f "${detailed_report}" ]]; then
|
||||
cat "${detailed_report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
printf '\n## Command coverage by OS/architecture/transport\n\n' >> "${GITHUB_STEP_SUMMARY}"
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Upload aggregate coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-coverage-summary
|
||||
path: coverage-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed prerequisites or failed or not-run coverage
|
||||
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
reflektor:
|
||||
name: Existing Reflektor test matrix
|
||||
needs: authorize
|
||||
if: ${{ inputs.test_scope != 'rportfwd' }}
|
||||
uses: ./.github/workflows/reflektor-tests.yml
|
||||
|
||||
shellcode:
|
||||
name: Shellcode E2E test matrix
|
||||
needs: authorize
|
||||
if: ${{ inputs.test_scope != 'rportfwd' }}
|
||||
uses: ./.github/workflows/shellcode-tests.yml
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Detect package manager
|
||||
id: detect-package-manager
|
||||
@@ -56,17 +56,17 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
.next/cache
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
run: ${{ steps.detect-package-manager.outputs.runner }} next build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/docs/sliver-docs/out
|
||||
|
||||
@@ -99,4 +99,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -0,0 +1,56 @@
|
||||
name: golangci-lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- ".golangci.yml"
|
||||
- ".github/workflows/golangci-lint.yml"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
- "go.work"
|
||||
- "go.work.sum"
|
||||
- "Makefile"
|
||||
- "vendor/**"
|
||||
- "**/*.go"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: golangci-lint (${{ matrix.goos }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: "0"
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Go 1.27.3
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: "1.27.3"
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.14.0
|
||||
# Gate only findings introduced since the event base revision.
|
||||
args: --new-from-rev=${{ github.event.pull_request.base.sha || github.event.before }} ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
|
||||
@@ -0,0 +1,164 @@
|
||||
name: Reflektor Integration Test Matrix
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
darwin:
|
||||
name: reflektor-darwin-${{ matrix.goarch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-intel
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: macos-15
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os darwin
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
linux-native:
|
||||
name: reflektor-linux-${{ matrix.goarch }}-native
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os linux
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
windows:
|
||||
name: reflektor-windows-${{ matrix.goarch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: windows-11-arm
|
||||
goarch: arm64
|
||||
server_arch: amd64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server.exe ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e.exe ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e.exe
|
||||
-repo .
|
||||
-server ./sliver-server.exe
|
||||
-target-os windows
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
@@ -0,0 +1,201 @@
|
||||
name: Shellcode E2E Test Matrix
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
shellcode_coverage_markdown:
|
||||
description: Shellcode E2E coverage table in Markdown
|
||||
value: ${{ jobs.aggregate.outputs.shellcode_coverage_markdown }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
target:
|
||||
name: shellcode-e2e-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-26
|
||||
target_os: darwin
|
||||
target_arch: arm64
|
||||
server_os: darwin
|
||||
server_arch: arm64
|
||||
server_executable: ./sliver-server
|
||||
test_executable: ./sliver-shellcode-e2e
|
||||
- runner: ubuntu-24.04
|
||||
target_os: linux
|
||||
target_arch: amd64
|
||||
server_os: linux
|
||||
server_arch: amd64
|
||||
server_executable: ./sliver-server
|
||||
test_executable: ./sliver-shellcode-e2e
|
||||
- runner: ubuntu-24.04-arm
|
||||
target_os: linux
|
||||
target_arch: arm64
|
||||
server_os: linux
|
||||
server_arch: arm64
|
||||
server_executable: ./sliver-server
|
||||
test_executable: ./sliver-shellcode-e2e
|
||||
- runner: windows-2022
|
||||
target_os: windows
|
||||
target_arch: amd64
|
||||
server_os: windows
|
||||
server_arch: amd64
|
||||
server_executable: ./sliver-server.exe
|
||||
test_executable: ./sliver-shellcode-e2e.exe
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare target artifact
|
||||
run: |
|
||||
mkdir -p shellcode-results
|
||||
printf '%s/%s\n' \
|
||||
'${{ matrix.target_os }}' \
|
||||
'${{ matrix.target_arch }}' \
|
||||
> 'shellcode-results/target-${{ matrix.target_os }}-${{ matrix.target_arch }}.txt'
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor -trimpath ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: ${{ matrix.server_os }}
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o ${{ matrix.server_executable }} ./server
|
||||
|
||||
- name: Compile shellcode E2E client
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.target_arch }}
|
||||
GOOS: ${{ matrix.target_os }}
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o ${{ matrix.test_executable }} ./test/e2e
|
||||
|
||||
- name: Run shellcode generation and execution tests
|
||||
run: >-
|
||||
${{ matrix.test_executable }}
|
||||
-test.v
|
||||
-test.run=^TestShellcodeE2E$
|
||||
-test.timeout=0
|
||||
-repo .
|
||||
-server ${{ matrix.server_executable }}
|
||||
-server-arch ${{ matrix.server_arch }}
|
||||
-target-os ${{ matrix.target_os }}
|
||||
-target-arch ${{ matrix.target_arch }}
|
||||
-results ./shellcode-results
|
||||
-transports mtls,wg,http
|
||||
-implant-modes session,beacon
|
||||
-beacon-interval 10s
|
||||
-shellcode-sgn-samples 4
|
||||
|
||||
- name: Upload target shellcode results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-target-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
||||
path: shellcode-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
aggregate:
|
||||
name: Aggregate shellcode E2E coverage
|
||||
needs: target
|
||||
if: ${{ always() }}
|
||||
outputs:
|
||||
shellcode_coverage_markdown: ${{ steps.publish_shellcode_coverage.outputs.shellcode_coverage_markdown }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
run: |
|
||||
mkdir -p shellcode-input shellcode-summary
|
||||
printf 'Shellcode E2E coverage aggregate\n' > shellcode-summary/README.txt
|
||||
|
||||
- name: Download target shellcode results
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: shellcode-e2e-target-*
|
||||
path: shellcode-input
|
||||
merge-multiple: true
|
||||
|
||||
- name: Aggregate shellcode coverage
|
||||
id: aggregate
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor -trimpath ./test/e2e/shellcode-report
|
||||
-input ./shellcode-input
|
||||
-output ./shellcode-summary
|
||||
|
||||
- name: Publish shellcode coverage table
|
||||
id: publish_shellcode_coverage
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
report="shellcode-summary/shellcode-coverage.md"
|
||||
if [[ ! -s "${report}" ]]; then
|
||||
printf 'Shellcode coverage report is missing or empty: %s\n' "${report}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
delimiter="SHELLCODE_E2E_COVERAGE_EOF"
|
||||
if grep -Fxq "${delimiter}" "${report}"; then
|
||||
printf 'Shellcode coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'shellcode_coverage_markdown<<%s\n' "${delimiter}"
|
||||
cat "${report}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Upload aggregate shellcode coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-coverage-summary
|
||||
path: shellcode-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed or not-run shellcode coverage
|
||||
if: ${{ always() && steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
@@ -6,11 +6,15 @@ on:
|
||||
paths:
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
- ".gitattributes"
|
||||
- "client/command/opfor/testdata/corpus/**"
|
||||
- ".github/workflows/unit-tests.yml"
|
||||
- "Makefile"
|
||||
- "**/*.go"
|
||||
- "**/*.c"
|
||||
- "**/*.h"
|
||||
- "**/*.sh"
|
||||
- "**/*.ps1"
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
@@ -18,15 +22,15 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
linux-windows-build-test:
|
||||
name: Linux & Windows Test
|
||||
linux-build-test:
|
||||
name: Linux Build & Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
uses: actions/setup-go@v6
|
||||
- name: Go 1.27.3
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.3"
|
||||
|
||||
- name: OS Packages
|
||||
run: |
|
||||
@@ -34,7 +38,7 @@ jobs:
|
||||
git build-essential zlib1g zlib1g-dev wget zip unzip
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
@@ -55,7 +59,8 @@ jobs:
|
||||
set -euo pipefail
|
||||
base_sha="${{ github.event.pull_request.base.sha }}"
|
||||
head_sha="${{ github.sha }}"
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
|
||||
# Consume the full diff so pipefail cannot mistake SIGPIPE for no matching changes.
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -E '^(implant/|server/gogo/|server/generate/)' >/dev/null; then
|
||||
echo "args=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
|
||||
@@ -64,19 +69,62 @@ jobs:
|
||||
- name: Test Linux
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
windows-build-test:
|
||||
name: Windows Build & Test
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.27.3
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: "1.27.3"
|
||||
|
||||
- name: Install make
|
||||
run: choco install make -y --no-progress
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
- name: Make Windows (amd64)
|
||||
run: make windows-amd64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: pwsh
|
||||
run: |
|
||||
$baseSha = "${{ github.event.pull_request.base.sha }}"
|
||||
$headSha = "${{ github.sha }}"
|
||||
$changedFiles = git diff --name-only $baseSha $headSha
|
||||
|
||||
if ($changedFiles | Select-String -Pattern '^(implant/|server/gogo/|server/generate/)') {
|
||||
"args=" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
} else {
|
||||
"args=--skip-generate" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
- name: Test Windows
|
||||
shell: pwsh
|
||||
run: |
|
||||
make clean
|
||||
make
|
||||
./go-tests.ps1 ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
|
||||
macos-build-test:
|
||||
name: MacOS Test
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
uses: actions/setup-go@v6
|
||||
- name: Go 1.27.3
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.3"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
@@ -94,7 +142,8 @@ jobs:
|
||||
set -euo pipefail
|
||||
base_sha="${{ github.event.pull_request.base.sha }}"
|
||||
head_sha="${{ github.sha }}"
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
|
||||
# Consume the full diff so pipefail cannot mistake SIGPIPE for no matching changes.
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -E '^(implant/|server/gogo/|server/generate/)' >/dev/null; then
|
||||
echo "args=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
|
||||
@@ -108,13 +157,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
uses: actions/setup-go@v6
|
||||
- name: Go 1.27.3
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.3"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/crush
|
||||
|
||||
/examples
|
||||
|
||||
/.vscode
|
||||
/.vscode/*
|
||||
/.devcontainer
|
||||
/.idea
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 10m
|
||||
modules-download-mode: vendor
|
||||
build-tags:
|
||||
- client
|
||||
- server
|
||||
- osusergo
|
||||
- netgo
|
||||
- go_sqlite
|
||||
- sliver_generate
|
||||
# Raw implant entry points are Go templates; lint their supporting packages.
|
||||
- sliver_lint
|
||||
|
||||
linters:
|
||||
default: standard
|
||||
enable:
|
||||
- gocyclo
|
||||
- misspell
|
||||
- revive
|
||||
settings:
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
@@ -4,7 +4,7 @@
|
||||
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
|
||||
- `make client` builds a CGO-free client; `make debug` keeps symbols.
|
||||
- `make pb` regenerates protobuf stubs from `protobuf/`.
|
||||
- `./go-tests.sh` runs the curated Go test suite with build tags.
|
||||
- `./go-tests.sh` runs the Go test suite, this is the only way you should run unit tests.
|
||||
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
@@ -2,31 +2,31 @@
|
||||
# Makefile for Sliver
|
||||
#
|
||||
|
||||
.DEFAULT_GOAL := default
|
||||
|
||||
GO ?= go
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ENV =
|
||||
TAGS ?= -tags go_sqlite
|
||||
CGO_ENABLED = 0
|
||||
GOLANGCI_LINT ?= golangci-lint
|
||||
# The tree has legacy lint findings, so gate issues introduced by this change.
|
||||
GOLANGCI_LINT_BASE ?= master
|
||||
GOLANGCI_LINT_ARGS ?= --new-from-merge-base=$(GOLANGCI_LINT_BASE)
|
||||
GOLANGCI_LINT_PACKAGES = \
|
||||
./client/... \
|
||||
./server/... \
|
||||
./util/... \
|
||||
./protobuf/... \
|
||||
./docs \
|
||||
./implant/...
|
||||
|
||||
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
|
||||
CGO_ENABLED = 1
|
||||
endif
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 25
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 27
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
|
||||
@@ -34,151 +34,299 @@ ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
ASSET_BUILD_SOURCES = $(filter-out %_test.go,$(wildcard util/assets/*.go util/cmd/assets/*.go util/cmd/sliver-wasm-go/*.go util/cmd/sliver-wasm-go/overlay/*.go.txt))
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_ARGS) $(GOLANGCI_LINT_PACKAGES)
|
||||
|
||||
ifneq ($(OS),Windows_NT)
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_P := $(shell uname -p)
|
||||
|
||||
# Programs required for generating protobuf/grpc files
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
ifeq ($(MAKECMDGOALS), pb)
|
||||
K := $(foreach exec,$(PB_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
||||
endif
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
#
|
||||
# Targets
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets: $(ASSET_BUILD_SOURCES)
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
|
||||
|
||||
#
|
||||
# Targets
|
||||
# >>> WINDOWS <<<
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
else
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
SHELL := cmd.exe
|
||||
.SHELLFLAGS := /C
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
GO_VERSION := $(patsubst go%,%,$(strip $(shell $(GO) env GOVERSION)))
|
||||
GO_MAJOR_VERSION := $(word 1,$(subst ., ,$(GO_VERSION)))
|
||||
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
COMMA := ,
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
define windows_exec
|
||||
$(strip $(foreach envvar,$(1),set "$(envvar)" && ))$(2)
|
||||
endef
|
||||
|
||||
define windows_go_build
|
||||
$(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build $(4) $(TAGS)$(COMMA)$(5) $(6) -o $(7) ./$(5))
|
||||
endef
|
||||
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client)
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX).exe)
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-386$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,windows,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-386$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,freebsd,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,freebsd,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-amd64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: pb
|
||||
pb: validate-pb-compilers
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor,server,$(LDFLAGS_DEBUG),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor,client,$(LDFLAGS_DEBUG),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: validate-pb-compilers
|
||||
validate-pb-compilers:
|
||||
@for %%P in ($(PB_COMPILERS)) do @where.exe %%P >NUL || (echo Missing protobuf util %%P in PATH & exit /b 1)
|
||||
|
||||
validate-go-version:
|
||||
@if $(GO_MAJOR_VERSION) GTR $(MIN_SUPPORTED_GO_MAJOR_VERSION) (exit /b 0) else if $(GO_MAJOR_VERSION) LSS $(MIN_SUPPORTED_GO_MAJOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1) else if $(GO_MINOR_VERSION) LSS $(MIN_SUPPORTED_GO_MINOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1)
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
-rmdir /S /Q server\assets\fs\darwin\amd64
|
||||
-rmdir /S /Q server\assets\fs\darwin\arm64
|
||||
-rmdir /S /Q server\assets\fs\windows\amd64
|
||||
-rmdir /S /Q server\assets\fs\linux\amd64
|
||||
-del /Q /F server\assets\fs\*.zip 2>NUL
|
||||
-del /Q /F .downloaded_assets 2>NUL
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
|
||||
|
||||
.downloaded_assets: $(ASSET_BUILD_SOURCES)
|
||||
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
|
||||
@type NUL > .downloaded_assets
|
||||
|
||||
endif
|
||||
@@ -1,30 +1,17 @@
|
||||
# Sliver
|
||||
|
||||
Sliver is an open source cross-platform adversary emulation/red team framework, it can be used by organizations of all sizes to perform security testing. Sliver's implants support C2 over Mutual TLS (mTLS), WireGuard, HTTP(S), and DNS and are dynamically compiled with per-binary asymmetric encryption keys.
|
||||
Sliver is an open-source, cross-platform adversary emulation and red team framework designed for organizations of all sizes to perform authorized security testing.
|
||||
|
||||
The server and client support MacOS, Windows, and Linux. Implants are supported on MacOS, Windows, and Linux (and possibly every Golang compiler target but we've not tested them all).
|
||||
Sliver implants support command-and-control (C2) over Mutual TLS (mTLS), WireGuard, HTTP(S), and DNS. Implants are dynamically compiled with unique per-binary asymmetric encryption keys and support a wide range of post-exploitation capabilities across macOS, Windows, and Linux.
|
||||
|
||||
[](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [](https://goreportcard.com/report/github.com/BishopFox/sliver) [](https://www.gnu.org/licenses/gpl-3.0)
|
||||
Sliver includes support for native and shellcode payloads, userspace reflective loading, built-in shellcode encoding, and BOF/COFF execution on both amd64 and arm64 architectures for macOS, Windows, and Linux.
|
||||
|
||||
The Sliver server and client run on macOS, Windows, and Linux. Implants officially support those same platforms and may also run on additional targets supported by the Go compiler, although those configurations are not regularly tested.
|
||||
|
||||
[](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml) [](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
|
||||
|
||||
### Features
|
||||
|
||||
- Dynamic code generation
|
||||
- Compile-time obfuscation
|
||||
- Multiplayer-mode
|
||||
- Staged and Stageless payloads
|
||||
- [Procedurally generated C2](https://sliver.sh/docs?name=HTTPS+C2) over HTTP(S)
|
||||
- [DNS canary](https://sliver.sh/docs?name=DNS+C2) blue team detection
|
||||
- [Secure C2](https://sliver.sh/docs?name=Transport+Encryption) over mTLS, WireGuard, HTTP(S), and DNS
|
||||
- Fully scriptable using [Python](https://github.com/moloch--/sliver-py)
|
||||
- Windows process migration, process injection, user token manipulation, etc.
|
||||
- Let's Encrypt integration
|
||||
- In-memory .NET assembly execution
|
||||
- COFF/BOF in-memory loader
|
||||
- TCP and named pipe pivots
|
||||
- Much more!
|
||||
|
||||
### Getting Started
|
||||
|
||||
Download the latest [release](https://github.com/BishopFox/sliver/releases) and see the Sliver [wiki](https://sliver.sh/docs?name=Getting+Started) for a quick tutorial on basic setup and usage. To get the very latest and greatest compile from source.
|
||||
|
||||
+18
-7
@@ -34,15 +34,26 @@ const (
|
||||
ConfigDirName = "configs"
|
||||
)
|
||||
|
||||
// ClientWGConfig - Optional WireGuard wrapper configuration for multiplayer.
|
||||
type ClientWGConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
ServerPubKey string `json:"server_pub_key"`
|
||||
ClientPrivateKey string `json:"client_private_key"`
|
||||
ClientPubKey string `json:"client_pub_key"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
ServerIP string `json:"server_ip"`
|
||||
}
|
||||
|
||||
// ClientConfig - Client JSON config
|
||||
type ClientConfig struct {
|
||||
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
|
||||
LHost string `json:"lhost"`
|
||||
LPort int `json:"lport"`
|
||||
Token string `json:"token"`
|
||||
CACertificate string `json:"ca_certificate"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
Certificate string `json:"certificate"`
|
||||
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
|
||||
LHost string `json:"lhost"`
|
||||
LPort int `json:"lport"`
|
||||
Token string `json:"token"`
|
||||
CACertificate string `json:"ca_certificate"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
Certificate string `json:"certificate"`
|
||||
WG *ClientWGConfig `json:"wg,omitempty"`
|
||||
}
|
||||
|
||||
// GetConfigDir - Returns the path to the config dir
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientWGConfigEnabledIsPositiveOptIn(t *testing.T) {
|
||||
legacyJSON := []byte(`{"wg":{"server_pub_key":"server-pub","client_private_key":"client-priv","client_ip":"100.64.0.2"}}`)
|
||||
legacy := &ClientConfig{}
|
||||
if err := json.Unmarshal(legacyJSON, legacy); err != nil {
|
||||
t.Fatalf("unmarshal legacy config: %v", err)
|
||||
}
|
||||
if legacy.WG == nil {
|
||||
t.Fatal("expected legacy WireGuard config")
|
||||
}
|
||||
if legacy.WG.Enabled {
|
||||
t.Fatal("expected an absent enabled marker to remain disabled")
|
||||
}
|
||||
|
||||
enabledJSON := []byte(`{"wg":{"enabled":true,"server_pub_key":"server-pub","client_private_key":"client-priv","client_ip":"100.64.0.2"}}`)
|
||||
enabled := &ClientConfig{}
|
||||
if err := json.Unmarshal(enabledJSON, enabled); err != nil {
|
||||
t.Fatalf("unmarshal enabled config: %v", err)
|
||||
}
|
||||
if enabled.WG == nil || !enabled.WG.Enabled {
|
||||
t.Fatal("expected enabled marker to opt in to WireGuard")
|
||||
}
|
||||
}
|
||||
+33
-3
@@ -19,6 +19,7 @@ package cli
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -34,6 +35,8 @@ const (
|
||||
logFileName = "sliver-client.log"
|
||||
)
|
||||
|
||||
var clientLogFile *os.File
|
||||
|
||||
// Initialize logging.
|
||||
func initLogging(appDir string) *os.File {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
@@ -47,11 +50,14 @@ func initLogging(appDir string) *os.File {
|
||||
|
||||
func init() {
|
||||
appDir := assets.GetRootAppDir()
|
||||
logFile := initLogging(appDir)
|
||||
defer logFile.Close()
|
||||
clientLogFile = initLogging(appDir)
|
||||
|
||||
rootCmd.TraverseChildren = true
|
||||
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
||||
rootCmd.PersistentFlags().String(ConsoleConfigFlagName, "", "path to client config file")
|
||||
rootCmd.PersistentFlags().Bool(enableWGFlag, false, "force multiplayer connections through the operator config's WireGuard wrapper")
|
||||
rootCmd.PersistentFlags().Bool(disableWGFlag, false, "force multiplayer connections to use direct mTLS")
|
||||
rootCmd.MarkFlagsMutuallyExclusive(enableWGFlag, disableWGFlag)
|
||||
|
||||
// Create the console client, without any RPC or commands bound to it yet.
|
||||
// This created before anything so that multiple commands can make use of
|
||||
@@ -93,10 +99,34 @@ var rootCmd = &cobra.Command{
|
||||
Long: ``,
|
||||
}
|
||||
|
||||
type commandExitError struct {
|
||||
code int
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *commandExitError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e *commandExitError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func commandExitCode(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if exitErr, ok := errors.AsType[*commandExitError](err); ok {
|
||||
return exitErr.code
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// Execute - Execute root command.
|
||||
func Execute() {
|
||||
defer func() { _ = clientLogFile.Close() }()
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Printf("root command: %s\n", err)
|
||||
os.Exit(1)
|
||||
os.Exit(commandExitCode(err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
)
|
||||
|
||||
func TestPackageLoggerRemainsOpenAfterInit(t *testing.T) {
|
||||
logFile, ok := log.Writer().(*os.File)
|
||||
if !ok {
|
||||
t.Fatalf("standard logger writer = %T, want *os.File", log.Writer())
|
||||
}
|
||||
if want := filepath.Join(assets.GetRootAppDir(), logFileName); filepath.Clean(logFile.Name()) != want {
|
||||
t.Fatalf("standard logger file = %q, want %q", logFile.Name(), want)
|
||||
}
|
||||
if _, err := logFile.Stat(); err != nil {
|
||||
t.Fatalf("standard logger file is closed after package initialization: %v", err)
|
||||
}
|
||||
}
|
||||
+19
-7
@@ -20,24 +20,37 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
)
|
||||
|
||||
func selectConfig() (string, *assets.ClientConfig) {
|
||||
configs := assets.GetConfigs()
|
||||
func selectConfig(configPath string, stdinTTY bool) (string, *assets.ClientConfig, error) {
|
||||
if configPath != "" {
|
||||
config, err := loadMCPClientConfig(configPath)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("load client config: %w", err)
|
||||
}
|
||||
return filepath.Base(configPath), config, nil
|
||||
}
|
||||
return selectConfigFrom(assets.GetConfigs(), stdinTTY)
|
||||
}
|
||||
|
||||
func selectConfigFrom(configs map[string]*assets.ClientConfig, stdinTTY bool) (string, *assets.ClientConfig, error) {
|
||||
if len(configs) == 0 {
|
||||
return "", nil
|
||||
return "", nil, fmt.Errorf("no config files found at %s", assets.GetConfigDir())
|
||||
}
|
||||
|
||||
if len(configs) == 1 {
|
||||
for key, config := range configs {
|
||||
return key, config
|
||||
return key, config, nil
|
||||
}
|
||||
}
|
||||
if !stdinTTY {
|
||||
return "", nil, fmt.Errorf("multiple configs found; use --config to select one")
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(configs))
|
||||
for key := range configs {
|
||||
@@ -48,9 +61,8 @@ func selectConfig() (string, *assets.ClientConfig) {
|
||||
selection := keys[0]
|
||||
err := forms.Select("Select a server:", keys, &selection)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return "", nil
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return selection, configs[selection]
|
||||
return selection, configs[selection], nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigFlagRoutesBeforeAndAfterConsoleAndMCP(t *testing.T) {
|
||||
flag := rootCmd.PersistentFlags().Lookup(ConsoleConfigFlagName)
|
||||
if flag == nil {
|
||||
t.Fatal("root --config flag is missing")
|
||||
}
|
||||
oldValue, oldChanged := flag.Value.String(), flag.Changed
|
||||
t.Cleanup(func() {
|
||||
_ = flag.Value.Set(oldValue)
|
||||
flag.Changed = oldChanged
|
||||
})
|
||||
|
||||
for _, command := range []string{"console", "mcp"} {
|
||||
for _, before := range []bool{true, false} {
|
||||
name := command + " after --config"
|
||||
args := []string{"--config", "selected.cfg", command}
|
||||
if !before {
|
||||
name = command + " before --config"
|
||||
args = []string{command, "--config", "selected.cfg"}
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_ = flag.Value.Set("")
|
||||
flag.Changed = false
|
||||
cmd, remaining, err := rootCmd.Traverse(args)
|
||||
if err != nil {
|
||||
t.Fatalf("traverse %q: %v", args, err)
|
||||
}
|
||||
if cmd.Name() != command {
|
||||
t.Fatalf("command = %q, want %q", cmd.Name(), command)
|
||||
}
|
||||
if err := cmd.ParseFlags(remaining); err != nil {
|
||||
t.Fatalf("parse %q: %v", remaining, err)
|
||||
}
|
||||
got, err := cmd.Flags().GetString(ConsoleConfigFlagName)
|
||||
if err != nil || got != "selected.cfg" {
|
||||
t.Fatalf("--config = %q, error = %v", got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
)
|
||||
|
||||
func TestSelectConfigFromNonTTYRequiresExplicitConfigWhenMultiple(t *testing.T) {
|
||||
configs := map[string]*assets.ClientConfig{
|
||||
"first": {Operator: "first"},
|
||||
"second": {Operator: "second"},
|
||||
}
|
||||
key, config, err := selectConfigFrom(configs, false)
|
||||
if err == nil || !strings.Contains(err.Error(), "--config") {
|
||||
t.Fatalf("error = %v, want --config guidance", err)
|
||||
}
|
||||
if key != "" || config != nil {
|
||||
t.Fatalf("selected %q (%v) without an explicit config", key, config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectConfigFromNonTTYUsesSoleConfig(t *testing.T) {
|
||||
want := &assets.ClientConfig{Operator: "only"}
|
||||
key, config, err := selectConfigFrom(map[string]*assets.ClientConfig{"only": want}, false)
|
||||
if err != nil || key != "only" || config != want {
|
||||
t.Fatalf("selection = %q (%v), error = %v", key, config, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectConfigWithExplicitPathOnNonTTY(t *testing.T) {
|
||||
want := &assets.ClientConfig{Operator: "selected", LHost: "127.0.0.1"}
|
||||
data, err := json.Marshal(want)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "selected.cfg")
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key, config, err := selectConfig(path, false)
|
||||
if err != nil {
|
||||
t.Fatalf("select explicit config: %v", err)
|
||||
}
|
||||
if key != "selected.cfg" || config.Operator != want.Operator || config.LHost != want.LHost {
|
||||
t.Fatalf("selection = %q (%v)", key, config)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
bspinner "charm.land/bubbles/v2/spinner"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
minConnectSpinnerDuration = 900 * time.Millisecond
|
||||
minConnectStatusDuration = 350 * time.Millisecond
|
||||
)
|
||||
|
||||
type connectResult struct {
|
||||
rpc rpcpb.SliverRPCClient
|
||||
conn *grpc.ClientConn
|
||||
err error
|
||||
}
|
||||
|
||||
func connectWithSpinner(out io.Writer, target string, connect func(transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error)) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
statusCh := make(chan string, 8)
|
||||
resultCh := make(chan connectResult, 1)
|
||||
|
||||
go func() {
|
||||
rpc, conn, err := connect(func(status string) {
|
||||
sendStatus(statusCh, status)
|
||||
})
|
||||
resultCh <- connectResult{rpc: rpc, conn: conn, err: err}
|
||||
}()
|
||||
|
||||
currentStatus := ""
|
||||
currentStatusSince := time.Time{}
|
||||
queuedStatus := ""
|
||||
lastWidth := 0
|
||||
spinner := bspinner.New(bspinner.WithSpinner(bspinner.Line))
|
||||
ticker := time.NewTicker(spinner.Spinner.FPS)
|
||||
defer ticker.Stop()
|
||||
startedAt := time.Now()
|
||||
var pendingResult *connectResult
|
||||
|
||||
setStatus := func(status string) {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" || status == currentStatus {
|
||||
return
|
||||
}
|
||||
currentStatus = status
|
||||
currentStatusSince = time.Now()
|
||||
}
|
||||
|
||||
stageStatus := func(status string) {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
return
|
||||
}
|
||||
if currentStatus == "" || time.Since(currentStatusSince) >= minConnectStatusDuration {
|
||||
queuedStatus = ""
|
||||
setStatus(status)
|
||||
return
|
||||
}
|
||||
queuedStatus = status
|
||||
}
|
||||
|
||||
flushQueuedStatus := func() {
|
||||
if queuedStatus == "" || time.Since(currentStatusSince) < minConnectStatusDuration {
|
||||
return
|
||||
}
|
||||
nextStatus := queuedStatus
|
||||
queuedStatus = ""
|
||||
setStatus(nextStatus)
|
||||
}
|
||||
|
||||
canReturnPendingResult := func() bool {
|
||||
if pendingResult == nil {
|
||||
return false
|
||||
}
|
||||
if pendingResult.err != nil {
|
||||
return true
|
||||
}
|
||||
if time.Since(startedAt) < minConnectSpinnerDuration {
|
||||
return false
|
||||
}
|
||||
if queuedStatus != "" {
|
||||
return false
|
||||
}
|
||||
if !currentStatusSince.IsZero() && time.Since(currentStatusSince) < minConnectStatusDuration {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
render := func() {
|
||||
line := fmt.Sprintf("%s %s", spinner.View(), formatConnectSpinnerMessage(target, currentStatus))
|
||||
lastWidth = writeSpinnerLine(out, line, lastWidth)
|
||||
}
|
||||
|
||||
render()
|
||||
for {
|
||||
select {
|
||||
case status := <-statusCh:
|
||||
stageStatus(status)
|
||||
spinner, _ = spinner.Update(spinner.Tick())
|
||||
render()
|
||||
|
||||
case result := <-resultCh:
|
||||
pendingResult = &result
|
||||
|
||||
case <-ticker.C:
|
||||
spinner, _ = spinner.Update(spinner.Tick())
|
||||
flushQueuedStatus()
|
||||
render()
|
||||
if canReturnPendingResult() {
|
||||
result := *pendingResult
|
||||
pendingResult = nil
|
||||
clearSpinnerLine(out, lastWidth)
|
||||
return result.rpc, result.conn, result.err
|
||||
}
|
||||
}
|
||||
|
||||
if pendingResult != nil && pendingResult.err != nil {
|
||||
result := *pendingResult
|
||||
pendingResult = nil
|
||||
clearSpinnerLine(out, lastWidth)
|
||||
return result.rpc, result.conn, result.err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendStatus(statusCh chan string, status string) {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
return
|
||||
}
|
||||
statusCh <- status
|
||||
}
|
||||
|
||||
func formatConnectSpinnerMessage(target string, status string) string {
|
||||
target = strings.TrimSpace(target)
|
||||
status = strings.TrimSpace(status)
|
||||
|
||||
if target == "" {
|
||||
if status == "" {
|
||||
return "Connecting ..."
|
||||
}
|
||||
return fmt.Sprintf("Connecting (%s) ...", status)
|
||||
}
|
||||
if status == "" {
|
||||
return fmt.Sprintf("Connecting to %s ...", target)
|
||||
}
|
||||
return fmt.Sprintf("Connecting to %s (%s) ...", target, status)
|
||||
}
|
||||
|
||||
func writeSpinnerLine(out io.Writer, line string, lastWidth int) int {
|
||||
width := utf8.RuneCountInString(line)
|
||||
padding := ""
|
||||
if lastWidth > width {
|
||||
padding = strings.Repeat(" ", lastWidth-width)
|
||||
}
|
||||
fmt.Fprintf(out, "\r%s%s", line, padding)
|
||||
if width > lastWidth {
|
||||
return width
|
||||
}
|
||||
return lastWidth
|
||||
}
|
||||
|
||||
func clearSpinnerLine(out io.Writer, lastWidth int) {
|
||||
if lastWidth <= 0 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(out, "\r%s\r", strings.Repeat(" ", lastWidth))
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestFormatConnectSpinnerMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target string
|
||||
status string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "target only",
|
||||
target: "127.0.0.1:31337",
|
||||
want: "Connecting to 127.0.0.1:31337 ...",
|
||||
},
|
||||
{
|
||||
name: "target with status",
|
||||
target: "127.0.0.1:31337",
|
||||
status: "wireguard",
|
||||
want: "Connecting to 127.0.0.1:31337 (wireguard) ...",
|
||||
},
|
||||
{
|
||||
name: "status without target",
|
||||
status: "grpc/mtls",
|
||||
want: "Connecting (grpc/mtls) ...",
|
||||
},
|
||||
{
|
||||
name: "trim whitespace",
|
||||
target: " 127.0.0.1:31337 ",
|
||||
status: " grpc/mtls over wireguard ",
|
||||
want: "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := formatConnectSpinnerMessage(test.target, test.status); got != test.want {
|
||||
t.Fatalf("expected %q, got %q", test.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectWithSpinnerOutput(t *testing.T) {
|
||||
synctest.Test(t, testConnectWithSpinnerOutput)
|
||||
}
|
||||
|
||||
func testConnectWithSpinnerOutput(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
spinnerDelay := 2 * (100 * time.Millisecond)
|
||||
|
||||
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
statusFn("wireguard")
|
||||
time.Sleep(spinnerDelay)
|
||||
statusFn("grpc/mtls over wireguard")
|
||||
time.Sleep(spinnerDelay)
|
||||
return nil, nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("connectWithSpinner returned error: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
|
||||
t.Fatalf("expected wireguard status in output, got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
|
||||
t.Fatalf("expected grpc over wireguard status in output, got %q", got)
|
||||
}
|
||||
frameCount := 0
|
||||
for _, frame := range []string{"|", "/", "-", "\\"} {
|
||||
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337") {
|
||||
frameCount++
|
||||
}
|
||||
}
|
||||
if frameCount < 2 {
|
||||
t.Fatalf("expected multiple spinner frames in output, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
|
||||
synctest.Test(t, testConnectWithSpinnerFastSuccessStillShowsMultipleFrames)
|
||||
}
|
||||
|
||||
func testConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
|
||||
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
statusFn("grpc/mtls")
|
||||
return nil, nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("connectWithSpinner returned error: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
frameCount := 0
|
||||
for _, frame := range []string{"|", "/", "-", "\\"} {
|
||||
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337 (grpc/mtls) ...") {
|
||||
frameCount++
|
||||
}
|
||||
}
|
||||
if frameCount < 2 {
|
||||
t.Fatalf("expected multiple spinner frames for fast success, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectWithSpinnerFastSuccessShowsEachStatus(t *testing.T) {
|
||||
synctest.Test(t, testConnectWithSpinnerFastSuccessShowsEachStatus)
|
||||
}
|
||||
|
||||
func testConnectWithSpinnerFastSuccessShowsEachStatus(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
|
||||
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
statusFn("wireguard")
|
||||
statusFn("grpc/mtls over wireguard")
|
||||
return nil, nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("connectWithSpinner returned error: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
|
||||
t.Fatalf("expected fast success output to include wireguard status, got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
|
||||
t.Fatalf("expected fast success output to include grpc over wireguard status, got %q", got)
|
||||
}
|
||||
if strings.Contains(got, "Connected to 127.0.0.1:31337") {
|
||||
t.Fatalf("did not expect past-tense success output, got %q", got)
|
||||
}
|
||||
}
|
||||
+35
-20
@@ -20,16 +20,20 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// ConsoleConfigFlagName is the flag used to select a client config file.
|
||||
const ConsoleConfigFlagName = "config"
|
||||
|
||||
// consoleCmd generates the console with required pre/post runners.
|
||||
func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
consoleCmd := &cobra.Command{
|
||||
@@ -44,37 +48,48 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
|
||||
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
|
||||
pre = func(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
|
||||
return nil
|
||||
}
|
||||
configKey, config := selectConfig()
|
||||
if config == nil {
|
||||
return nil
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rcScript, err := ReadRCScript(cmd)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read rc script: %s\n", err)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Don't clobber output when simply running an implant command from system shell.
|
||||
if run {
|
||||
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
|
||||
configPath := ""
|
||||
if cmd.Flags().Lookup(ConsoleConfigFlagName) != nil {
|
||||
configPath, err = cmd.Flags().GetString(ConsoleConfigFlagName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stdinTTY := term.IsTerminal(int(os.Stdin.Fd()))
|
||||
configKey, config, err := selectConfig(configPath, stdinTTY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%d", config.LHost, config.LPort)
|
||||
var rpc rpcpb.SliverRPCClient
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Connection to server failed %s", err)
|
||||
return nil
|
||||
// Don't clobber output when simply running an implant command from system shell.
|
||||
if run && stdinTTY {
|
||||
rpc, ln, err = connectWithSpinner(os.Stdout, target, func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
return transport.MTLSConnectWithStatus(config, statusFn)
|
||||
})
|
||||
} else {
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
}
|
||||
return console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connection to server failed: %w", err)
|
||||
}
|
||||
if err := console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript); err != nil {
|
||||
_ = con.CloseConnection()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close the RPC connection once exiting
|
||||
|
||||
@@ -57,7 +57,9 @@ func makeRunners(implantCmd *cobra.Command, con *console.SliverClient) (pre, pos
|
||||
// The pre-run function connects to the server and sets up a "fake" console,
|
||||
// so we can have access to active sessions/beacons, and other stuff needed.
|
||||
pre = func(_ *cobra.Command, args []string) error {
|
||||
startConsole(implantCmd, args)
|
||||
if err := startConsole(implantCmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the active target.
|
||||
target, _ := implantCmd.Flags().GetString("use")
|
||||
|
||||
+21
-13
@@ -20,7 +20,6 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/rsteube/carapace"
|
||||
@@ -32,19 +31,12 @@ func importCmd() *cobra.Command {
|
||||
Use: "import",
|
||||
Short: "Import a client configuration file",
|
||||
Long: `import [config files]`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if 0 < len(args) {
|
||||
for _, arg := range args {
|
||||
conf, err := assets.ReadConfig(arg)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] %s\n", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
assets.SaveConfig(conf)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Missing config file path, see --help")
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
_, _ = fmt.Fprint(cmd.OutOrStdout(), "Missing config file path, see --help")
|
||||
return nil
|
||||
}
|
||||
return importConfigs(args)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{}, cobra.ShellCompDirectiveDefault
|
||||
@@ -55,3 +47,19 @@ func importCmd() *cobra.Command {
|
||||
|
||||
return cmdImport
|
||||
}
|
||||
|
||||
func importConfigs(paths []string) error {
|
||||
for _, path := range paths {
|
||||
conf, err := assets.ReadConfig(path)
|
||||
if err != nil {
|
||||
return &commandExitError{
|
||||
code: 3,
|
||||
err: fmt.Errorf("read import %q: %w", path, err),
|
||||
}
|
||||
}
|
||||
if err := assets.SaveConfig(conf); err != nil {
|
||||
return fmt.Errorf("save import %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeImportConfig(t *testing.T, name, contents string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), name)
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestImportCommandReportsInvalidConfig(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
path := writeImportConfig(t, "missing-operator.cfg", `{"lhost":"server"}`)
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want error identifying %q", err, path)
|
||||
}
|
||||
if got := commandExitCode(err); got != 1 {
|
||||
t.Fatalf("invalid config exit code = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandReportsConfigWriteFailure(t *testing.T) {
|
||||
clientRoot := t.TempDir()
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
|
||||
path := writeImportConfig(t, "alice.cfg", `{"operator":"alice","lhost":"server"}`)
|
||||
|
||||
configDir := filepath.Join(clientRoot, "configs")
|
||||
if err := os.MkdirAll(filepath.Join(configDir, "alice_server.cfg"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want write error identifying %q", err, path)
|
||||
}
|
||||
if got := commandExitCode(err); got != 1 {
|
||||
t.Fatalf("config write failure exit code = %d, want 1", got)
|
||||
}
|
||||
if _, ok := errors.AsType[*os.PathError](err); !ok {
|
||||
t.Fatalf("config write failure does not wrap the filesystem error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandReadFailureExitCode(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
path := filepath.Join(t.TempDir(), "missing.cfg")
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want read error identifying %q", err, path)
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("import error does not wrap the missing file error: %v", err)
|
||||
}
|
||||
if got := commandExitCode(fmt.Errorf("command failed: %w", err)); got != 3 {
|
||||
t.Fatalf("wrapped read failure exit code = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandParseFailureExitCode(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
path := writeImportConfig(t, "malformed.cfg", `{"operator":`)
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want parse error identifying %q", err, path)
|
||||
}
|
||||
if _, ok := errors.AsType[*json.SyntaxError](err); !ok {
|
||||
t.Fatalf("import error does not wrap the JSON syntax error: %v", err)
|
||||
}
|
||||
if got := commandExitCode(err); got != 3 {
|
||||
t.Fatalf("parse failure exit code = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandStopsAfterFirstFailure(t *testing.T) {
|
||||
clientRoot := t.TempDir()
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
|
||||
invalidPath := writeImportConfig(t, "invalid.cfg", `{"lhost":"server"}`)
|
||||
validPath := writeImportConfig(t, "valid.cfg", `{"operator":"alice","lhost":"server"}`)
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{invalidPath, validPath})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(invalidPath)) {
|
||||
t.Fatalf("import error = %v, want first file %q", err, invalidPath)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(clientRoot, "configs", "alice_server.cfg")); !os.IsNotExist(err) {
|
||||
t.Fatalf("later config was imported after first failure: stat error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandMissingPathPreservesSuccess(t *testing.T) {
|
||||
cmd := importCmd()
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
err := cmd.RunE(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("import without a path failed: %v", err)
|
||||
}
|
||||
if got := commandExitCode(err); got != 0 {
|
||||
t.Fatalf("missing path exit code = %d, want 0", got)
|
||||
}
|
||||
if got, want := output.String(), "Missing config file path, see --help"; got != want {
|
||||
t.Fatalf("missing path output = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
+19
-3
@@ -22,12 +22,14 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
clientmcp "github.com/bishopfox/sliver/client/mcp"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
)
|
||||
|
||||
func mcpCmd(con *console.SliverClient) *cobra.Command {
|
||||
@@ -38,11 +40,14 @@ func mcpCmd(con *console.SliverClient) *cobra.Command {
|
||||
return runMCP(cmd, con)
|
||||
},
|
||||
}
|
||||
mcpCmd.Flags().String("config", "", "path to client config file")
|
||||
return mcpCmd
|
||||
}
|
||||
|
||||
func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath, _ := cmd.Flags().GetString("config")
|
||||
config, err := loadMCPClientConfig(configPath)
|
||||
if err != nil {
|
||||
@@ -50,15 +55,26 @@ func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keep the gRPC link alive while the MCP bridge sits idle between tool calls.
|
||||
// The daemon permits pings >=30s without streams (server.yaml: grpc.keepalive).
|
||||
transport.SetKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 60 * time.Second,
|
||||
Timeout: 20 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
})
|
||||
|
||||
rpc, ln, err := transport.MTLSConnect(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Connection to server failed %s\n", err)
|
||||
return nil
|
||||
}
|
||||
defer ln.Close()
|
||||
defer transport.CloseGRPCConnection(ln)
|
||||
|
||||
con.Rpc = rpc
|
||||
go handleConnectionLost(ln)
|
||||
// Do not install the connection-lost watchdog here: gRPC channels idle
|
||||
// out after 30 minutes without RPCs (grpc-go default) and transparently
|
||||
// reconnect on the next call. Exiting on the Idle state would kill an
|
||||
// otherwise healthy MCP bridge between tool calls.
|
||||
|
||||
cfg := clientmcp.DefaultConfig()
|
||||
if err := clientmcp.ServeStdio(cfg, con.Rpc); err != nil {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
enableWGFlag = "enable-wg"
|
||||
disableWGFlag = "disable-wg"
|
||||
)
|
||||
|
||||
func applyMultiplayerConnectMode(cmd *cobra.Command) error {
|
||||
if cmd == nil {
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
return nil
|
||||
}
|
||||
|
||||
enableWG, err := cmd.Flags().GetBool(enableWGFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
disableWG, err := cmd.Flags().GetBool(disableWGFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.Flags().Changed(enableWGFlag) && cmd.Flags().Changed(disableWGFlag) {
|
||||
return fmt.Errorf("--%s and --%s cannot be used together", enableWGFlag, disableWGFlag)
|
||||
}
|
||||
|
||||
mode := transport.MultiplayerConnectAuto
|
||||
switch {
|
||||
case enableWG:
|
||||
mode = transport.MultiplayerConnectEnableWG
|
||||
case disableWG:
|
||||
mode = transport.MultiplayerConnectDisableWG
|
||||
}
|
||||
transport.SetMultiplayerConnectMode(mode)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestWireGuardOverrideFlagsDefaultToFalse(t *testing.T) {
|
||||
for _, name := range []string{enableWGFlag, disableWGFlag} {
|
||||
flag := rootCmd.PersistentFlags().Lookup(name)
|
||||
if flag == nil {
|
||||
t.Fatalf("expected --%s persistent flag", name)
|
||||
}
|
||||
if flag.DefValue != "false" {
|
||||
t.Fatalf("expected --%s to default to false, got %q", name, flag.DefValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeDefaultsToAuto(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
t.Fatalf("apply multiplayer mode: %v", err)
|
||||
}
|
||||
if !transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
|
||||
t.Fatal("expected automatic mode to honor an explicitly enabled WireGuard config")
|
||||
}
|
||||
if transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
|
||||
t.Fatal("expected automatic mode to ignore a legacy WireGuard config without the opt-in marker")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeEnableWGForcesLegacyConfig(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", enableWGFlag, err)
|
||||
}
|
||||
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
t.Fatalf("apply multiplayer mode: %v", err)
|
||||
}
|
||||
if !transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
|
||||
t.Fatal("expected --enable-wg to force a complete legacy config through WireGuard")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeDisableWGOverridesEnabledConfig(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", disableWGFlag, err)
|
||||
}
|
||||
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
t.Fatalf("apply multiplayer mode: %v", err)
|
||||
}
|
||||
if transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
|
||||
t.Fatal("expected --disable-wg to force direct mTLS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeRejectsConflictingOverrides(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", enableWGFlag, err)
|
||||
}
|
||||
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", disableWGFlag, err)
|
||||
}
|
||||
|
||||
err := applyMultiplayerConnectMode(cmd)
|
||||
if err == nil {
|
||||
t.Fatal("expected conflicting WireGuard overrides to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot be used together") {
|
||||
t.Fatalf("unexpected conflict error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeRejectsBothOverridesWhenExplicitlyFalse(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(enableWGFlag, "false"); err != nil {
|
||||
t.Fatalf("set --%s: %v", enableWGFlag, err)
|
||||
}
|
||||
if err := cmd.Flags().Set(disableWGFlag, "false"); err != nil {
|
||||
t.Fatalf("set --%s: %v", disableWGFlag, err)
|
||||
}
|
||||
|
||||
err := applyMultiplayerConnectMode(cmd)
|
||||
if err == nil {
|
||||
t.Fatal("expected explicitly combining both WireGuard override flags to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot be used together") {
|
||||
t.Fatalf("unexpected conflict error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newTransportModeTestCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool(enableWGFlag, false, "")
|
||||
cmd.Flags().Bool(disableWGFlag, false, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func resetMultiplayerConnectModeForTest(t *testing.T) {
|
||||
t.Helper()
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
t.Cleanup(func() {
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
})
|
||||
}
|
||||
|
||||
func enabledWGClientConfig() *assets.ClientConfig {
|
||||
config := legacyWGClientConfig()
|
||||
config.WG.Enabled = true
|
||||
return config
|
||||
}
|
||||
|
||||
func legacyWGClientConfig() *assets.ClientConfig {
|
||||
return &assets.ClientConfig{
|
||||
WG: &assets.ClientWGConfig{
|
||||
ServerPubKey: "server-pub",
|
||||
ClientPrivateKey: "client-priv",
|
||||
ClientIP: "100.64.0.2",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
|
||||
|
||||
## Sub-packages
|
||||
|
||||
- `ai/` – Implements the 'ai' Bubble Tea layout preview command for the Sliver client console.
|
||||
- `alias/` – Implements the 'alias' command group for the Sliver client console.
|
||||
- `armory/` – Implements the 'armory' command group for the Sliver client console.
|
||||
- `backdoor/` – Implements the 'backdoor' command group for the Sliver client console.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# client/command/ai
|
||||
|
||||
Implements the `ai` command for the Sliver client console.
|
||||
|
||||
The TUI loads server-backed AI conversation threads over gRPC, submits prompts to the server, and refreshes when `AIConversationEvent` updates arrive with assistant replies or failure messages.
|
||||
@@ -0,0 +1,242 @@
|
||||
package ai
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/termio"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var openAIProgramTTY = tea.OpenTTY
|
||||
|
||||
// AICmd launches the AI conversation TUI.
|
||||
func AICmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
if con == nil || con.Rpc == nil {
|
||||
if con != nil {
|
||||
con.PrintErrorf("Connect to a server before using `ai`.\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
listenerID, listener := con.CreateEventListener()
|
||||
defer con.RemoveEventListener(listenerID)
|
||||
restoreEventNotifications := con.SuppressEventNotifications()
|
||||
defer restoreEventNotifications()
|
||||
|
||||
model := newAIModel(con, buildAIContext(con), listener)
|
||||
model.showExperimentalWarningModal()
|
||||
|
||||
width, height := 100, 30
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
width, height = w, h
|
||||
}
|
||||
|
||||
opts := []tea.ProgramOption{
|
||||
tea.WithWindowSize(width, height),
|
||||
tea.WithColorProfile(colorprofile.TrueColor),
|
||||
}
|
||||
ttyOpts, cleanup := configureAIProgramTTY()
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
opts = append(opts, ttyOpts...)
|
||||
|
||||
program := tea.NewProgram(model, opts...)
|
||||
if _, err := program.Run(); err != nil {
|
||||
con.PrintErrorf("AI TUI error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Console logging can swap stdout/stderr to pipes so session output can be tee'd
|
||||
// to disk. Bubble Tea needs a real TTY output to enable terminal features like
|
||||
// mouse reporting, so bind the AI TUI back to the interactive terminal when
|
||||
// stdout is no longer the controlling TTY.
|
||||
func configureAIProgramTTY() ([]tea.ProgramOption, func()) {
|
||||
stdinTTY := isAITTY(os.Stdin)
|
||||
stdoutTTY := isAITTY(os.Stdout)
|
||||
|
||||
switch {
|
||||
case !stdinTTY || stdoutTTY:
|
||||
return nil, nil
|
||||
|
||||
case isAITTY(termio.InteractiveInput()) && isAITTY(termio.InteractiveOutput()):
|
||||
return []tea.ProgramOption{
|
||||
tea.WithInput(termio.InteractiveInput()),
|
||||
tea.WithOutput(termio.InteractiveOutput()),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
inTTY, outTTY, err := openAIProgramTTY()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []tea.ProgramOption{
|
||||
tea.WithInput(inTTY),
|
||||
tea.WithOutput(outTTY),
|
||||
}, func() {
|
||||
_ = inTTY.Close()
|
||||
if outTTY != inTTY {
|
||||
_ = outTTY.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isAITTY(file *os.File) bool {
|
||||
return file != nil && term.IsTerminal(int(file.Fd()))
|
||||
}
|
||||
|
||||
type aiContext struct {
|
||||
target aiTargetSummary
|
||||
connection aiConnectionSummary
|
||||
status string
|
||||
}
|
||||
|
||||
type aiTargetSummary struct {
|
||||
SessionID string
|
||||
BeaconID string
|
||||
Label string
|
||||
Host string
|
||||
OS string
|
||||
Arch string
|
||||
C2 string
|
||||
Mode string
|
||||
Details []string
|
||||
}
|
||||
|
||||
type aiConnectionSummary struct {
|
||||
Profile string
|
||||
Server string
|
||||
Operator string
|
||||
State string
|
||||
}
|
||||
|
||||
func defaultAITargetSummary() aiTargetSummary {
|
||||
return aiTargetSummary{
|
||||
Label: "No active target",
|
||||
Host: "Select a session or beacon with `use` or ctrl+s",
|
||||
Mode: "offline preview",
|
||||
C2: "n/a",
|
||||
OS: "unknown",
|
||||
Arch: "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
func aiSessionTargetSummary(session *clientpb.Session) aiTargetSummary {
|
||||
if session == nil {
|
||||
return defaultAITargetSummary()
|
||||
}
|
||||
|
||||
return aiTargetSummary{
|
||||
SessionID: session.ID,
|
||||
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
|
||||
Host: fallback(session.Hostname, "<unknown host>"),
|
||||
OS: fallback(session.OS, "unknown"),
|
||||
Arch: fallback(session.Arch, "unknown"),
|
||||
C2: fallback(session.ActiveC2, "unknown"),
|
||||
Mode: "interactive session",
|
||||
Details: []string{
|
||||
fmt.Sprintf("User: %s", fallback(session.Username, "<unknown>")),
|
||||
fmt.Sprintf("PID: %d", session.PID),
|
||||
fmt.Sprintf("Remote: %s", fallback(session.RemoteAddress, "<unknown>")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func aiBeaconTargetSummary(beacon *clientpb.Beacon) aiTargetSummary {
|
||||
if beacon == nil {
|
||||
return defaultAITargetSummary()
|
||||
}
|
||||
|
||||
return aiTargetSummary{
|
||||
BeaconID: beacon.ID,
|
||||
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
|
||||
Host: fallback(beacon.Hostname, "<unknown host>"),
|
||||
OS: fallback(beacon.OS, "unknown"),
|
||||
Arch: fallback(beacon.Arch, "unknown"),
|
||||
C2: fallback(beacon.ActiveC2, "unknown"),
|
||||
Mode: "asynchronous beacon",
|
||||
Details: []string{
|
||||
fmt.Sprintf("User: %s", fallback(beacon.Username, "<unknown>")),
|
||||
fmt.Sprintf("PID: %d", beacon.PID),
|
||||
fmt.Sprintf("Remote: %s", fallback(beacon.RemoteAddress, "<unknown>")),
|
||||
fmt.Sprintf("Interval: %s", time.Duration(beacon.Interval).String()),
|
||||
fmt.Sprintf("Next checkin: %s", formatUnix(beacon.NextCheckin)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildAIContext(con *console.SliverClient) aiContext {
|
||||
ctx := aiContext{
|
||||
target: defaultAITargetSummary(),
|
||||
connection: aiConnectionSummary{
|
||||
Profile: "<disconnected>",
|
||||
Server: "<unknown>",
|
||||
Operator: "<unknown>",
|
||||
State: "idle",
|
||||
},
|
||||
status: "Loading AI conversations from the server...",
|
||||
}
|
||||
|
||||
if con != nil {
|
||||
if details, state, ok := con.CurrentConnection(); ok {
|
||||
ctx.connection.State = strings.ToLower(state.String())
|
||||
if details != nil && details.Config != nil {
|
||||
ctx.connection.Profile = fallback(details.ConfigKey, "<profile unavailable>")
|
||||
ctx.connection.Server = fmt.Sprintf("%s:%d", details.Config.LHost, details.Config.LPort)
|
||||
ctx.connection.Operator = fallback(details.Config.Operator, "<unknown>")
|
||||
}
|
||||
}
|
||||
|
||||
session, beacon := con.ActiveTarget.Get()
|
||||
switch {
|
||||
case session != nil:
|
||||
ctx.target = aiSessionTargetSummary(session)
|
||||
case beacon != nil:
|
||||
ctx.target = aiBeaconTargetSummary(beacon)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func fallback(value, def string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return def
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func formatUnix(ts int64) string {
|
||||
if ts <= 0 {
|
||||
return "<unknown>"
|
||||
}
|
||||
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package ai
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Commands returns the ai command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newAICommand(consts.SliverCoreHelpGroup, con)}
|
||||
}
|
||||
|
||||
// ServerCommands returns the ai command for the top-level client REPL.
|
||||
func ServerCommands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newAICommand(consts.GenericHelpGroup, con)}
|
||||
}
|
||||
|
||||
func newAICommand(groupID string, con *console.SliverClient) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: consts.AIStr,
|
||||
Short: "Open the Sliver AI conversation TUI",
|
||||
Long: help.GetHelpFor([]string{consts.AIStr}),
|
||||
Args: cobra.NoArgs,
|
||||
GroupID: groupID,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
AICmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
package ai
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestLoadAIStateCmdLoadsConversationOverRPC(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
providersResp: &clientpb.AIProviderConfigs{
|
||||
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
|
||||
Config: &clientpb.AIConfigSummary{
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
conversationsResp: &clientpb.AIConversations{
|
||||
Conversations: []*clientpb.AIConversation{
|
||||
{ID: "conv-1", Provider: "openai", Model: "gpt-test", Title: "First"},
|
||||
{ID: "conv-2", Provider: "openai", Model: "gpt-test", Title: "Second"},
|
||||
},
|
||||
},
|
||||
conversationByID: map[string]*clientpb.AIConversation{
|
||||
"conv-1": {
|
||||
ID: "conv-1",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "First",
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "user", Content: "Hello"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
cmd := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")
|
||||
msg := cmd()
|
||||
|
||||
loaded, ok := msg.(aiLoadedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiLoadedMsg, got %T", msg)
|
||||
}
|
||||
if loaded.selectedID != "conv-1" {
|
||||
t.Fatalf("unexpected selected conversation id: got=%q want=%q", loaded.selectedID, "conv-1")
|
||||
}
|
||||
if loaded.conversation == nil || loaded.conversation.GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected loaded conversation: %+v", loaded.conversation)
|
||||
}
|
||||
if len(loaded.conversations) != 2 {
|
||||
t.Fatalf("unexpected conversation count: got=%d want=%d", len(loaded.conversations), 2)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if server.getProvidersCalls != 1 {
|
||||
t.Fatalf("expected GetAIProviders to be called once, got %d", server.getProvidersCalls)
|
||||
}
|
||||
if server.getConversationsCalls != 1 {
|
||||
t.Fatalf("expected GetAIConversations to be called once, got %d", server.getConversationsCalls)
|
||||
}
|
||||
if len(server.getConversationReqs) != 1 {
|
||||
t.Fatalf("expected GetAIConversation to be called once, got %d", len(server.getConversationReqs))
|
||||
}
|
||||
if server.getConversationReqs[0].GetID() != "conv-1" || !server.getConversationReqs[0].GetIncludeMessages() {
|
||||
t.Fatalf("unexpected GetAIConversation request: %+v", server.getConversationReqs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAIStateCmdCreatesConversationWhenNoneExist(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
providersResp: &clientpb.AIProviderConfigs{
|
||||
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
|
||||
Config: &clientpb.AIConfigSummary{
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
SystemPrompt: "Stay concise.",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
conversationsResp: &clientpb.AIConversations{},
|
||||
saveConversationResp: &clientpb.AIConversation{
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "New conversation",
|
||||
SystemPrompt: "Stay concise.",
|
||||
},
|
||||
conversationByID: map[string]*clientpb.AIConversation{
|
||||
"conv-created": {
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "New conversation",
|
||||
SystemPrompt: "Stay concise.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")()
|
||||
|
||||
loaded, ok := msg.(aiLoadedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiLoadedMsg, got %T", msg)
|
||||
}
|
||||
if loaded.status != "Created a new AI conversation." {
|
||||
t.Fatalf("unexpected status: %q", loaded.status)
|
||||
}
|
||||
if loaded.selectedID != "conv-created" {
|
||||
t.Fatalf("unexpected selected conversation id: %q", loaded.selectedID)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
|
||||
}
|
||||
request := server.saveConversationReqs[0]
|
||||
if request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetTitle() != "New conversation" {
|
||||
t.Fatalf("unexpected SaveAIConversation request: %+v", request)
|
||||
}
|
||||
if request.GetSystemPrompt() != "Stay concise." {
|
||||
t.Fatalf("unexpected SaveAIConversation system prompt: %q", request.GetSystemPrompt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitPromptCmdCreatesConversationAndSavesUserMessage(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
saveConversationResp: &clientpb.AIConversation{
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "Explain the workflow.",
|
||||
},
|
||||
saveMessageResp: &clientpb.AIConversationMessage{
|
||||
ID: "msg-1",
|
||||
ConversationID: "conv-created",
|
||||
Role: "user",
|
||||
Content: "Explain the workflow.\n\nWith details.",
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := submitPromptCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
aiTargetSummary{SessionID: "session-1"},
|
||||
nil,
|
||||
"openai",
|
||||
"gpt-test",
|
||||
"Stay concise.",
|
||||
"Explain the workflow.\n\nWith details.",
|
||||
)()
|
||||
|
||||
submitted, ok := msg.(aiPromptSubmittedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
|
||||
}
|
||||
if submitted.conversationID != "conv-created" {
|
||||
t.Fatalf("unexpected conversation id: %q", submitted.conversationID)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
|
||||
}
|
||||
if server.saveConversationReqs[0].GetTitle() != "Explain the workflow." {
|
||||
t.Fatalf("unexpected conversation title: %q", server.saveConversationReqs[0].GetTitle())
|
||||
}
|
||||
if server.saveConversationReqs[0].GetTargetSessionID() != "session-1" {
|
||||
t.Fatalf("expected target session to be forwarded, got %+v", server.saveConversationReqs[0])
|
||||
}
|
||||
if server.saveConversationReqs[0].GetSystemPrompt() != "Stay concise." {
|
||||
t.Fatalf("unexpected conversation system prompt: %q", server.saveConversationReqs[0].GetSystemPrompt())
|
||||
}
|
||||
if len(server.saveMessageReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
|
||||
}
|
||||
request := server.saveMessageReqs[0]
|
||||
if request.GetConversationID() != "conv-created" || request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetRole() != "user" {
|
||||
t.Fatalf("unexpected SaveAIConversationMessage request: %+v", request)
|
||||
}
|
||||
if request.GetContent() != "Explain the workflow.\n\nWith details." {
|
||||
t.Fatalf("unexpected prompt content: %q", request.GetContent())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitPromptCmdUsesExistingConversationSettings(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
saveMessageResp: &clientpb.AIConversationMessage{
|
||||
ID: "msg-1",
|
||||
ConversationID: "conv-1",
|
||||
Role: "user",
|
||||
Content: "hello",
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := submitPromptCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
aiTargetSummary{},
|
||||
&clientpb.AIConversation{ID: "conv-1", Provider: "anthropic", Model: "claude-test", Title: "Thread"},
|
||||
"openai",
|
||||
"gpt-test",
|
||||
"Stay concise.",
|
||||
"hello",
|
||||
)()
|
||||
|
||||
if _, ok := msg.(aiPromptSubmittedMsg); !ok {
|
||||
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 0 {
|
||||
t.Fatalf("did not expect SaveAIConversation to be called, got %d calls", len(server.saveConversationReqs))
|
||||
}
|
||||
if len(server.saveMessageReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
|
||||
}
|
||||
request := server.saveMessageReqs[0]
|
||||
if request.GetProvider() != "anthropic" || request.GetModel() != "claude-test" {
|
||||
t.Fatalf("unexpected conversation settings in message request: %+v", request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteConversationCmdSendsDeleteRequest(t *testing.T) {
|
||||
server := &aiRPCServer{}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := deleteConversationCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
"conv-1",
|
||||
"conv-2",
|
||||
`Deleted "Thread".`,
|
||||
)()
|
||||
|
||||
deleted, ok := msg.(aiConversationDeletedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiConversationDeletedMsg, got %T", msg)
|
||||
}
|
||||
if deleted.conversationID != "conv-1" || deleted.selectedID != "conv-2" {
|
||||
t.Fatalf("unexpected delete message: %+v", deleted)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.deleteConversationReqs) != 1 {
|
||||
t.Fatalf("expected DeleteAIConversation to be called once, got %d", len(server.deleteConversationReqs))
|
||||
}
|
||||
if server.deleteConversationReqs[0].GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected DeleteAIConversation request: %+v", server.deleteConversationReqs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdFiltersForAIEvents(t *testing.T) {
|
||||
eventPayload := &clientpb.AIConversationEvent{
|
||||
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
|
||||
Conversation: &clientpb.AIConversation{
|
||||
ID: "conv-1",
|
||||
OperatorName: "alice",
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(eventPayload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal conversation event: %v", err)
|
||||
}
|
||||
|
||||
listener := make(chan *clientpb.Event, 2)
|
||||
listener <- &clientpb.Event{EventType: consts.WatchtowerEvent}
|
||||
listener <- &clientpb.Event{EventType: consts.AIConversationEvent, Data: data}
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
|
||||
aiEvent, ok := msg.(aiConversationEventMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiConversationEventMsg, got %T", msg)
|
||||
}
|
||||
if aiEvent.event == nil || aiEvent.event.GetConversation() == nil || aiEvent.event.GetConversation().GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected AI conversation event payload: %+v", aiEvent.event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdReturnsToastMessages(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event, 1)
|
||||
listener <- &clientpb.Event{
|
||||
EventType: consts.ClientToastEvent,
|
||||
Data: []byte("Session alpha connected"),
|
||||
Err: "info",
|
||||
}
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
|
||||
toast, ok := msg.(aiToastMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiToastMsg, got %T", msg)
|
||||
}
|
||||
if toast.level != "info" || toast.message != "Session alpha connected" {
|
||||
t.Fatalf("unexpected toast payload: %+v", toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdReturnsClosedMessage(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
if _, ok := msg.(aiListenerClosedMsg); !ok {
|
||||
t.Fatalf("expected aiListenerClosedMsg, got %T", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIModelAppliesSharedConversationEventsWithoutReload(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
model := newAIModel(nil, aiContext{
|
||||
connection: aiConnectionSummary{Operator: "alice"},
|
||||
}, listener)
|
||||
model.loading = false
|
||||
model.conversations = []*clientpb.AIConversation{{ID: "conv-1", Title: "First"}}
|
||||
|
||||
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
|
||||
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
|
||||
Conversation: &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
OperatorName: "bob",
|
||||
Title: "Shared thread",
|
||||
},
|
||||
}})
|
||||
|
||||
nextModel := updated.(*aiModel)
|
||||
if nextModel.loading {
|
||||
t.Fatal("did not expect shared conversation event to trigger a reload")
|
||||
}
|
||||
if conversationIndexByID(nextModel.conversations, "conv-2") < 0 {
|
||||
t.Fatalf("expected shared conversation to be merged locally, got %+v", nextModel.conversations)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected shared conversation event to continue listening for events")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIModelAppliesDeleteEventsWhileAwaitingResponse(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
model := newAIModel(nil, aiContext{
|
||||
connection: aiConnectionSummary{Operator: "alice"},
|
||||
}, listener)
|
||||
model.loading = false
|
||||
model.awaitingResponse = true
|
||||
model.currentConversation = &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
OperatorName: "alice",
|
||||
UpdatedAt: 100,
|
||||
TurnState: clientpb.AIConversationTurnState_AI_TURN_STATE_IN_PROGRESS,
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "user", Content: "still waiting"},
|
||||
},
|
||||
}
|
||||
model.conversations = []*clientpb.AIConversation{
|
||||
{ID: "conv-2", Title: "Current"},
|
||||
{ID: "conv-1", Title: "Fallback"},
|
||||
}
|
||||
|
||||
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
|
||||
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_DELETED,
|
||||
ConversationID: "conv-2",
|
||||
Conversation: &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
},
|
||||
}})
|
||||
|
||||
nextModel := updated.(*aiModel)
|
||||
if nextModel.loading {
|
||||
t.Fatal("did not expect delete tombstone to trigger a reload")
|
||||
}
|
||||
if nextModel.currentConversation != nil {
|
||||
t.Fatalf("expected deleted conversation to be cleared, got %+v", nextModel.currentConversation)
|
||||
}
|
||||
if conversationIndexByID(nextModel.conversations, "conv-2") >= 0 {
|
||||
t.Fatalf("expected deleted conversation to be removed, got %+v", nextModel.conversations)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected delete tombstone to continue listening for events")
|
||||
}
|
||||
}
|
||||
|
||||
type aiRPCServer struct {
|
||||
rpcpb.UnimplementedSliverRPCServer
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
providersResp *clientpb.AIProviderConfigs
|
||||
conversationsResp *clientpb.AIConversations
|
||||
conversationByID map[string]*clientpb.AIConversation
|
||||
saveConversationResp *clientpb.AIConversation
|
||||
saveMessageResp *clientpb.AIConversationMessage
|
||||
sessionsResp *clientpb.Sessions
|
||||
beaconsResp *clientpb.Beacons
|
||||
|
||||
getProvidersCalls int
|
||||
getConversationsCalls int
|
||||
getSessionsCalls int
|
||||
getBeaconsCalls int
|
||||
getConversationReqs []*clientpb.AIConversationReq
|
||||
deleteConversationReqs []*clientpb.AIConversationReq
|
||||
saveConversationReqs []*clientpb.AIConversation
|
||||
saveMessageReqs []*clientpb.AIConversationMessage
|
||||
getConversationStart chan struct{}
|
||||
getConversationWait <-chan struct{}
|
||||
saveMessageStarted chan struct{}
|
||||
saveMessageRelease <-chan struct{}
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetAIProviders(context.Context, *commonpb.Empty) (*clientpb.AIProviderConfigs, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getProvidersCalls++
|
||||
if s.providersResp == nil {
|
||||
return &clientpb.AIProviderConfigs{}, nil
|
||||
}
|
||||
return proto.Clone(s.providersResp).(*clientpb.AIProviderConfigs), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetAIConversations(context.Context, *commonpb.Empty) (*clientpb.AIConversations, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getConversationsCalls++
|
||||
if s.conversationsResp == nil {
|
||||
return &clientpb.AIConversations{}, nil
|
||||
}
|
||||
return proto.Clone(s.conversationsResp).(*clientpb.AIConversations), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetSessions(context.Context, *commonpb.Empty) (*clientpb.Sessions, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getSessionsCalls++
|
||||
if s.sessionsResp == nil {
|
||||
return &clientpb.Sessions{}, nil
|
||||
}
|
||||
return proto.Clone(s.sessionsResp).(*clientpb.Sessions), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetBeacons(context.Context, *commonpb.Empty) (*clientpb.Beacons, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getBeaconsCalls++
|
||||
if s.beaconsResp == nil {
|
||||
return &clientpb.Beacons{}, nil
|
||||
}
|
||||
return proto.Clone(s.beaconsResp).(*clientpb.Beacons), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*clientpb.AIConversation, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getConversationReqs = append(s.getConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
|
||||
if s.getConversationStart != nil {
|
||||
select {
|
||||
case s.getConversationStart <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if s.getConversationWait != nil {
|
||||
s.mu.Unlock()
|
||||
<-s.getConversationWait
|
||||
s.mu.Lock()
|
||||
}
|
||||
if s.conversationByID == nil {
|
||||
return &clientpb.AIConversation{}, nil
|
||||
}
|
||||
conversation := s.conversationByID[req.GetID()]
|
||||
if conversation == nil {
|
||||
return &clientpb.AIConversation{}, nil
|
||||
}
|
||||
return proto.Clone(conversation).(*clientpb.AIConversation), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) SaveAIConversation(_ context.Context, req *clientpb.AIConversation) (*clientpb.AIConversation, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.saveConversationReqs = append(s.saveConversationReqs, proto.Clone(req).(*clientpb.AIConversation))
|
||||
if s.saveConversationResp == nil {
|
||||
return proto.Clone(req).(*clientpb.AIConversation), nil
|
||||
}
|
||||
return proto.Clone(s.saveConversationResp).(*clientpb.AIConversation), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) DeleteAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*commonpb.Empty, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.deleteConversationReqs = append(s.deleteConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) SaveAIConversationMessage(_ context.Context, req *clientpb.AIConversationMessage) (*clientpb.AIConversationMessage, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.saveMessageReqs = append(s.saveMessageReqs, proto.Clone(req).(*clientpb.AIConversationMessage))
|
||||
if s.saveMessageStarted != nil {
|
||||
select {
|
||||
case s.saveMessageStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if s.saveMessageRelease != nil {
|
||||
s.mu.Unlock()
|
||||
<-s.saveMessageRelease
|
||||
s.mu.Lock()
|
||||
}
|
||||
if s.saveMessageResp == nil {
|
||||
return proto.Clone(req).(*clientpb.AIConversationMessage), nil
|
||||
}
|
||||
return proto.Clone(s.saveMessageResp).(*clientpb.AIConversationMessage), nil
|
||||
}
|
||||
|
||||
func newAITestRPCClient(t *testing.T, srv rpcpb.SliverRPCServer) (rpcpb.SliverRPCClient, func()) {
|
||||
t.Helper()
|
||||
|
||||
listener := bufconn.Listen(1024 * 1024)
|
||||
grpcServer := grpc.NewServer()
|
||||
rpcpb.RegisterSliverRPCServer(grpcServer, srv)
|
||||
|
||||
go func() {
|
||||
_ = grpcServer.Serve(listener)
|
||||
}()
|
||||
|
||||
dialer := func(context.Context, string) (net.Conn, error) { return listener.Dial() }
|
||||
conn, err := grpc.DialContext(context.Background(), "bufnet",
|
||||
grpc.WithContextDialer(dialer),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
grpcServer.Stop()
|
||||
_ = listener.Close()
|
||||
t.Fatalf("dial bufconn: %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
_ = conn.Close()
|
||||
grpcServer.Stop()
|
||||
_ = listener.Close()
|
||||
}
|
||||
|
||||
return rpcpb.NewSliverRPCClient(conn), cleanup
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,251 @@
|
||||
package alias
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Alias commands run with cobra's DisableFlagParsing enabled (see LoadAlias),
|
||||
// the same treatment extension commands received for their arguments. Before
|
||||
// that, an unknown flag such as the payload's own `-pid` was rejected outright
|
||||
// by pflag ("unknown flag"), and operators had to inject cobra's "--"
|
||||
// separator (documented on the wiki) to pass arguments through at all (#2264).
|
||||
//
|
||||
// With flag parsing disabled cobra hands Run the raw, unstripped slice, and we
|
||||
// extract the Sliver-owned flags ourselves here. Everything we do not own is
|
||||
// passed to the payload untouched, so all of these now work:
|
||||
//
|
||||
// amsi-bypass -pid 6969
|
||||
// fakealias --process notepad.exe arg1 arg2
|
||||
// fakealias -- --anything still-here (existing workaround, unchanged)
|
||||
type ownedFlagKind int
|
||||
|
||||
const (
|
||||
ownedBool ownedFlagKind = iota
|
||||
ownedString
|
||||
ownedUint32
|
||||
ownedInt64
|
||||
)
|
||||
|
||||
// ownedFlagSpec describes one Sliver-owned flag that splitAliasArgs extracts
|
||||
// from the raw argument slice before the rest is handed to the alias payload.
|
||||
type ownedFlagSpec struct {
|
||||
long string // canonical pflag name, e.g. "process-arguments"
|
||||
short string // shorthand letter, e.g. "A" ("" if the flag has none)
|
||||
kind ownedFlagKind // how the value is consumed and validated
|
||||
}
|
||||
|
||||
// baseAliasOwnedFlags are registered for every alias command.
|
||||
var baseAliasOwnedFlags = []ownedFlagSpec{
|
||||
{long: "process", short: "p", kind: ownedString},
|
||||
{long: "process-arguments", short: "A", kind: ownedString},
|
||||
{long: "ppid", short: "P", kind: ownedUint32},
|
||||
{long: "save", short: "s", kind: ownedBool},
|
||||
{long: "timeout", short: "t", kind: ownedInt64},
|
||||
}
|
||||
|
||||
// assemblyAliasOwnedFlags are only owned when the alias wraps a .NET assembly;
|
||||
// for every other alias they are unknown tokens and pass through to the payload.
|
||||
var assemblyAliasOwnedFlags = []ownedFlagSpec{
|
||||
{long: "method", short: "m", kind: ownedString},
|
||||
{long: "class", short: "c", kind: ownedString},
|
||||
{long: "app-domain", short: "d", kind: ownedString},
|
||||
{long: "arch", short: "a", kind: ownedString},
|
||||
{long: "in-process", short: "i", kind: ownedBool},
|
||||
{long: "runtime", short: "r", kind: ownedString},
|
||||
{long: "amsi-bypass", short: "M", kind: ownedBool},
|
||||
{long: "etw-bypass", short: "E", kind: ownedBool},
|
||||
}
|
||||
|
||||
// aliasOwnedFlagSpecs returns the flag set owned by a particular alias.
|
||||
func aliasOwnedFlagSpecs(isAssembly bool) []ownedFlagSpec {
|
||||
if !isAssembly {
|
||||
return baseAliasOwnedFlags
|
||||
}
|
||||
specs := make([]ownedFlagSpec, 0, len(baseAliasOwnedFlags)+len(assemblyAliasOwnedFlags))
|
||||
specs = append(specs, baseAliasOwnedFlags...)
|
||||
return append(specs, assemblyAliasOwnedFlags...)
|
||||
}
|
||||
|
||||
// ownedFlagMatch is a raw token that spelled one of the Sliver-owned flags.
|
||||
type ownedFlagMatch struct {
|
||||
spec ownedFlagSpec
|
||||
value string // value from a "--flag=value" / "-f=value" spelling
|
||||
inline bool // whether the token carried its value inline
|
||||
}
|
||||
|
||||
// splitAliasArgs separates the Sliver-owned flags from the alias payload's own
|
||||
// arguments in a raw (DisableFlagParsing) argument slice. It accepts the long
|
||||
// forms --flag, --flag=value, --flag value and the shorthand forms -f, -f=value,
|
||||
// -f value, mirroring pflag. Two deliberate differences from pflag:
|
||||
//
|
||||
// - a glued shorthand value (-pnotepad) does NOT match --process: tokens
|
||||
// like the payload's own "-pid" must survive intact (#2264);
|
||||
// - after a literal "--" every token is copied through verbatim, preserving
|
||||
// the long-standing separator workaround.
|
||||
//
|
||||
// The returned map holds the raw string value of each flag that was provided;
|
||||
// callers re-publish the values with applyOwnedAliasFlags.
|
||||
func splitAliasArgs(raw []string, specs []ownedFlagSpec) (map[string]string, bool, []string, error) {
|
||||
values := make(map[string]string)
|
||||
extArgs := make([]string, 0, len(raw))
|
||||
byLong, byShort := ownedFlagTables(specs)
|
||||
|
||||
passthrough := false
|
||||
for i := 0; i < len(raw); i++ {
|
||||
tok := raw[i]
|
||||
|
||||
// Once we have seen "--", copy everything through untouched.
|
||||
if passthrough {
|
||||
extArgs = append(extArgs, tok)
|
||||
continue
|
||||
}
|
||||
if tok == "--" {
|
||||
passthrough = true
|
||||
continue
|
||||
}
|
||||
if tok == "--help" || tok == "-h" {
|
||||
return values, true, extArgs, nil
|
||||
}
|
||||
|
||||
match, ok := matchOwnedFlag(tok, byLong, byShort)
|
||||
if !ok {
|
||||
// Not one of ours (including glued shorthands and the payload's
|
||||
// own flags): the alias owns the token, pass it through untouched.
|
||||
extArgs = append(extArgs, tok)
|
||||
continue
|
||||
}
|
||||
i = takeOwnedValue(match, raw, i, values)
|
||||
}
|
||||
|
||||
if err := validateOwnedValues(specs, values); err != nil {
|
||||
return nil, false, nil, err
|
||||
}
|
||||
return values, false, extArgs, nil
|
||||
}
|
||||
|
||||
// ownedFlagTables indexes the specs by long name and shorthand.
|
||||
func ownedFlagTables(specs []ownedFlagSpec) (byLong, byShort map[string]ownedFlagSpec) {
|
||||
byLong = make(map[string]ownedFlagSpec, len(specs))
|
||||
byShort = make(map[string]ownedFlagSpec, len(specs))
|
||||
for _, spec := range specs {
|
||||
byLong[spec.long] = spec
|
||||
if spec.short != "" {
|
||||
byShort[spec.short] = spec
|
||||
}
|
||||
}
|
||||
return byLong, byShort
|
||||
}
|
||||
|
||||
// matchOwnedFlag checks whether tok spells one of the Sliver-owned flags, in
|
||||
// the long form (--name, --name=value) or shorthand form (-f, -f=value). A
|
||||
// glued shorthand value (-pnotepad) deliberately does not match: pflag would
|
||||
// read "-pid" as -p with value "id", which is exactly the mangling that broke
|
||||
// #2264, so such tokens must fall through to the payload.
|
||||
func matchOwnedFlag(tok string, byLong, byShort map[string]ownedFlagSpec) (ownedFlagMatch, bool) {
|
||||
var match ownedFlagMatch
|
||||
var name, value string
|
||||
var hasValue, isShort bool
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(tok, "--"):
|
||||
name, value, hasValue = strings.Cut(tok[2:], "=")
|
||||
case strings.HasPrefix(tok, "-") && len(tok) > 1:
|
||||
name, value, hasValue = strings.Cut(tok[1:], "=")
|
||||
isShort = len(name) == 1
|
||||
default:
|
||||
return match, false
|
||||
}
|
||||
|
||||
spec, ok := byLong[name]
|
||||
if isShort {
|
||||
spec, ok = byShort[name]
|
||||
}
|
||||
if !ok {
|
||||
return match, false
|
||||
}
|
||||
match.spec, match.value, match.inline = spec, value, hasValue
|
||||
return match, true
|
||||
}
|
||||
|
||||
// takeOwnedValue records the flag's value in values and returns the index of
|
||||
// the last raw token it consumed. The "--flag value" / "-f value" spellings
|
||||
// consume the following token; a bool without an inline value defaults to
|
||||
// true; a value flag as the very last token is dropped and keeps its default,
|
||||
// mirroring the extension behaviour.
|
||||
func takeOwnedValue(match ownedFlagMatch, raw []string, i int, values map[string]string) int {
|
||||
switch {
|
||||
case match.spec.kind == ownedBool:
|
||||
if !match.inline {
|
||||
match.value = "true"
|
||||
}
|
||||
values[match.spec.long] = match.value
|
||||
case match.inline:
|
||||
values[match.spec.long] = match.value
|
||||
case i+1 < len(raw):
|
||||
values[match.spec.long] = raw[i+1]
|
||||
return i + 1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// validateOwnedValues rejects non-numeric values for the numeric flag kinds,
|
||||
// the way pflag's typed values would have.
|
||||
func validateOwnedValues(specs []ownedFlagSpec, values map[string]string) error {
|
||||
for _, spec := range specs {
|
||||
value, provided := values[spec.long]
|
||||
if !provided {
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
switch spec.kind {
|
||||
case ownedUint32:
|
||||
_, err = strconv.ParseUint(value, 10, 32)
|
||||
case ownedInt64:
|
||||
_, err = strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value %q for --%s", value, spec.long)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyOwnedAliasFlags republishes the manually parsed Sliver-owned flag values
|
||||
// onto the command's flag set. DisableFlagParsing prevents pflag from
|
||||
// populating them, so readers such as runAliasCommand's Get* calls and
|
||||
// ActiveTarget.Request (which derives the gRPC timeout from --timeout) would
|
||||
// otherwise observe only the defaults.
|
||||
func applyOwnedAliasFlags(cmd *cobra.Command, values map[string]string) error {
|
||||
for name, value := range values {
|
||||
flag := cmd.Flags().Lookup(name)
|
||||
if flag == nil {
|
||||
continue // e.g. an assembly flag on a non-assembly alias
|
||||
}
|
||||
if err := flag.Value.Set(value); err != nil {
|
||||
return fmt.Errorf("invalid value %q for --%s: %w", value, name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package alias
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// TestSplitAliasArgs covers the argument passthrough regression from #2264:
|
||||
// the alias payload's own flags must reach the payload intact, while the
|
||||
// Sliver-owned flags are extracted and validated.
|
||||
func TestSplitAliasArgs(t *testing.T) {
|
||||
base := aliasOwnedFlagSpecs(false)
|
||||
assembly := aliasOwnedFlagSpecs(true)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
specs []ownedFlagSpec
|
||||
in []string
|
||||
values map[string]string
|
||||
help bool
|
||||
extArgs []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "payload flag without -- separator (#2264 regression)",
|
||||
specs: base,
|
||||
in: []string{"-pid", "6969"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"-pid", "6969"},
|
||||
},
|
||||
{
|
||||
name: "glued shorthand is not ours and passes through",
|
||||
specs: base,
|
||||
in: []string{"-pid"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"-pid"},
|
||||
},
|
||||
{
|
||||
name: "payload flag with -- separator (existing workaround)",
|
||||
specs: base,
|
||||
in: []string{"--", "--pid", "6969"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"--pid", "6969"},
|
||||
},
|
||||
{
|
||||
name: "owned flag after -- belongs to the payload",
|
||||
specs: base,
|
||||
in: []string{"--", "--save"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"--save"},
|
||||
},
|
||||
{
|
||||
name: "long flag with separate value, then payload args",
|
||||
specs: base,
|
||||
in: []string{"--process", "notepad.exe", "arg1", "arg2"},
|
||||
values: map[string]string{"process": "notepad.exe"},
|
||||
extArgs: []string{"arg1", "arg2"},
|
||||
},
|
||||
{
|
||||
name: "long flag with equals value",
|
||||
specs: base,
|
||||
in: []string{"--process=notepad.exe", "arg1"},
|
||||
values: map[string]string{"process": "notepad.exe"},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "shorthand with separate value",
|
||||
specs: base,
|
||||
in: []string{"-p", "notepad.exe", "arg1"},
|
||||
values: map[string]string{"process": "notepad.exe"},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "shorthand with equals value",
|
||||
specs: base,
|
||||
in: []string{"-p=notepad.exe"},
|
||||
values: map[string]string{"process": "notepad.exe"},
|
||||
extArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "bool shorthand and long false form",
|
||||
specs: base,
|
||||
in: []string{"-s", "--save=false", "arg1"},
|
||||
values: map[string]string{"save": "false"},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "quoted value containing spaces stays a single argument",
|
||||
specs: base,
|
||||
in: []string{"-A", "-kalc -kdisable", "arg1"},
|
||||
values: map[string]string{"process-arguments": "-kalc -kdisable"},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "valid uint32 and int64 values",
|
||||
specs: base,
|
||||
in: []string{"-P", "1234", "--timeout=90"},
|
||||
values: map[string]string{"ppid": "1234", "timeout": "90"},
|
||||
extArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "invalid uint32 value errors",
|
||||
specs: base,
|
||||
in: []string{"--ppid", "notanumber"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid int64 value errors",
|
||||
specs: base,
|
||||
in: []string{"--timeout=x"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "assembly flags are owned for assembly aliases",
|
||||
specs: assembly,
|
||||
in: []string{"--method", "Run", "--class", "Prog", "-i", "-M", "--arch", "x64", "payloadArg"},
|
||||
values: map[string]string{
|
||||
"method": "Run", "class": "Prog", "in-process": "true",
|
||||
"amsi-bypass": "true", "arch": "x64",
|
||||
},
|
||||
extArgs: []string{"payloadArg"},
|
||||
},
|
||||
{
|
||||
name: "assembly flags pass through for non-assembly aliases",
|
||||
specs: base,
|
||||
in: []string{"--method", "Run"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"--method", "Run"},
|
||||
},
|
||||
{
|
||||
name: "help flags are detected",
|
||||
specs: base,
|
||||
in: []string{"--help"},
|
||||
help: true,
|
||||
values: map[string]string{},
|
||||
extArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "short help is detected",
|
||||
specs: base,
|
||||
in: []string{"-h"},
|
||||
help: true,
|
||||
values: map[string]string{},
|
||||
extArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "value flag as last token keeps its default",
|
||||
specs: base,
|
||||
in: []string{"arg1", "--process"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
specs: base,
|
||||
in: nil,
|
||||
values: map[string]string{},
|
||||
extArgs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
values, help, extArgs, err := splitAliasArgs(tc.in, tc.specs)
|
||||
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got values=%#v help=%v extArgs=%#v", values, help, extArgs)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if help != tc.help {
|
||||
t.Errorf("help: got %v, want %v", help, tc.help)
|
||||
}
|
||||
if !reflect.DeepEqual(values, tc.values) {
|
||||
t.Errorf("values: got %#v, want %#v", values, tc.values)
|
||||
}
|
||||
if !reflect.DeepEqual(extArgs, tc.extArgs) {
|
||||
t.Errorf("extArgs: got %#v, want %#v", extArgs, tc.extArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyOwnedAliasFlags checks that manually parsed values are re-published
|
||||
// on the command's flag set for every value type an alias can own.
|
||||
func TestApplyOwnedAliasFlags(t *testing.T) {
|
||||
cmd := &cobra.Command{}
|
||||
f := pflag.NewFlagSet("alias-test", pflag.ContinueOnError)
|
||||
f.StringP("process", "p", "", "host process")
|
||||
f.StringP("process-arguments", "A", "", "host process arguments")
|
||||
f.Uint32P("ppid", "P", 0, "parent pid")
|
||||
f.BoolP("save", "s", false, "save output")
|
||||
f.Int64P("timeout", "t", 60, "timeout")
|
||||
f.BoolP("amsi-bypass", "M", false, "amsi bypass")
|
||||
cmd.Flags().AddFlagSet(f)
|
||||
|
||||
err := applyOwnedAliasFlags(cmd, map[string]string{
|
||||
"process": "notepad.exe",
|
||||
"process-arguments": "-k foo",
|
||||
"ppid": "1234",
|
||||
"save": "true",
|
||||
"timeout": "90",
|
||||
"amsi-bypass": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
for name, want := range map[string]interface{}{
|
||||
"process": "notepad.exe", "process-arguments": "-k foo",
|
||||
} {
|
||||
got, _ := cmd.Flags().GetString(name)
|
||||
if got != want.(string) {
|
||||
t.Errorf("%s: got %q, want %q", name, got, want)
|
||||
}
|
||||
}
|
||||
if ppid, _ := cmd.Flags().GetUint32("ppid"); ppid != 1234 {
|
||||
t.Errorf("ppid: got %d, want 1234", ppid)
|
||||
}
|
||||
if save, _ := cmd.Flags().GetBool("save"); !save {
|
||||
t.Error("save: got false, want true")
|
||||
}
|
||||
if timeout, _ := cmd.Flags().GetInt64("timeout"); timeout != 90 {
|
||||
t.Errorf("timeout: got %d, want 90", timeout)
|
||||
}
|
||||
if amsi, _ := cmd.Flags().GetBool("amsi-bypass"); !amsi {
|
||||
t.Error("amsi-bypass: got false, want true")
|
||||
}
|
||||
|
||||
// Unset flags keep their defaults.
|
||||
if inProc := cmd.Flags().Lookup("in-process"); inProc != nil {
|
||||
t.Error("in-process should not be registered on this command")
|
||||
}
|
||||
}
|
||||
@@ -215,8 +215,6 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
longHelp.WriteString(fmt.Sprintf("%s (%s):\t%s%s", strings.ToUpper(arg.Name), aType, optStr, arg.Desc))
|
||||
}
|
||||
}
|
||||
longHelp.WriteString("\n\n⚠️ If you're having issues passing arguments to the alias please read:\n")
|
||||
longHelp.WriteString("https://github.com/BishopFox/sliver/wiki/Aliases-&-Extensions#aliases-command-parsing")
|
||||
|
||||
// for each alias command, add a new app command
|
||||
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
|
||||
@@ -227,9 +225,14 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runAliasCommand(cmd, con, args)
|
||||
},
|
||||
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
GroupID: consts.AliasHelpGroup,
|
||||
Annotations: makeAliasPlatformFilters(aliasManifest),
|
||||
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
// DisableFlagParsing keeps the alias payload's own flags (which are
|
||||
// unknown to pflag) from being rejected or swallowed; we split the
|
||||
// Sliver-owned flags out of the raw slice ourselves in
|
||||
// runAliasCommand (#2264, same treatment as extensions).
|
||||
DisableFlagParsing: true,
|
||||
GroupID: consts.AliasHelpGroup,
|
||||
Annotations: makeAliasPlatformFilters(aliasManifest),
|
||||
}
|
||||
|
||||
if aliasManifest.IsAssembly {
|
||||
@@ -250,7 +253,7 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
|
||||
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
|
||||
f.BoolP("save", "s", false, "Save output to disk")
|
||||
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
addAliasCmd.Flags().AddFlagSet(f)
|
||||
|
||||
// Setup completions for alias arguments
|
||||
@@ -333,6 +336,27 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
return
|
||||
}
|
||||
aliasManifest := loadedAlias.Manifest
|
||||
|
||||
// The command runs with DisableFlagParsing, so args is the raw, unstripped
|
||||
// slice. Separate the Sliver-owned flags from the payload's own arguments
|
||||
// and re-publish them on the flag set (so the Get* readers below and
|
||||
// Request()'s --timeout still work), then hand only the payload arguments
|
||||
// to the execution paths (#2264, same treatment as extensions in #2323).
|
||||
ownedValues, helpRequested, payloadArgs, err := splitAliasArgs(args, aliasOwnedFlagSpecs(aliasManifest.IsAssembly))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if helpRequested {
|
||||
_ = cmd.Help()
|
||||
return
|
||||
}
|
||||
if err := applyOwnedAliasFlags(cmd, ownedValues); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
args = payloadArgs
|
||||
|
||||
binPath, err := aliasManifest.getFileForTarget(cmd.Name(), goos, goarch)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Fail to find alias file: %s\n", err)
|
||||
@@ -400,7 +424,13 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
|
||||
var outFilePath *os.File
|
||||
if save, _ := cmd.Flags().GetBool("save"); save {
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(session.GetHostname())))
|
||||
hostname := ""
|
||||
if session != nil {
|
||||
hostname = session.GetHostname()
|
||||
} else if beacon != nil {
|
||||
hostname = beacon.GetHostname()
|
||||
}
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(hostname)))
|
||||
outFilePath, err = os.CreateTemp("", outFile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
|
||||
@@ -164,7 +164,7 @@ func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
// Something is wrong with this entry
|
||||
pkgCache.Delete(value)
|
||||
pkgCache.Delete(key)
|
||||
return true
|
||||
}
|
||||
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package armory
|
||||
@@ -1,5 +1,23 @@
|
||||
package armory
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -37,7 +38,6 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/util/minisign"
|
||||
)
|
||||
|
||||
@@ -301,32 +301,44 @@ func getCommandsInCache(armoryPK string) []string {
|
||||
return commandNames
|
||||
}
|
||||
|
||||
func packageMatchesInstallName(cacheEntry pkgCacheEntry, name string) bool {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
return cacheEntry.Alias != nil && cacheEntry.Alias.CommandName == name
|
||||
}
|
||||
if cacheEntry.Extension == nil {
|
||||
return false
|
||||
}
|
||||
if cacheEntry.Pkg.CommandName == name || cacheEntry.Extension.PackageName == name {
|
||||
return true
|
||||
}
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if command.CommandName == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCacheEntry {
|
||||
packages := []*pkgCacheEntry{}
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.LastErr == nil {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
if cacheEntry.Alias.CommandName == name {
|
||||
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Alias.Version >= minimumVersion) {
|
||||
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if command.CommandName == name {
|
||||
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Extension.Version >= minimumVersion) {
|
||||
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cacheEntry.LastErr != nil || !packageMatchesInstallName(cacheEntry, name) {
|
||||
return true
|
||||
}
|
||||
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
|
||||
return true
|
||||
}
|
||||
|
||||
packageVersion := ""
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
packageVersion = cacheEntry.Alias.Version
|
||||
} else {
|
||||
packageVersion = cacheEntry.Extension.Version
|
||||
}
|
||||
if minimumVersion == "" || packageVersion >= minimumVersion {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -336,7 +348,7 @@ func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCac
|
||||
|
||||
func getPackageIDFromUser(name string, options map[string]string) string {
|
||||
selectedPackageKey := ""
|
||||
optionKeys := util.Keys(options)
|
||||
optionKeys := slices.Collect(maps.Keys(options))
|
||||
slices.Sort(optionKeys)
|
||||
// Add a cancel option
|
||||
optionKeys = append(optionKeys, doNotInstallOption)
|
||||
@@ -732,9 +744,7 @@ func installExtensionPackage(entry *pkgCacheEntry, promptToOverwrite bool, clien
|
||||
|
||||
con.Printf(console.Clearln + "\r") // Clear download message
|
||||
|
||||
extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
|
||||
|
||||
return nil
|
||||
return extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
|
||||
}
|
||||
|
||||
func writeArmoryTempFile(data []byte) (string, error) {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
)
|
||||
|
||||
func resetPackageCache(t *testing.T) {
|
||||
t.Helper()
|
||||
pkgCache.Range(func(key, _ any) bool {
|
||||
pkgCache.Delete(key)
|
||||
return true
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
pkgCache.Range(func(key, _ any) bool {
|
||||
pkgCache.Delete(key)
|
||||
return true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testExtensionCacheEntry() pkgCacheEntry {
|
||||
manifest := &extensions.ExtensionManifest{
|
||||
Name: "ADCS Request (Remote)",
|
||||
PackageName: "remote-adcs-package",
|
||||
Version: "v0.1.5",
|
||||
ArmoryName: "Default Armory",
|
||||
ArmoryPK: "armory-public-key",
|
||||
ExtCommand: []*extensions.ExtCommand{
|
||||
{CommandName: "remote-adcs-request"},
|
||||
{CommandName: "remote-adcs-request-on-behalf"},
|
||||
},
|
||||
}
|
||||
return pkgCacheEntry{
|
||||
ArmoryConfig: &assets.ArmoryConfig{PublicKey: "armory-public-key"},
|
||||
Pkg: ArmoryPackage{
|
||||
Name: "Remote ADCS package",
|
||||
CommandName: "remote-adcs-index",
|
||||
},
|
||||
Extension: manifest,
|
||||
ID: "remote-adcs-request-id",
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPackageForCommandAcceptsStableExtensionIdentities(t *testing.T) {
|
||||
resetPackageCache(t)
|
||||
entry := testExtensionCacheEntry()
|
||||
pkgCache.Store(entry.ID, entry)
|
||||
|
||||
for _, name := range []string{
|
||||
"remote-adcs-index",
|
||||
"remote-adcs-request",
|
||||
"remote-adcs-request-on-behalf",
|
||||
"remote-adcs-package",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
found, err := getPackageForCommand(name, "armory-public-key", "v0.1.5")
|
||||
if err != nil {
|
||||
t.Fatalf("lookup %q failed: %v", name, err)
|
||||
}
|
||||
if found.ID != entry.ID {
|
||||
t.Fatalf("lookup %q returned %q, want %q", name, found.ID, entry.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := getPackageForCommand("remote-adcs-package", "other-armory", "v0.1.5"); !errors.Is(err, ErrPackageNotFound) {
|
||||
t.Fatalf("wrong-armory lookup error = %v, want %v", err, ErrPackageNotFound)
|
||||
}
|
||||
if _, err := getPackageForCommand("remote-adcs-package", "armory-public-key", "v0.1.6"); !errors.Is(err, ErrPackageNotFound) {
|
||||
t.Fatalf("newer-version lookup error = %v, want %v", err, ErrPackageNotFound)
|
||||
}
|
||||
for _, name := range []string{"ADCS Request (Remote)", "Remote ADCS package", "unknown-package"} {
|
||||
if _, err := getPackageForCommand(name, "", ""); !errors.Is(err, ErrPackageNotFound) {
|
||||
t.Fatalf("unstable identity %q lookup error = %v, want %v", name, err, ErrPackageNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageMatchesInstallNameLeavesAliasesCommandOnly(t *testing.T) {
|
||||
entry := pkgCacheEntry{
|
||||
Pkg: ArmoryPackage{
|
||||
Name: "Human Alias Name",
|
||||
CommandName: "alias-command",
|
||||
IsAlias: true,
|
||||
},
|
||||
Alias: &alias.AliasManifest{CommandName: "alias-command"},
|
||||
}
|
||||
if packageMatchesInstallName(entry, "Human Alias Name") {
|
||||
t.Fatal("alias unexpectedly matched its display name")
|
||||
}
|
||||
if !packageMatchesInstallName(entry, "alias-command") {
|
||||
t.Fatal("alias did not match its command name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionManifestsMatch(t *testing.T) {
|
||||
manifest := func(name, packageName string, commands ...string) *extensions.ExtensionManifest {
|
||||
extCommands := make([]*extensions.ExtCommand, 0, len(commands))
|
||||
for _, command := range commands {
|
||||
extCommands = append(extCommands, &extensions.ExtCommand{CommandName: command})
|
||||
}
|
||||
return &extensions.ExtensionManifest{Name: name, PackageName: packageName, ExtCommand: extCommands}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
local *extensions.ExtensionManifest
|
||||
latest *extensions.ExtensionManifest
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "v2 package identity",
|
||||
local: manifest("Old Display Name", "stable-package", "old-command"),
|
||||
latest: manifest("New Display Name", "stable-package", "new-command"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different v2 packages",
|
||||
local: manifest("Shared Display Name", "package-one", "shared-command"),
|
||||
latest: manifest("Shared Display Name", "package-two", "shared-command"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "v2 does not downgrade to legacy identity",
|
||||
local: manifest("Human Display Name", "stable-package", "shared-command"),
|
||||
latest: manifest("legacy-command", "", "shared-command"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "legacy command identity",
|
||||
local: manifest("legacy-command", "", "legacy-command"),
|
||||
latest: manifest("Human Display Name", "stable-package", "other-command", "legacy-command"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "display names are not identity",
|
||||
local: manifest("Shared Name", "", "old-command"),
|
||||
latest: manifest("Shared Name", "stable-package", "new-command"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unrelated legacy package",
|
||||
local: manifest("Old Name", "", "old-command"),
|
||||
latest: manifest("New Name", "stable-package", "new-command"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := extensionManifestsMatch(test.local, test.latest); got != test.want {
|
||||
t.Fatalf("extensionManifestsMatch() = %t, want %t", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionUpdateDisplayNameResolvesPackage(t *testing.T) {
|
||||
resetPackageCache(t)
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
|
||||
installedManifest := []byte(`{
|
||||
"name":"ADCS Request (Remote)",
|
||||
"package_name":"remote-adcs-package",
|
||||
"version":"v0.1.3",
|
||||
"commands":[{
|
||||
"command_name":"remote-adcs-request",
|
||||
"help":"Request a certificate",
|
||||
"files":[{"os":"windows","arch":"amd64","path":"adcs_request.x64.o"}]
|
||||
}]
|
||||
}`)
|
||||
installDir := filepath.Join(assets.GetExtensionsDir(), "remote-adcs-request")
|
||||
if err := os.MkdirAll(installDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(installDir, extensions.ManifestFileName), installedManifest, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entry := testExtensionCacheEntry()
|
||||
pkgCache.Store(entry.ID, entry)
|
||||
otherEntry := testExtensionCacheEntry()
|
||||
otherEntry.ID = "other-armory-package-id"
|
||||
otherEntry.ArmoryConfig = &assets.ArmoryConfig{PublicKey: "other-armory"}
|
||||
otherManifest := *otherEntry.Extension
|
||||
otherManifest.Version = "v9.0.0"
|
||||
otherManifest.ArmoryName = "Other Armory"
|
||||
otherManifest.ArmoryPK = "other-armory"
|
||||
otherEntry.Extension = &otherManifest
|
||||
pkgCache.Store(otherEntry.ID, otherEntry)
|
||||
|
||||
updates := checkForExtensionUpdates("armory-public-key")
|
||||
versionInfo, ok := updates["ADCS Request (Remote)"]
|
||||
if !ok {
|
||||
t.Fatal("display-name update was not detected")
|
||||
}
|
||||
if versionInfo.OldVersion != "v0.1.3" || versionInfo.NewVersion != "v0.1.5" {
|
||||
t.Fatalf("unexpected versions: %#v", versionInfo)
|
||||
}
|
||||
if versionInfo.PackageID != entry.ID {
|
||||
t.Fatalf("update package ID = %q, want %q", versionInfo.PackageID, entry.ID)
|
||||
}
|
||||
|
||||
found := packageCacheLookupByID(versionInfo.PackageID)
|
||||
if found == nil {
|
||||
t.Fatal("detected update package is missing from the cache")
|
||||
}
|
||||
if found.ID != entry.ID {
|
||||
t.Fatalf("resolved package %q, want %q", found.ID, entry.ID)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,23 @@
|
||||
package armory
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -21,7 +21,9 @@ package armory
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
@@ -33,13 +35,13 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
)
|
||||
|
||||
type VersionInformation struct {
|
||||
OldVersion string
|
||||
NewVersion string
|
||||
ArmoryName string
|
||||
PackageID string
|
||||
}
|
||||
|
||||
type PackageType uint
|
||||
@@ -124,9 +126,9 @@ func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
updatedPackage, err := getPackageForCommand(update.Name, armoryPK, extVersionInfo.NewVersion)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, err)
|
||||
updatedPackage := packageCacheLookupByID(extVersionInfo.PackageID)
|
||||
if updatedPackage == nil {
|
||||
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, ErrPackageNotFound)
|
||||
continue
|
||||
}
|
||||
err = installExtensionPackage(updatedPackage, false, clientConfig, con)
|
||||
@@ -173,8 +175,21 @@ func checkForAliasUpdates(armoryPK string) map[string]VersionInformation {
|
||||
return results
|
||||
}
|
||||
|
||||
func extensionManifestsMatch(localManifest, latestManifest *extensions.ExtensionManifest) bool {
|
||||
if localManifest.PackageName != "" {
|
||||
return latestManifest.PackageName != "" && localManifest.PackageName == latestManifest.PackageName
|
||||
}
|
||||
for _, localCommand := range localManifest.ExtCommand {
|
||||
for _, latestCommand := range latestManifest.ExtCommand {
|
||||
if localCommand.CommandName == latestCommand.CommandName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
_, cachedExtensions := packageManifestsInCache()
|
||||
// Return a map of extension name to minimum version to install
|
||||
results := make(map[string]VersionInformation)
|
||||
for _, extManifestPath := range assets.GetInstalledExtensionManifests() {
|
||||
@@ -186,7 +201,12 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, latestExt := range cachedExtensions {
|
||||
pkgCache.Range(func(_, value interface{}) bool {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok || cacheEntry.LastErr != nil || cacheEntry.Pkg.IsAlias || cacheEntry.Extension == nil {
|
||||
return true
|
||||
}
|
||||
latestExt := cacheEntry.Extension
|
||||
/*
|
||||
We used to check if the version identifiers were different between the installed version and the
|
||||
version in the armory. This worked when we had one armory, but with multiple potential armories,
|
||||
@@ -194,16 +214,22 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
so we will rely on the author of the package incrementing the version identifier somehow to determine
|
||||
if a package is newer.
|
||||
*/
|
||||
if latestExt.Name == localManifest.Name && latestExt.Version > localManifest.Version {
|
||||
if extensionManifestsMatch(localManifest, latestExt) && latestExt.Version > localManifest.Version {
|
||||
if latestExt.ArmoryPK == armoryPK || armoryPK == "" {
|
||||
results[localManifest.Name] = VersionInformation{
|
||||
OldVersion: localManifest.Version,
|
||||
NewVersion: latestExt.Version,
|
||||
ArmoryName: latestExt.ArmoryName,
|
||||
current, found := results[localManifest.Name]
|
||||
if !found || latestExt.Version > current.NewVersion ||
|
||||
(latestExt.Version == current.NewVersion && cacheEntry.ID < current.PackageID) {
|
||||
results[localManifest.Name] = VersionInformation{
|
||||
OldVersion: localManifest.Version,
|
||||
NewVersion: latestExt.Version,
|
||||
ArmoryName: latestExt.ArmoryName,
|
||||
PackageID: cacheEntry.ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
@@ -219,8 +245,8 @@ func sortUpdateIdentifiers(aliasUpdates, extensionUpdates map[string]VersionInfo
|
||||
|
||||
result := []UpdateIdentifier{}
|
||||
|
||||
aliasNames := util.Keys(aliasUpdates)
|
||||
extensionNames := util.Keys(extensionUpdates)
|
||||
aliasNames := slices.Collect(maps.Keys(aliasUpdates))
|
||||
extensionNames := slices.Collect(maps.Keys(extensionUpdates))
|
||||
for _, name := range aliasNames {
|
||||
result = append(result, UpdateIdentifier{
|
||||
Type: AliasPackage,
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/kill"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/command/targettable"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
@@ -121,50 +122,30 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
if err != nil {
|
||||
width = 999
|
||||
}
|
||||
return renderBeaconsAtWidth(beacons, filter, filterRegex, con, width, time.Now())
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
wideTermWidth := con.Settings.SmallTermWidth < width
|
||||
// renderBeaconsAtWidth uses one reference time for all candidate layouts,
|
||||
// preventing a second boundary from changing the table width while it is fitted.
|
||||
func renderBeaconsAtWidth(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, width int, now time.Time) table.Writer {
|
||||
windowsBeaconInList := false
|
||||
for _, beacon := range beacons {
|
||||
if beacon.OS == "windows" {
|
||||
windowsBeaconInList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if wideTermWidth {
|
||||
if windowsBeaconInList {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
"Integrity",
|
||||
"Operating System",
|
||||
"Locale",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
} else {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
"Operating System",
|
||||
"Locale",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
return targettable.Fit(width, windowsBeaconInList, func(layout targettable.Layout) table.Writer {
|
||||
return buildBeaconTable(beacons, filter, filterRegex, con, layout, windowsBeaconInList, now)
|
||||
})
|
||||
}
|
||||
|
||||
func buildBeaconTable(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, layout targettable.Layout, windowsBeaconInList bool, now time.Time) table.Writer {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
includeIntegrity := windowsBeaconInList && layout.IncludeIntegrity
|
||||
if layout.Compact {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
@@ -175,22 +156,84 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
} else {
|
||||
header := table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
}
|
||||
if includeIntegrity {
|
||||
header = append(header, "Integrity")
|
||||
}
|
||||
header = append(header, "Operating System")
|
||||
if layout.IncludeLocale {
|
||||
header = append(header, "Locale")
|
||||
}
|
||||
header = append(header, "Last Check-in", "Next Check-in")
|
||||
tw.AppendHeader(header)
|
||||
}
|
||||
|
||||
activeBeaconID := ""
|
||||
if con.ActiveTarget != nil {
|
||||
if activeBeacon := con.ActiveTarget.GetBeacon(); activeBeacon != nil {
|
||||
activeBeaconID = activeBeacon.ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, beacon := range beacons {
|
||||
integrity := beacon.Integrity
|
||||
if integrity == "" {
|
||||
integrity = "-"
|
||||
}
|
||||
username := strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")
|
||||
lastCheckin := time.Unix(beacon.LastCheckin, 0)
|
||||
nextCheckin := time.Unix(beacon.NextCheckin, 0)
|
||||
canonicalEntries := []string{
|
||||
strings.Split(beacon.ID, "-")[0],
|
||||
beacon.Name,
|
||||
fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount),
|
||||
beacon.Transport,
|
||||
beacon.RemoteAddress,
|
||||
beacon.Hostname,
|
||||
username,
|
||||
fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID),
|
||||
integrity,
|
||||
fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch),
|
||||
beacon.Locale,
|
||||
formatBeaconDateDelta(lastCheckin, now, true, false),
|
||||
formatBeaconDateDelta(nextCheckin, now, true, false),
|
||||
}
|
||||
if !beaconMatchesFilter(canonicalEntries, filter, filterRegex) {
|
||||
continue
|
||||
}
|
||||
|
||||
style := console.StyleNormal
|
||||
activeBeacon := con.ActiveTarget.GetBeacon()
|
||||
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
|
||||
if activeBeaconID == beacon.ID {
|
||||
style = console.StyleGreen
|
||||
}
|
||||
if beacon.Integrity == "" {
|
||||
beacon.Integrity = "-"
|
||||
}
|
||||
|
||||
// We need a slice of strings so we can apply filters
|
||||
var rowEntries []string
|
||||
|
||||
if wideTermWidth {
|
||||
if layout.Compact {
|
||||
rowEntries = []string{
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(username),
|
||||
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||
formatBeaconDateDelta(lastCheckin, now, false, false),
|
||||
formatBeaconDateDelta(nextCheckin, now, false, true),
|
||||
}
|
||||
} else {
|
||||
processName := beacon.Filename
|
||||
if layout.ProcessBasename {
|
||||
processName = targettable.ProcessName(processName)
|
||||
}
|
||||
rowEntries = []string{
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
@@ -198,56 +241,69 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.RemoteAddress),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
style.Render(fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID)),
|
||||
style.Render(username),
|
||||
style.Render(fmt.Sprintf("%s (%d)", processName, beacon.PID)),
|
||||
}
|
||||
|
||||
if windowsBeaconInList {
|
||||
rowEntries = append(rowEntries, style.Render(beacon.Integrity))
|
||||
if includeIntegrity {
|
||||
rowEntries = append(rowEntries, style.Render(integrity))
|
||||
}
|
||||
|
||||
rowEntries = append(rowEntries, []string{
|
||||
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||
style.Render(beacon.Locale),
|
||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||
}...)
|
||||
} else {
|
||||
rowEntries = []string{
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||
rowEntries = append(rowEntries, style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)))
|
||||
if layout.IncludeLocale {
|
||||
rowEntries = append(rowEntries, style.Render(beacon.Locale))
|
||||
}
|
||||
rowEntries = append(rowEntries,
|
||||
formatBeaconDateDelta(lastCheckin, now, true, false),
|
||||
formatBeaconDateDelta(nextCheckin, now, true, true),
|
||||
)
|
||||
}
|
||||
// Build the row struct
|
||||
|
||||
row := table.Row{}
|
||||
for _, entry := range rowEntries {
|
||||
row = append(row, entry)
|
||||
}
|
||||
// Apply filters if any
|
||||
if filter == "" && filterRegex == nil {
|
||||
tw.AppendRow(row)
|
||||
} else {
|
||||
for _, rowEntry := range rowEntries {
|
||||
if filter != "" {
|
||||
if strings.Contains(rowEntry, filter) {
|
||||
tw.AppendRow(row)
|
||||
break
|
||||
}
|
||||
}
|
||||
if filterRegex != nil {
|
||||
if filterRegex.MatchString(rowEntry) {
|
||||
tw.AppendRow(row)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tw.AppendRow(row)
|
||||
}
|
||||
return tw
|
||||
}
|
||||
|
||||
func beaconMatchesFilter(entries []string, filter string, filterRegex *regexp.Regexp) bool {
|
||||
if filter == "" && filterRegex == nil {
|
||||
return true
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if filter != "" && strings.Contains(entry, filter) {
|
||||
return true
|
||||
}
|
||||
if filterRegex != nil && filterRegex.MatchString(entry) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatBeaconDateDelta(timestamp time.Time, now time.Time, includeDate bool, color bool) string {
|
||||
date := timestamp.Format(time.UnixDate)
|
||||
interval := ""
|
||||
if timestamp.Before(now) {
|
||||
delta := now.Sub(timestamp).Round(time.Second)
|
||||
if includeDate {
|
||||
interval = fmt.Sprintf("%s (%s ago)", date, delta)
|
||||
} else {
|
||||
interval = delta.String()
|
||||
}
|
||||
if color {
|
||||
interval = console.StyleBoldRed.Render(interval)
|
||||
}
|
||||
} else {
|
||||
delta := timestamp.Sub(now).Round(time.Second)
|
||||
if includeDate {
|
||||
interval = fmt.Sprintf("%s (in %s)", date, delta)
|
||||
} else {
|
||||
interval = delta.String()
|
||||
}
|
||||
if color {
|
||||
interval = console.StyleBoldGreen.Render(interval)
|
||||
}
|
||||
}
|
||||
return interval
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package beacons
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/targettable"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
var beaconTableTestNow = time.Date(2026, time.September, 3, 21, 25, 41, 0, time.UTC)
|
||||
|
||||
func TestRenderBeaconsUsesFullProcessPathWhenItFits(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
|
||||
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
|
||||
|
||||
rendered := renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
|
||||
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
|
||||
t.Fatalf("full process path missing from table:\n%s", rendered)
|
||||
}
|
||||
if !strings.Contains(rendered, "(36s ago)") || !strings.Contains(rendered, "(in 24s)") {
|
||||
t.Fatalf("wide check-in timestamps changed unexpectedly:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, width)
|
||||
}
|
||||
|
||||
func TestRenderBeaconsUsesPortableProcessBasenameWhenRequired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
beacon *clientpb.Beacon
|
||||
hasIntegrity bool
|
||||
wantProcess string
|
||||
}{
|
||||
{
|
||||
name: "windows path",
|
||||
beacon: testWindowsBeacon(),
|
||||
hasIntegrity: true,
|
||||
wantProcess: "winterfell.exe (2020)",
|
||||
},
|
||||
{
|
||||
name: "posix path",
|
||||
beacon: testLinuxBeacon(),
|
||||
hasIntegrity: false,
|
||||
wantProcess: "winterfell-agent (2020)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
beacons := []*clientpb.Beacon{tt.beacon}
|
||||
fullLayout := targettable.Layout{
|
||||
IncludeIntegrity: tt.hasIntegrity,
|
||||
IncludeLocale: true,
|
||||
}
|
||||
basenameLayout := fullLayout
|
||||
basenameLayout.ProcessBasename = true
|
||||
fullWidth := beaconCandidateWidth(t, beacons, fullLayout, tt.hasIntegrity)
|
||||
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, tt.hasIntegrity)
|
||||
if basenameWidth >= fullWidth {
|
||||
t.Fatalf("basename candidate width %d is not narrower than full-path width %d", basenameWidth, fullWidth)
|
||||
}
|
||||
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, basenameWidth)
|
||||
if !strings.Contains(rendered, tt.wantProcess) {
|
||||
t.Fatalf("process basename missing from table:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, tt.beacon.Filename) {
|
||||
t.Fatalf("full process path remained in basename layout:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, basenameWidth)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBeaconsOmitsIntegrityBeforeLocale(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
withAll := targettable.Layout{
|
||||
ProcessBasename: true,
|
||||
IncludeIntegrity: true,
|
||||
IncludeLocale: true,
|
||||
}
|
||||
withoutIntegrity := withAll
|
||||
withoutIntegrity.IncludeIntegrity = false
|
||||
withoutLocale := withoutIntegrity
|
||||
withoutLocale.IncludeLocale = false
|
||||
|
||||
allWidth := beaconCandidateWidth(t, beacons, withAll, true)
|
||||
withoutIntegrityWidth := beaconCandidateWidth(t, beacons, withoutIntegrity, true)
|
||||
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
|
||||
if !(withoutLocaleWidth < withoutIntegrityWidth && withoutIntegrityWidth < allWidth) {
|
||||
t.Fatalf("candidate widths did not shrink in omission order: all=%d no-integrity=%d no-locale=%d", allWidth, withoutIntegrityWidth, withoutLocaleWidth)
|
||||
}
|
||||
|
||||
withoutIntegrityRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutIntegrityWidth)
|
||||
assertHeaderPresence(t, withoutIntegrityRendered, "Integrity", false)
|
||||
assertHeaderPresence(t, withoutIntegrityRendered, "Locale", true)
|
||||
assertBeaconTableFits(t, withoutIntegrityRendered, withoutIntegrityWidth)
|
||||
|
||||
withoutLocaleRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutLocaleWidth)
|
||||
assertHeaderPresence(t, withoutLocaleRendered, "Integrity", false)
|
||||
assertHeaderPresence(t, withoutLocaleRendered, "Locale", false)
|
||||
assertBeaconTableFits(t, withoutLocaleRendered, withoutLocaleWidth)
|
||||
}
|
||||
|
||||
func TestRenderBeaconsDoesNotAddIntegrityForNonWindowsTargets(t *testing.T) {
|
||||
beacon := testLinuxBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
layout := targettable.Layout{IncludeLocale: true}
|
||||
width := beaconCandidateWidth(t, beacons, layout, false)
|
||||
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, width)
|
||||
assertHeaderPresence(t, rendered, "Integrity", false)
|
||||
assertHeaderPresence(t, rendered, "Locale", true)
|
||||
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
|
||||
t.Fatalf("non-Windows full process path missing from table:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, width)
|
||||
}
|
||||
|
||||
func TestRenderBeaconsFiltersCanonicalFullRows(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
basenameLayout := targettable.Layout{
|
||||
ProcessBasename: true,
|
||||
IncludeIntegrity: true,
|
||||
IncludeLocale: true,
|
||||
}
|
||||
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, true)
|
||||
withoutLocale := basenameLayout
|
||||
withoutLocale.IncludeIntegrity = false
|
||||
withoutLocale.IncludeLocale = false
|
||||
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
|
||||
|
||||
t.Run("full process path survives basename projection", func(t *testing.T) {
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, `AppData\Local\Temp`, nil, basenameWidth)
|
||||
if !strings.Contains(rendered, beacon.Name) {
|
||||
t.Fatalf("filter matching canonical process path removed beacon:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, `AppData\Local\Temp`) {
|
||||
t.Fatalf("basename layout unexpectedly displayed filtered directory:\n%s", rendered)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hidden integrity survives column projection", func(t *testing.T) {
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", regexp.MustCompile(`^High$`), withoutLocaleWidth)
|
||||
if !strings.Contains(rendered, beacon.Name) {
|
||||
t.Fatalf("filter matching hidden integrity removed beacon:\n%s", rendered)
|
||||
}
|
||||
assertHeaderPresence(t, rendered, "Integrity", false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRenderBeaconsDoesNotMutateEmptyIntegrity(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacon.Integrity = ""
|
||||
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
|
||||
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
|
||||
|
||||
_ = renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
|
||||
if beacon.Integrity != "" {
|
||||
t.Fatalf("beacon integrity mutated to %q", beacon.Integrity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBeaconsCompactFallbackPreservesLegacyColumnsAndTimes(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
compactWidth := beaconCandidateWidth(t, beacons, targettable.Layout{Compact: true}, true)
|
||||
withoutOptionalWidth := beaconCandidateWidth(t, beacons, targettable.Layout{ProcessBasename: true}, true)
|
||||
if compactWidth >= withoutOptionalWidth {
|
||||
t.Fatalf("test fixture does not require compact fallback: compact=%d detailed=%d", compactWidth, withoutOptionalWidth)
|
||||
}
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, compactWidth)
|
||||
|
||||
for _, omitted := range []string{"Tasks", "Remote Address", "Process (PID)", "Integrity", "Locale"} {
|
||||
assertHeaderPresence(t, rendered, omitted, false)
|
||||
}
|
||||
if !strings.Contains(rendered, "36s") || !strings.Contains(rendered, "24s") {
|
||||
t.Fatalf("compact relative check-in times missing:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, "36s ago") || strings.Contains(rendered, "in 24s") {
|
||||
t.Fatalf("compact fallback did not preserve legacy timestamp format:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, compactWidth)
|
||||
}
|
||||
|
||||
func testWindowsBeacon() *clientpb.Beacon {
|
||||
return &clientpb.Beacon{
|
||||
ID: "0c9f6b27-1111-2222-3333-444444444444",
|
||||
Name: "winterfell-session",
|
||||
TasksCount: 3,
|
||||
TasksCountCompleted: 2,
|
||||
Transport: "mtls",
|
||||
RemoteAddress: "10.13.37.11:49760",
|
||||
Hostname: "winterfell",
|
||||
Username: `winterfell\vagrant`,
|
||||
Filename: `C:\Users\vagrant\AppData\Local\Temp\sliver-range\winterfell.exe`,
|
||||
PID: 2020,
|
||||
Integrity: "High",
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Locale: "en-US",
|
||||
LastCheckin: beaconTableTestNow.Add(-36 * time.Second).Unix(),
|
||||
NextCheckin: beaconTableTestNow.Add(24 * time.Second).Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func testLinuxBeacon() *clientpb.Beacon {
|
||||
beacon := testWindowsBeacon()
|
||||
beacon.OS = "linux"
|
||||
beacon.Hostname = "winterfell-linux"
|
||||
beacon.Username = "vagrant"
|
||||
beacon.Filename = "/opt/sliver/temporary/build/output/winterfell-agent"
|
||||
beacon.Integrity = ""
|
||||
return beacon
|
||||
}
|
||||
|
||||
func testBeaconConsole() *console.SliverClient {
|
||||
return &console.SliverClient{
|
||||
Settings: &assets.ClientSettings{TableStyle: "SliverDefault"},
|
||||
}
|
||||
}
|
||||
|
||||
func beaconCandidateWidth(t *testing.T, beacons []*clientpb.Beacon, layout targettable.Layout, hasIntegrity bool) int {
|
||||
t.Helper()
|
||||
tw := buildBeaconTable(beacons, "", nil, testBeaconConsole(), layout, hasIntegrity, beaconTableTestNow)
|
||||
return text.LongestLineLen(tw.Render())
|
||||
}
|
||||
|
||||
func renderBeaconTableAtWidth(t *testing.T, beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, width int) string {
|
||||
t.Helper()
|
||||
tw := renderBeaconsAtWidth(beacons, filter, filterRegex, testBeaconConsole(), width, beaconTableTestNow)
|
||||
return text.StripEscape(tw.Render())
|
||||
}
|
||||
|
||||
func assertBeaconTableFits(t *testing.T, rendered string, width int) {
|
||||
t.Helper()
|
||||
if got := text.LongestLineLen(rendered); got > width {
|
||||
t.Fatalf("rendered table width %d exceeds terminal width %d:\n%s", got, width, rendered)
|
||||
}
|
||||
}
|
||||
|
||||
func assertHeaderPresence(t *testing.T, rendered string, header string, want bool) {
|
||||
t.Helper()
|
||||
headerLine := strings.SplitN(rendered, "\n", 2)[0]
|
||||
if got := strings.Contains(strings.ToLower(headerLine), strings.ToLower(header)); got != want {
|
||||
t.Fatalf("header %q presence = %v, want %v; header line: %s", header, got, want, headerLine)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
},
|
||||
}
|
||||
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("kill", "k", "", "kill the designated beacon")
|
||||
@@ -145,8 +145,8 @@ func filterCompletionValues(values []string, prefix string) []string {
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
if before, _, ok := strings.Cut(value, "\t"); ok {
|
||||
candidate = before
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
|
||||
@@ -28,11 +28,11 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
|
||||
@@ -84,8 +84,8 @@ func filterCompletionValues(values []string, prefix string) []string {
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
if before, _, ok := strings.Cut(value, "\t"); ok {
|
||||
candidate = before
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'crack' command group for the Sliver client console. Handlers map Cobra invocations to crack workflows such as crack files.
|
||||
Implements the 'crack' command group for the Sliver client console. Handlers map Cobra invocations to crack workflows such as cached benchmark inspection, durable jobs, real-time monitoring, and crack files.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `commands.go` – Defines the crack command hierarchy, attaching operations for stations and file management.
|
||||
- `crack-benchmarks.go` – Displays bounded cached crackstation benchmark summaries, freshness metadata, and representative or complete hash-mode rates.
|
||||
- `crack-files.go` – Manages cracking asset uploads, listings, and removals, handling streaming transfers and chunking.
|
||||
- `crack-jobs.go` – Lists durable cracking jobs, renders detailed task progress, manages pause/resume/cancel lifecycle actions, and removes terminal jobs after confirmation.
|
||||
- `top.go` – Preserves the crack command's compatibility entry point and delegates to the dedicated monitor package.
|
||||
- `top/` – Owns the full-screen Bubble Tea monitor, its lean real-time snapshot loader, live telemetry, history, charts, actions, and tests.
|
||||
- `internal/hashcatdisplay/` – Shares Hashcat mode names and rate formatting between the command handlers and monitor without coupling the packages.
|
||||
- `crack.go` – Implements station status reporting, health checks, and list rendering for distributed cracking nodes.
|
||||
- `helpers.go` – Supplies completion helpers and byte formatting utilities for crack subcommands.
|
||||
@@ -1,6 +1,8 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -25,8 +27,15 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
flags.Bind("", true, crackCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("", false, crackCmd, func(f *pflag.FlagSet) {
|
||||
bindCrackFlags(f)
|
||||
})
|
||||
flags.BindFlagCompletions(crackCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["input"] = CrackWordlistCompleter(con)
|
||||
(*comp)["rules-file"] = CrackRulesCompleter(con)
|
||||
(*comp)["markov-hcstat2"] = CrackHcstat2Completer(con)
|
||||
})
|
||||
|
||||
crackStationsCmd := &cobra.Command{
|
||||
Use: consts.StationsStr,
|
||||
@@ -41,6 +50,112 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
})
|
||||
crackCmd.AddCommand(crackStationsCmd)
|
||||
|
||||
crackBenchmarksCmd := &cobra.Command{
|
||||
Use: consts.BenchmarksStr,
|
||||
Short: "Show cached crackstation benchmark results",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
BenchmarksCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, crackBenchmarksCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("all", "a", false, "show every cached hash mode")
|
||||
})
|
||||
crackCmd.AddCommand(crackBenchmarksCmd)
|
||||
|
||||
crackJobsCmd := &cobra.Command{
|
||||
Use: "jobs",
|
||||
Short: "List hash cracking jobs",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobsCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", true, crackJobsCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
crackCmd.AddCommand(crackJobsCmd)
|
||||
|
||||
crackTopCmd := &cobra.Command{
|
||||
Use: "top",
|
||||
Short: "Monitor crack jobs and crackstations in real time",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackTopCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, crackTopCmd, func(f *pflag.FlagSet) {
|
||||
f.Duration("poll-interval", time.Second, "crack job and crackstation refresh interval")
|
||||
})
|
||||
crackCmd.AddCommand(crackTopCmd)
|
||||
|
||||
crackJobCmd := &cobra.Command{
|
||||
Use: "job [id]",
|
||||
Short: "Show a hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", true, crackJobCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("", false, crackJobCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("watch", "w", false, "watch until the job reaches a terminal state")
|
||||
f.Duration("poll-interval", time.Second, "job status polling interval")
|
||||
})
|
||||
carapace.Gen(crackJobCmd).PositionalCompletion(CrackJobIDCompleter(con).Usage("crack job ID (leave empty to select)"))
|
||||
registerCrackJobIDCompletion(crackJobCmd, con)
|
||||
|
||||
crackJobCancelCmd := &cobra.Command{
|
||||
Use: consts.CancelStr + " [id]",
|
||||
Short: "Cancel an active or paused hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobCancelCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(crackJobCancelCmd).PositionalCompletion(crackJobIDCompleter(con, crackJobCancellable).Usage("active or paused crack job ID (leave empty to select)"))
|
||||
registerCrackJobIDCompletionFiltered(crackJobCancelCmd, con, crackJobCancellable)
|
||||
crackJobCmd.AddCommand(crackJobCancelCmd)
|
||||
|
||||
crackJobPauseCmd := &cobra.Command{
|
||||
Use: "pause [id]",
|
||||
Short: "Pause an active hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobPauseCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(crackJobPauseCmd).PositionalCompletion(crackJobIDCompleter(con, crackJobPausable).Usage("active crack job ID (leave empty to select)"))
|
||||
registerCrackJobIDCompletionFiltered(crackJobPauseCmd, con, crackJobPausable)
|
||||
crackJobCmd.AddCommand(crackJobPauseCmd)
|
||||
|
||||
crackJobResumeCmd := &cobra.Command{
|
||||
Use: "resume [id]",
|
||||
Short: "Resume a paused hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobResumeCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(crackJobResumeCmd).PositionalCompletion(crackJobIDCompleter(con, crackJobResumable).Usage("paused crack job ID (leave empty to select)"))
|
||||
registerCrackJobIDCompletionFiltered(crackJobResumeCmd, con, crackJobResumable)
|
||||
crackJobCmd.AddCommand(crackJobResumeCmd)
|
||||
|
||||
crackJobRmCmd := &cobra.Command{
|
||||
Use: consts.RmStr + " [id]",
|
||||
Short: "Remove a completed, failed, or cancelled hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobRmCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(crackJobRmCmd).PositionalCompletion(crackTerminalJobIDCompleter(con).Usage("terminal crack job ID (leave empty to select)"))
|
||||
registerCrackTerminalJobIDCompletion(crackJobRmCmd, con)
|
||||
crackJobCmd.AddCommand(crackJobRmCmd)
|
||||
crackCmd.AddCommand(crackJobCmd)
|
||||
|
||||
wordlistsCmd := &cobra.Command{
|
||||
Use: consts.WordlistsStr,
|
||||
Short: "Manage wordlists",
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
type backendDeviceDetails struct {
|
||||
deviceType string
|
||||
vendor string
|
||||
vendorID int32
|
||||
version string
|
||||
runtimeVersion string
|
||||
deviceID uint32
|
||||
deviceAlias *uint32
|
||||
preferredThreadSize *uint32
|
||||
memoryUnified *bool
|
||||
localMemory string
|
||||
cacheSize string
|
||||
memoryAllocPerBlock string
|
||||
pciAddress string
|
||||
}
|
||||
|
||||
func hasReportedBackendDevice(cracker *clientpb.Crackstation) bool {
|
||||
for _, device := range cracker.CUDA {
|
||||
if device != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, device := range cracker.HIP {
|
||||
if device != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, device := range cracker.Metal {
|
||||
if device != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, device := range cracker.OpenCL {
|
||||
if device != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appendCrackstationDetailRows(tw table.Writer, cracker *clientpb.Crackstation) {
|
||||
if cracker.HostUUID != "" {
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Host UUID"), safeCrackCell(cracker.HostUUID)})
|
||||
}
|
||||
if cracker.Version != "" {
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Crackstation Version"), safeCrackCell(cracker.Version)})
|
||||
}
|
||||
}
|
||||
|
||||
func appendCUDABackendRows(tw table.Writer, device *clientpb.CUDABackendInfo, level uint32) {
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
appendBackendSummaryRows(tw, "CUDA Device", formatCUDADevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
|
||||
if level < 2 {
|
||||
return
|
||||
}
|
||||
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
|
||||
deviceType: device.Type,
|
||||
vendor: device.Vendor,
|
||||
vendorID: device.VendorID,
|
||||
version: device.Version,
|
||||
runtimeVersion: device.CUDAVersion,
|
||||
deviceID: device.BackendDeviceID,
|
||||
deviceAlias: device.BackendDeviceAlias,
|
||||
preferredThreadSize: device.PreferredThreadSize,
|
||||
memoryUnified: device.MemoryUnified,
|
||||
localMemory: device.LocalMemory,
|
||||
cacheSize: device.CacheSize,
|
||||
pciAddress: device.PCIAddress,
|
||||
})
|
||||
}
|
||||
|
||||
func appendHIPBackendRows(tw table.Writer, device *clientpb.HIPBackendInfo, level uint32) {
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
appendBackendSummaryRows(tw, "HIP Device", formatHIPDevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
|
||||
if level < 2 {
|
||||
return
|
||||
}
|
||||
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
|
||||
deviceType: device.Type,
|
||||
vendor: device.Vendor,
|
||||
vendorID: device.VendorID,
|
||||
version: device.Version,
|
||||
runtimeVersion: device.HIPVersion,
|
||||
deviceID: device.BackendDeviceID,
|
||||
deviceAlias: device.BackendDeviceAlias,
|
||||
preferredThreadSize: device.PreferredThreadSize,
|
||||
memoryUnified: device.MemoryUnified,
|
||||
localMemory: device.LocalMemory,
|
||||
cacheSize: device.CacheSize,
|
||||
pciAddress: device.PCIAddress,
|
||||
})
|
||||
}
|
||||
|
||||
func appendMetalBackendRows(tw table.Writer, device *clientpb.MetalBackendInfo, level uint32) {
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
appendBackendSummaryRows(tw, "Metal Device", formatMetalDevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
|
||||
if level < 2 {
|
||||
return
|
||||
}
|
||||
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
|
||||
deviceType: device.Type,
|
||||
vendor: device.Vendor,
|
||||
vendorID: device.VendorID,
|
||||
version: device.Version,
|
||||
runtimeVersion: device.MetalVersion,
|
||||
deviceID: device.BackendDeviceID,
|
||||
deviceAlias: device.BackendDeviceAlias,
|
||||
preferredThreadSize: device.PreferredThreadSize,
|
||||
memoryUnified: device.MemoryUnified,
|
||||
localMemory: device.LocalMemory,
|
||||
cacheSize: device.CacheSize,
|
||||
memoryAllocPerBlock: device.MemoryAllocPerBlock,
|
||||
})
|
||||
appendStringBackendDetailRow(tw, "Physical Location", device.PhysicalLocation)
|
||||
appendUint32BackendDetailRow(tw, "Registry ID", device.RegistryID)
|
||||
appendStringBackendDetailRow(tw, "Max TX Rate", device.MaxTXRate)
|
||||
appendBoolBackendDetailRow(tw, "Headless", device.GPUHeadless)
|
||||
appendBoolBackendDetailRow(tw, "Low Power", device.GPULowPower)
|
||||
appendBoolBackendDetailRow(tw, "Removable", device.GPURemovable)
|
||||
}
|
||||
|
||||
func appendOpenCLBackendRows(tw table.Writer, device *clientpb.OpenCLBackendInfo, level uint32) {
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
appendBackendSummaryRows(tw, "OpenCL Device", formatOpenCLDevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
|
||||
if level < 2 {
|
||||
return
|
||||
}
|
||||
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
|
||||
deviceType: device.Type,
|
||||
vendor: device.Vendor,
|
||||
vendorID: device.VendorID,
|
||||
version: device.Version,
|
||||
runtimeVersion: device.OpenCLVersion,
|
||||
deviceID: device.BackendDeviceID,
|
||||
deviceAlias: device.BackendDeviceAlias,
|
||||
preferredThreadSize: device.PreferredThreadSize,
|
||||
memoryUnified: device.MemoryUnified,
|
||||
localMemory: device.LocalMemory,
|
||||
memoryAllocPerBlock: device.MemoryAllocPerBlock,
|
||||
pciAddress: device.PCIAddress,
|
||||
})
|
||||
appendStringBackendDetailRow(tw, "OpenCL Driver", device.OpenCLDriverVersion)
|
||||
if device.PlatformID != 0 || device.PlatformVendor != "" || device.PlatformName != "" || device.PlatformVersion != "" {
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("OpenCL Platform ID"), strconv.FormatUint(uint64(device.PlatformID), 10)})
|
||||
}
|
||||
appendStringBackendDetailRow(tw, "OpenCL Platform Vendor", device.PlatformVendor)
|
||||
appendStringBackendDetailRow(tw, "OpenCL Platform", device.PlatformName)
|
||||
appendStringBackendDetailRow(tw, "OpenCL Platform Version", device.PlatformVersion)
|
||||
}
|
||||
|
||||
func appendBackendSummaryRows(tw table.Writer, label, name, memoryFree, memoryTotal string, clock, processors int32) {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render(label), console.StyleBoldGreen.Render(safeCrackCell(name))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", valueOrDash(memoryFree), valueOrDash(memoryTotal))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), strconv.FormatInt(int64(clock), 10)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), strconv.FormatInt(int64(processors), 10)})
|
||||
}
|
||||
|
||||
func appendBackendDeviceDetailRows(tw table.Writer, details backendDeviceDetails) {
|
||||
appendStringBackendDetailRow(tw, "Device Type", details.deviceType)
|
||||
appendStringBackendDetailRow(tw, "Vendor", details.vendor)
|
||||
if details.vendorID != 0 {
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Vendor ID"), strconv.FormatInt(int64(details.vendorID), 10)})
|
||||
}
|
||||
if details.version != "" && details.version != details.runtimeVersion {
|
||||
appendStringBackendDetailRow(tw, "Device Version", details.version)
|
||||
}
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Backend Device ID"), strconv.FormatUint(uint64(details.deviceID), 10)})
|
||||
appendUint32BackendDetailRow(tw, "Backend Device Alias", details.deviceAlias)
|
||||
appendUint32BackendDetailRow(tw, "Preferred Thread Size", details.preferredThreadSize)
|
||||
appendBoolBackendDetailRow(tw, "Unified Memory", details.memoryUnified)
|
||||
appendStringBackendDetailRow(tw, "Local Memory", details.localMemory)
|
||||
appendStringBackendDetailRow(tw, "Cache Size", details.cacheSize)
|
||||
appendStringBackendDetailRow(tw, "Memory Allocation per Block", details.memoryAllocPerBlock)
|
||||
appendStringBackendDetailRow(tw, "PCI Address", details.pciAddress)
|
||||
}
|
||||
|
||||
func appendStringBackendDetailRow(tw table.Writer, label, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render(label), safeCrackCell(value)})
|
||||
}
|
||||
|
||||
func appendUint32BackendDetailRow(tw table.Writer, label string, value *uint32) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render(label), strconv.FormatUint(uint64(*value), 10)})
|
||||
}
|
||||
|
||||
func appendBoolBackendDetailRow(tw table.Writer, label string, value *bool) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render(label), strconv.FormatBool(*value)})
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
|
||||
var representativeCrackBenchmarkModes = map[int32]struct{}{
|
||||
0: {}, // MD5
|
||||
100: {}, // SHA1
|
||||
1000: {}, // NTLM
|
||||
1400: {}, // SHA2-256
|
||||
3200: {}, // bcrypt
|
||||
22000: {}, // WPA-PBKDF2
|
||||
}
|
||||
|
||||
const (
|
||||
crackBenchmarkOutputWidth = 80
|
||||
crackBenchmarkHashTypeWidth = 48
|
||||
crackBenchmarkMetadataValueWidth = 48
|
||||
)
|
||||
|
||||
// BenchmarksCmd displays benchmark results persisted by the server.
|
||||
func BenchmarksCmd(cmd *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
|
||||
response, err := con.Rpc.CrackstationBenchmarks(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
showAll, _ := cmd.Flags().GetBool("all")
|
||||
benchmarks := response.GetSnapshots()
|
||||
rendered := renderCrackBenchmarks(benchmarks, settings.GetTableStyle(con), showAll)
|
||||
if rendered == "" {
|
||||
con.PrintInfof("No cached crackstation benchmarks\n")
|
||||
return
|
||||
}
|
||||
con.Printf("%s\n", rendered)
|
||||
}
|
||||
|
||||
func renderCrackBenchmarks(benchmarks []*clientpb.CrackBenchmarkSnapshot, style table.Style, showAll bool) string {
|
||||
ordered := make([]*clientpb.CrackBenchmarkSnapshot, 0, len(benchmarks))
|
||||
for _, benchmark := range benchmarks {
|
||||
if benchmark != nil {
|
||||
ordered = append(ordered, benchmark)
|
||||
}
|
||||
}
|
||||
if len(ordered) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
leftName := safeCrackCell(ordered[i].GetName())
|
||||
rightName := safeCrackCell(ordered[j].GetName())
|
||||
if leftName == rightName {
|
||||
return safeCrackCell(ordered[i].GetHostUUID()) < safeCrackCell(ordered[j].GetHostUUID())
|
||||
}
|
||||
return leftName < rightName
|
||||
})
|
||||
|
||||
sections := make([]string, 0, len(ordered)*2+1)
|
||||
for index, benchmark := range ordered {
|
||||
identity := cachedBenchmarkIdentity(benchmark)
|
||||
title := renderCachedBenchmarkTitle(index, identity)
|
||||
metadata := table.NewWriter()
|
||||
metadata.SetStyle(boundedCrackBenchmarkStyle(style))
|
||||
metadata.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 2, WidthMax: crackBenchmarkMetadataValueWidth},
|
||||
})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Name"), valueOrDash(benchmark.GetName())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Host UUID"), console.StyleBoldPrimary.Render(valueOrDash(benchmark.GetHostUUID()))})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Operator"), valueOrDash(benchmark.GetOperatorName())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Connection"), renderCachedBenchmarkConnection(benchmark.GetOnline())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Cache"), renderCachedBenchmarkFreshness(benchmark.GetFresh())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Current Hashcat Version"), valueOrDash(benchmark.GetCurrentHashcatVersion())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Benchmark Hashcat Version"), valueOrDash(benchmark.GetBenchmarkHashcatVersion())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Benchmark Schema"), strconv.FormatUint(uint64(benchmark.GetBenchmarkSchemaVersion()), 10)})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Benchmarked At"), formatCachedBenchmarkTime(benchmark.GetBenchmarkedAt())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Cached Modes"), strconv.Itoa(len(benchmark.GetBenchmarks()))})
|
||||
sections = append(sections, title+"\n"+metadata.Render())
|
||||
|
||||
rates := table.NewWriter()
|
||||
rates.SetStyle(boundedCrackBenchmarkStyle(style))
|
||||
rates.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Name: "Hash Type", WidthMax: crackBenchmarkHashTypeWidth},
|
||||
})
|
||||
if showAll {
|
||||
rates.SetTitle(console.StyleBold.Render("All Cached Rates"))
|
||||
} else {
|
||||
rates.SetTitle(console.StyleBold.Render("Representative Rates"))
|
||||
}
|
||||
rates.AppendHeader(table.Row{"Mode", "Hash Type", "Rate"})
|
||||
modes := cachedBenchmarkModes(benchmark.GetBenchmarks(), showAll)
|
||||
if len(modes) == 0 {
|
||||
rates.AppendRow(table.Row{"-", "No benchmark rates cached", "-"})
|
||||
} else {
|
||||
for _, mode := range modes {
|
||||
rates.AppendRow(table.Row{
|
||||
strconv.FormatInt(int64(mode), 10),
|
||||
crackBenchmarkModeName(mode),
|
||||
humanizeHashRate(benchmark.GetBenchmarks()[mode]),
|
||||
})
|
||||
}
|
||||
}
|
||||
sections = append(sections, rates.Render())
|
||||
}
|
||||
if !showAll {
|
||||
sections = append(sections, console.StyleGray.Render("Showing representative rates; use --all for every cached mode."))
|
||||
}
|
||||
return strings.Join(sections, "\n\n")
|
||||
}
|
||||
|
||||
func boundedCrackBenchmarkStyle(style table.Style) table.Style {
|
||||
style.Size.WidthMax = crackBenchmarkOutputWidth
|
||||
return style
|
||||
}
|
||||
|
||||
func renderCachedBenchmarkTitle(index int, identity string) string {
|
||||
prefix := fmt.Sprintf(">>> Cached Crackstation %02d - ", index+1)
|
||||
maximumIdentityWidth := crackBenchmarkOutputWidth - ansi.StringWidth(prefix)
|
||||
if maximumIdentityWidth < 1 {
|
||||
return console.StyleBoldPrimary.Render(ansi.Truncate(prefix, crackBenchmarkOutputWidth, "…"))
|
||||
}
|
||||
identity = ansi.Truncate(identity, maximumIdentityWidth, "…")
|
||||
return console.StyleBoldPrimary.Render(prefix + identity)
|
||||
}
|
||||
|
||||
func cachedBenchmarkIdentity(benchmark *clientpb.CrackBenchmarkSnapshot) string {
|
||||
if benchmark == nil {
|
||||
return "Unknown crackstation"
|
||||
}
|
||||
if name := safeCrackCell(benchmark.GetName()); name != "" {
|
||||
return name
|
||||
}
|
||||
if hostUUID := safeCrackCell(benchmark.GetHostUUID()); hostUUID != "" {
|
||||
return hostUUID
|
||||
}
|
||||
return "Unknown crackstation"
|
||||
}
|
||||
|
||||
func renderCachedBenchmarkConnection(online bool) string {
|
||||
if online {
|
||||
return console.StyleBoldSuccess.Render("Online")
|
||||
}
|
||||
return console.StyleBoldGray.Render("Offline")
|
||||
}
|
||||
|
||||
func renderCachedBenchmarkFreshness(fresh bool) string {
|
||||
if fresh {
|
||||
return console.StyleBoldSuccess.Render("Fresh")
|
||||
}
|
||||
return console.StyleBoldWarning.Render("Stale")
|
||||
}
|
||||
|
||||
func formatCachedBenchmarkTime(unix int64) string {
|
||||
if unix == 0 {
|
||||
return "-"
|
||||
}
|
||||
return time.Unix(unix, 0).UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func cachedBenchmarkModes(benchmarks map[int32]uint64, showAll bool) []int32 {
|
||||
modes := make([]int32, 0, len(benchmarks))
|
||||
for mode := range benchmarks {
|
||||
if showAll {
|
||||
modes = append(modes, mode)
|
||||
continue
|
||||
}
|
||||
if _, representative := representativeCrackBenchmarkModes[mode]; representative {
|
||||
modes = append(modes, mode)
|
||||
}
|
||||
}
|
||||
sort.Slice(modes, func(i, j int) bool { return modes[i] < modes[j] })
|
||||
if showAll || len(modes) != 0 {
|
||||
return modes
|
||||
}
|
||||
|
||||
for mode := range benchmarks {
|
||||
modes = append(modes, mode)
|
||||
}
|
||||
sort.Slice(modes, func(i, j int) bool { return modes[i] < modes[j] })
|
||||
if len(modes) > 6 {
|
||||
modes = modes[:6]
|
||||
}
|
||||
return modes
|
||||
}
|
||||
|
||||
func crackBenchmarkModeName(mode int32) string {
|
||||
if name, ok := hashcatHashTypeName(mode); ok {
|
||||
return name
|
||||
}
|
||||
if enumName := clientpb.HashType(mode).String(); enumName != strconv.FormatInt(int64(mode), 10) {
|
||||
return enumName
|
||||
}
|
||||
return "Unknown hash mode"
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestCrackBenchmarksCommandIsRegistered(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
benchmarks, _, err := root.Find([]string{"benchmarks"})
|
||||
if err != nil || benchmarks == root {
|
||||
t.Fatalf("find crack benchmarks: command=%v error=%v", benchmarks, err)
|
||||
}
|
||||
if benchmarks.Use != "benchmarks" {
|
||||
t.Fatalf("crack benchmarks use = %q, want benchmarks", benchmarks.Use)
|
||||
}
|
||||
if err := benchmarks.Args(benchmarks, nil); err != nil {
|
||||
t.Fatalf("crack benchmarks rejected no arguments: %v", err)
|
||||
}
|
||||
if err := benchmarks.Args(benchmarks, []string{"unexpected"}); err == nil {
|
||||
t.Fatal("crack benchmarks accepted a positional argument")
|
||||
}
|
||||
|
||||
allFlag := benchmarks.Flags().Lookup("all")
|
||||
if allFlag == nil {
|
||||
t.Fatal("crack benchmarks is missing --all")
|
||||
}
|
||||
if allFlag.Shorthand != "a" {
|
||||
t.Fatalf("--all shorthand = %q, want %q", allFlag.Shorthand, "a")
|
||||
}
|
||||
if benchmarks.PersistentFlags().Lookup("all") != nil || root.PersistentFlags().Lookup("all") != nil {
|
||||
t.Fatal("--all must remain local to crack benchmarks")
|
||||
}
|
||||
if benchmarks.InheritedFlags().Lookup("timeout") == nil {
|
||||
t.Fatal("crack benchmarks must inherit --timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksSortsStationsAndRepresentativeModes(t *testing.T) {
|
||||
benchmarks := []*clientpb.CrackBenchmarkSnapshot{
|
||||
nil,
|
||||
{
|
||||
Name: "zeta",
|
||||
HostUUID: "host-z",
|
||||
OperatorName: "operator-z",
|
||||
CurrentHashcatVersion: "v7.1.3",
|
||||
BenchmarkHashcatVersion: "v7.1.2",
|
||||
BenchmarkSchemaVersion: 1,
|
||||
BenchmarkedAt: 1_700_000_000,
|
||||
Online: true,
|
||||
Fresh: true,
|
||||
Benchmarks: map[int32]uint64{
|
||||
22000: 6_000_000_000_000,
|
||||
0: 999,
|
||||
3200: 5_000_000_000,
|
||||
100: 1_000,
|
||||
1400: 3_000_000,
|
||||
1000: 2_000_000,
|
||||
99999: 7_000_000,
|
||||
},
|
||||
},
|
||||
{Name: "alpha", HostUUID: "host-b", Benchmarks: map[int32]uint64{0: 1}},
|
||||
{Name: "alpha", HostUUID: "host-a", Benchmarks: map[int32]uint64{0: 2}},
|
||||
}
|
||||
|
||||
raw := renderCrackBenchmarks(benchmarks, settings.SliverDefault, false)
|
||||
output := ansi.Strip(raw)
|
||||
requireOrderedCrackBenchmarkText(t, output, "host-a", "host-b", "host-z")
|
||||
zetaStart := strings.Index(output, ">>> Cached Crackstation 03 - zeta")
|
||||
if zetaStart < 0 {
|
||||
t.Fatalf("rendered benchmarks are missing sorted zeta section:\n%s", output)
|
||||
}
|
||||
requireOrderedCrackBenchmarkText(t, output[zetaStart:],
|
||||
"MD5", "SHA1", "NTLM", "SHA2-256", "bcrypt $2\\*$, Blowfish (Unix)", "WPA-PBKDF2-PMKID+EAPOL",
|
||||
)
|
||||
|
||||
for _, expected := range []string{
|
||||
"Name", "Host UUID", "Operator", "Connection", "Online", "Cache", "Fresh",
|
||||
"Current Hashcat Version", "v7.1.3", "Benchmark Hashcat Version", "v7.1.2",
|
||||
"Benchmark Schema", "Benchmarked At", "2023-11-14T22:13:20Z", "Cached Modes",
|
||||
"operator-z", "Representative Rates", "Showing representative rates; use --all for every cached mode.",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("rendered benchmarks do not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
requireCrackBenchmarkRow(t, output, 0, "MD5", "999 H/s")
|
||||
requireCrackBenchmarkRow(t, output, 100, "SHA1", "1.00 kH/s")
|
||||
requireCrackBenchmarkRow(t, output, 1000, "NTLM", "2.00 MH/s")
|
||||
requireCrackBenchmarkRow(t, output, 1400, "SHA2-256", "3.00 MH/s")
|
||||
requireCrackBenchmarkRow(t, output, 3200, "bcrypt $2\\*$, Blowfish (Unix)", "5.00 GH/s")
|
||||
requireCrackBenchmarkRow(t, output, 22000, "WPA-PBKDF2-PMKID+EAPOL", "6.00 TH/s")
|
||||
if hasCrackBenchmarkRow(output, 99999, "Plaintext", "7.00 MH/s") {
|
||||
t.Fatalf("default benchmark view unexpectedly contains non-representative mode 99999:\n%s", output)
|
||||
}
|
||||
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldPrimary.Render(">>> Cached Crackstation 01 - alpha"),
|
||||
console.StyleBoldPrimary.Render("host-a"),
|
||||
console.StyleBoldSuccess.Render("Online"),
|
||||
console.StyleBoldSuccess.Render("Fresh"),
|
||||
console.StyleBold.Render("Representative Rates"),
|
||||
console.StyleGray.Render("Showing representative rates; use --all for every cached mode."),
|
||||
} {
|
||||
if !strings.Contains(raw, styled) {
|
||||
t.Errorf("rendered benchmarks are missing shared Lip Gloss styling %q:\n%s", styled, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksFallsBackToFirstSixModes(t *testing.T) {
|
||||
benchmarks := []*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: "fallback",
|
||||
Benchmarks: map[int32]uint64{
|
||||
23: 23,
|
||||
10: 10,
|
||||
22: 22,
|
||||
12: 12,
|
||||
20: 20,
|
||||
11: 11,
|
||||
21: 21,
|
||||
},
|
||||
}}
|
||||
|
||||
output := ansi.Strip(renderCrackBenchmarks(benchmarks, settings.SliverDefault, false))
|
||||
requireOrderedCrackBenchmarkText(t, output,
|
||||
"md5($pass.$salt)",
|
||||
"Joomla < 2.5.18",
|
||||
"PostgreSQL",
|
||||
"md5($salt.$pass)",
|
||||
"osCommerce, xt:Commerce",
|
||||
"Juniper NetScreen/SSG (ScreenOS)",
|
||||
)
|
||||
if hasCrackBenchmarkRow(output, 23, "Skype", "23 H/s") {
|
||||
t.Fatalf("fallback benchmark view contains more than six modes:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksAllShowsEveryModeAndUnknownFallback(t *testing.T) {
|
||||
benchmarks := []*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: "all-modes",
|
||||
Benchmarks: map[int32]uint64{
|
||||
424242: 1_234,
|
||||
36100: 3_600,
|
||||
99999: 7_000_000,
|
||||
0: 500,
|
||||
},
|
||||
}}
|
||||
|
||||
raw := renderCrackBenchmarks(benchmarks, settings.SliverDefault, true)
|
||||
output := ansi.Strip(raw)
|
||||
for _, row := range []struct {
|
||||
mode int32
|
||||
hashType string
|
||||
rate string
|
||||
}{
|
||||
{mode: 0, hashType: "MD5", rate: "500 H/s"},
|
||||
{mode: 36100, hashType: "YESCRYPT", rate: "3.60 kH/s"},
|
||||
{mode: 99999, hashType: "Plaintext", rate: "7.00 MH/s"},
|
||||
{mode: 424242, hashType: "Unknown hash mode", rate: "1.23 kH/s"},
|
||||
} {
|
||||
requireCrackBenchmarkRow(t, output, row.mode, row.hashType, row.rate)
|
||||
}
|
||||
if !strings.Contains(raw, console.StyleBold.Render("All Cached Rates")) {
|
||||
t.Fatalf("all-modes view is missing styled title:\n%s", raw)
|
||||
}
|
||||
if strings.Contains(output, "Showing representative rates") {
|
||||
t.Fatalf("all-modes view unexpectedly contains representative-rate hint:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksSanitizesMetadata(t *testing.T) {
|
||||
benchmarks := []*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: "alpha\nbeta\x1b]52;c;payload\a",
|
||||
HostUUID: "host\r\nuuid",
|
||||
OperatorName: "op\tname",
|
||||
CurrentHashcatVersion: "current-v7\x00.2",
|
||||
BenchmarkHashcatVersion: "benchmark-v7\x00.1",
|
||||
Benchmarks: map[int32]uint64{0: 1},
|
||||
}}
|
||||
|
||||
raw := renderCrackBenchmarks(benchmarks, settings.SliverDefault, false)
|
||||
output := ansi.Strip(raw)
|
||||
for _, expected := range []string{"alphabeta]52;c;payload", "hostuuid", "opname", "current-v7.2", "benchmark-v7.1"} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("sanitized benchmark metadata does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
for _, control := range []string{"\x1b]52;", "\a", "\r", "\x00", "\t"} {
|
||||
if strings.Contains(raw, control) {
|
||||
t.Errorf("rendered benchmark metadata retained control text %q:\n%q", control, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksStylesConnectionAndFreshness(t *testing.T) {
|
||||
raw := renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{
|
||||
{Name: "fresh", Online: true, Fresh: true, Benchmarks: map[int32]uint64{0: 1}},
|
||||
{Name: "stale", Online: false, Fresh: false, Benchmarks: map[int32]uint64{0: 1}},
|
||||
}, settings.SliverDefault, false)
|
||||
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldSuccess.Render("Online"),
|
||||
console.StyleBoldSuccess.Render("Fresh"),
|
||||
console.StyleBoldGray.Render("Offline"),
|
||||
console.StyleBoldWarning.Render("Stale"),
|
||||
} {
|
||||
if !strings.Contains(raw, styled) {
|
||||
t.Errorf("rendered benchmarks are missing status styling %q:\n%s", styled, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksBoundsEveryLine(t *testing.T) {
|
||||
raw := renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: strings.Repeat("very-long-crackstation-name-", 8),
|
||||
HostUUID: "12345678-1234-1234-1234-123456789abc",
|
||||
OperatorName: strings.Repeat("operator-", 16),
|
||||
CurrentHashcatVersion: strings.Repeat("current-version-", 10),
|
||||
BenchmarkHashcatVersion: strings.Repeat("benchmark-version-", 10),
|
||||
BenchmarkedAt: 1_700_000_000,
|
||||
Benchmarks: map[int32]uint64{6211: 1_000_000},
|
||||
}}, settings.SliverDefault, true)
|
||||
|
||||
for lineNumber, line := range strings.Split(raw, "\n") {
|
||||
if width := ansi.StringWidth(line); width > crackBenchmarkOutputWidth {
|
||||
t.Errorf("rendered line %d width = %d, want <= %d:\n%s", lineNumber+1, width, crackBenchmarkOutputWidth, line)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(ansi.Strip(raw), "…") {
|
||||
t.Fatalf("long benchmark heading was not truncated:\n%s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatCachedBenchmarkTime(t *testing.T) {
|
||||
if got := formatCachedBenchmarkTime(0); got != "-" {
|
||||
t.Fatalf("zero benchmark time = %q, want -", got)
|
||||
}
|
||||
if got := formatCachedBenchmarkTime(1_700_000_000); got != "2023-11-14T22:13:20Z" {
|
||||
t.Fatalf("benchmark time = %q, want UTC RFC3339", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksHandlesNilAndEmptyData(t *testing.T) {
|
||||
for _, benchmarks := range [][]*clientpb.CrackBenchmarkSnapshot{nil, {nil, nil}} {
|
||||
if got := renderCrackBenchmarks(benchmarks, settings.SliverDefault, false); got != "" {
|
||||
t.Fatalf("renderCrackBenchmarks(%#v) = %q, want empty output", benchmarks, got)
|
||||
}
|
||||
}
|
||||
|
||||
raw := renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{{
|
||||
HostUUID: "host-only",
|
||||
}}, settings.SliverDefault, false)
|
||||
output := ansi.Strip(raw)
|
||||
for _, expected := range []string{
|
||||
">>> Cached Crackstation 01 - host-only",
|
||||
"No benchmark rates cached",
|
||||
"Representative Rates",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("empty benchmark snapshot does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
unknown := ansi.Strip(renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{{}}, settings.SliverDefault, false))
|
||||
if !strings.Contains(unknown, ">>> Cached Crackstation 01 - Unknown crackstation") {
|
||||
t.Fatalf("missing unknown-station identity fallback:\n%s", unknown)
|
||||
}
|
||||
}
|
||||
|
||||
type crackBenchmarksRPCCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
response *clientpb.CrackBenchmarkSnapshots
|
||||
err error
|
||||
benchmarkCalls int
|
||||
crackstationsCalls int
|
||||
hadDeadline bool
|
||||
}
|
||||
|
||||
func (capture *crackBenchmarksRPCCapture) CrackstationBenchmarks(ctx context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.CrackBenchmarkSnapshots, error) {
|
||||
capture.benchmarkCalls++
|
||||
_, capture.hadDeadline = ctx.Deadline()
|
||||
return capture.response, capture.err
|
||||
}
|
||||
|
||||
func (capture *crackBenchmarksRPCCapture) Crackstations(_ context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.Crackstations, error) {
|
||||
capture.crackstationsCalls++
|
||||
return &clientpb.Crackstations{}, nil
|
||||
}
|
||||
|
||||
func TestCrackBenchmarksCommandUsesDedicatedRPCWithDeadline(t *testing.T) {
|
||||
capture := &crackBenchmarksRPCCapture{response: &clientpb.CrackBenchmarkSnapshots{Snapshots: []*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: "station",
|
||||
HostUUID: "host-id",
|
||||
Benchmarks: map[int32]uint64{0: 1},
|
||||
}}}}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
root := Commands(con)[0]
|
||||
root.SetArgs([]string{"benchmarks", "--timeout=1", "-a"})
|
||||
root.SilenceErrors = true
|
||||
root.SilenceUsage = true
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute crack benchmarks: %v", err)
|
||||
}
|
||||
if capture.benchmarkCalls != 1 {
|
||||
t.Fatalf("CrackstationBenchmarks calls = %d, want 1", capture.benchmarkCalls)
|
||||
}
|
||||
if capture.crackstationsCalls != 0 {
|
||||
t.Fatalf("Crackstations calls = %d, want 0", capture.crackstationsCalls)
|
||||
}
|
||||
if !capture.hadDeadline {
|
||||
t.Fatal("CrackstationBenchmarks context did not inherit --timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackBenchmarksCommandHandlesNilResponse(t *testing.T) {
|
||||
capture := &crackBenchmarksRPCCapture{}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
root := Commands(con)[0]
|
||||
root.SetArgs([]string{"benchmarks", "--timeout=1"})
|
||||
root.SilenceErrors = true
|
||||
root.SilenceUsage = true
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute crack benchmarks with nil response: %v", err)
|
||||
}
|
||||
if capture.benchmarkCalls != 1 {
|
||||
t.Fatalf("CrackstationBenchmarks calls = %d, want 1", capture.benchmarkCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func requireOrderedCrackBenchmarkText(t *testing.T, output string, values ...string) {
|
||||
t.Helper()
|
||||
remaining := output
|
||||
for _, value := range values {
|
||||
index := strings.Index(remaining, value)
|
||||
if index < 0 {
|
||||
t.Fatalf("rendered benchmarks do not contain %q in order:\n%s", value, output)
|
||||
}
|
||||
remaining = remaining[index+len(value):]
|
||||
}
|
||||
}
|
||||
|
||||
func requireCrackBenchmarkRow(t *testing.T, output string, mode int32, hashType string, rate string) {
|
||||
t.Helper()
|
||||
if !hasCrackBenchmarkRow(output, mode, hashType, rate) {
|
||||
t.Fatalf("rendered benchmarks are missing mode %d, hash type %q, rate %q:\n%s", mode, hashType, rate, output)
|
||||
}
|
||||
}
|
||||
|
||||
func hasCrackBenchmarkRow(output string, mode int32, hashType string, rate string) bool {
|
||||
pattern := `(?m)^[ \t]*` + regexp.QuoteMeta(strconv.FormatInt(int64(mode), 10)) + `[ \t]+` +
|
||||
regexp.QuoteMeta(hashType) + `[ \t]+` + regexp.QuoteMeta(rate) + `[ \t]*$`
|
||||
return regexp.MustCompile(pattern).MatchString(output)
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package crack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -10,6 +12,38 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
const maxKeyboardLayoutMappingBytes int64 = 1 << 20
|
||||
|
||||
func readKeyboardLayoutMapping(path string) ([]byte, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open keyboard layout mapping %q: %w", path, err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat keyboard layout mapping %q: %w", path, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("keyboard layout mapping %q is not a regular file", path)
|
||||
}
|
||||
if info.Size() > maxKeyboardLayoutMappingBytes {
|
||||
return nil, fmt.Errorf("keyboard layout mapping %q exceeds %d bytes", path, maxKeyboardLayoutMappingBytes)
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(file, maxKeyboardLayoutMappingBytes+1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read keyboard layout mapping %q: %w", path, err)
|
||||
}
|
||||
if int64(len(data)) > maxKeyboardLayoutMappingBytes {
|
||||
return nil, fmt.Errorf("keyboard layout mapping %q exceeds %d bytes", path, maxKeyboardLayoutMappingBytes)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("keyboard layout mapping %q is empty", path)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func shouldRunCrack(cmd *cobra.Command, args []string) bool {
|
||||
if len(args) > 0 {
|
||||
return true
|
||||
@@ -17,16 +51,25 @@ func shouldRunCrack(cmd *cobra.Command, args []string) bool {
|
||||
|
||||
hasFlag := false
|
||||
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
if flag.Changed && flag.Name != "timeout" {
|
||||
hasFlag = true
|
||||
if !flag.Changed || flag.Name == "timeout" {
|
||||
return
|
||||
}
|
||||
// An explicitly disabled standalone mode is a no-op, not a crack job.
|
||||
switch flag.Name {
|
||||
case "backend-info", "keyspace", "total-candidates", "identify-mode", "hash-info":
|
||||
enabled, _ := cmd.Flags().GetBool(flag.Name)
|
||||
if !enabled {
|
||||
return
|
||||
}
|
||||
}
|
||||
hasFlag = true
|
||||
})
|
||||
return hasFlag
|
||||
}
|
||||
|
||||
func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackCommand, error) {
|
||||
flags := cmd.Flags()
|
||||
req := &clientpb.CrackCommand{}
|
||||
req := &clientpb.CrackCommand{HashType: clientpb.HashType_INVALID}
|
||||
|
||||
if flags.Changed("attack-mode") {
|
||||
raw, _ := flags.GetString("attack-mode")
|
||||
@@ -45,12 +88,39 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
}
|
||||
req.HashType = hashType
|
||||
}
|
||||
if flags.Changed("hash-mode") {
|
||||
hashMode, _ := flags.GetUint32("hash-mode")
|
||||
req.HashMode = &hashMode
|
||||
}
|
||||
|
||||
hashes, _ := flags.GetStringSlice("hash")
|
||||
hashes, _ := flags.GetStringArray("hash")
|
||||
if len(args) > 0 {
|
||||
hashes = append(hashes, args...)
|
||||
}
|
||||
req.Hashes = hashes
|
||||
req.CredentialIDs, _ = flags.GetStringArray("credential")
|
||||
for index, credentialID := range req.CredentialIDs {
|
||||
req.CredentialIDs[index] = strings.TrimSpace(credentialID)
|
||||
if req.CredentialIDs[index] == "" {
|
||||
return nil, fmt.Errorf("--credential cannot be empty")
|
||||
}
|
||||
}
|
||||
req.CredentialCollection, _ = flags.GetString("credential-collection")
|
||||
req.CredentialCollection = strings.TrimSpace(req.CredentialCollection)
|
||||
req.IncludeCrackedCredentials, _ = flags.GetBool("include-cracked-credentials")
|
||||
if req.IncludeCrackedCredentials && len(req.CredentialIDs) == 0 && req.CredentialCollection == "" {
|
||||
return nil, fmt.Errorf("--include-cracked-credentials requires --credential or --credential-collection")
|
||||
}
|
||||
req.PositionalArguments, _ = flags.GetStringArray("input")
|
||||
req.Crackstation, _ = flags.GetString("crackstation")
|
||||
req.Crackstation = strings.TrimSpace(req.Crackstation)
|
||||
if stdin, _ := flags.GetString("stdin"); stdin != "" {
|
||||
req.Stdin = []byte(stdin)
|
||||
}
|
||||
|
||||
if flags.Changed("segment-size") {
|
||||
return nil, fmt.Errorf("--segment-size was removed in Hashcat v7")
|
||||
}
|
||||
|
||||
req.Quiet, _ = flags.GetBool("quiet")
|
||||
req.HexCharset, _ = flags.GetBool("hex-charset")
|
||||
@@ -58,10 +128,19 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
req.HexWordlist, _ = flags.GetBool("hex-wordlist")
|
||||
req.Force, _ = flags.GetBool("force")
|
||||
req.DeprecatedCheckDisable, _ = flags.GetBool("deprecated-check-disable")
|
||||
req.AdviceDisable, _ = flags.GetBool("advice-disable")
|
||||
req.Status, _ = flags.GetBool("status")
|
||||
req.StatusJSON, _ = flags.GetBool("status-json")
|
||||
req.StatusTimer, _ = flags.GetUint32("status-timer")
|
||||
if flags.Changed("status-timer") {
|
||||
value := req.StatusTimer
|
||||
req.StatusTimerV7 = &value
|
||||
}
|
||||
req.StdinTimeoutAbort, _ = flags.GetUint32("stdin-timeout-abort")
|
||||
if flags.Changed("stdin-timeout-abort") {
|
||||
value := req.StdinTimeoutAbort
|
||||
req.StdinTimeoutAbortV7 = &value
|
||||
}
|
||||
req.MachineReadable, _ = flags.GetBool("machine-readable")
|
||||
req.KeepGuessing, _ = flags.GetBool("keep-guessing")
|
||||
req.SelfTestDisable, _ = flags.GetBool("self-test-disable")
|
||||
@@ -81,7 +160,13 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
if raw, _ := flags.GetString("restore-file"); raw != "" {
|
||||
req.RestoreFile = []byte(raw)
|
||||
}
|
||||
req.RestorePosition, _ = flags.GetBool("restore-position")
|
||||
req.RestoreShowCommand, _ = flags.GetBool("restore-show-command")
|
||||
if req.RestoreShowCommand && (req.Restore || req.RestorePosition) { //nolint:staticcheck // Restore is required for legacy command compatibility.
|
||||
return nil, fmt.Errorf("--restore-show-command and restore-position modes are mutually exclusive")
|
||||
}
|
||||
|
||||
req.Outfile, _ = flags.GetString("outfile")
|
||||
if formats, _ := flags.GetStringSlice("outfile-format"); len(formats) > 0 {
|
||||
for _, raw := range formats {
|
||||
format, err := parseCrackOutfileFormat(raw)
|
||||
@@ -91,11 +176,15 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
req.OutfileFormat = append(req.OutfileFormat, format)
|
||||
}
|
||||
}
|
||||
req.OutfileJSON, _ = flags.GetBool("outfile-json")
|
||||
req.OutfileAutohexDisable, _ = flags.GetBool("outfile-autohex-disable")
|
||||
req.OutfileCheckTimer, _ = flags.GetUint32("outfile-check-timer")
|
||||
if flags.Changed("outfile-check-timer") {
|
||||
value := req.OutfileCheckTimer
|
||||
req.OutfileCheckTimerV7 = &value
|
||||
}
|
||||
req.WordlistAutohexDisable, _ = flags.GetBool("wordlist-autohex-disable")
|
||||
req.Separator, _ = flags.GetString("separator")
|
||||
req.Stdout, _ = flags.GetBool("stdout")
|
||||
req.Show, _ = flags.GetBool("show")
|
||||
req.Left, _ = flags.GetBool("left")
|
||||
req.Username, _ = flags.GetBool("username")
|
||||
@@ -108,48 +197,107 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
|
||||
if flags.Changed("encoding-from") {
|
||||
raw, _ := flags.GetString("encoding-from")
|
||||
enc, err := parseCrackEncoding(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if enc, err := parseCrackEncoding(raw); err == nil {
|
||||
req.EncodingFrom = enc
|
||||
}
|
||||
if _, err := strconv.ParseInt(raw, 10, 32); err != nil {
|
||||
req.EncodingFromName = raw
|
||||
}
|
||||
req.EncodingFrom = enc
|
||||
}
|
||||
if flags.Changed("encoding-to") {
|
||||
raw, _ := flags.GetString("encoding-to")
|
||||
enc, err := parseCrackEncoding(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if enc, err := parseCrackEncoding(raw); err == nil {
|
||||
req.EncodingTo = enc
|
||||
}
|
||||
if _, err := strconv.ParseInt(raw, 10, 32); err != nil {
|
||||
req.EncodingToName = raw
|
||||
}
|
||||
req.EncodingTo = enc
|
||||
}
|
||||
|
||||
req.DebugMode, _ = flags.GetUint32("debug-mode")
|
||||
req.DebugFile, _ = flags.GetString("debug-file")
|
||||
req.InductionDir, _ = flags.GetString("induction-dir")
|
||||
req.OutfileCheckDir, _ = flags.GetString("outfile-check-dir")
|
||||
req.LogfileDisable, _ = flags.GetBool("logfile-disable")
|
||||
req.HccapxMessagePair, _ = flags.GetUint32("hccapx-message-pair")
|
||||
if flags.Changed("hccapx-message-pair") {
|
||||
value := req.HccapxMessagePair
|
||||
req.HccapxMessagePairV7 = &value
|
||||
}
|
||||
req.NonceErrorCorrections, _ = flags.GetUint32("nonce-error-corrections")
|
||||
if flags.Changed("nonce-error-corrections") {
|
||||
value := req.NonceErrorCorrections
|
||||
req.NonceErrorCorrectionsV7 = &value
|
||||
}
|
||||
if raw, _ := flags.GetString("keyboard-layout-mapping"); raw != "" {
|
||||
req.KeyboardLayoutMapping = []byte(raw)
|
||||
mapping, err := readKeyboardLayoutMapping(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.KeyboardLayoutMapping = mapping
|
||||
}
|
||||
req.TruecryptKeyfiles, _ = flags.GetString("truecrypt-keyfiles")
|
||||
req.VeracryptKeyfiles, _ = flags.GetString("veracrypt-keyfiles")
|
||||
if flags.Changed("veracrypt-pim-start") {
|
||||
value, _ := flags.GetUint32("veracrypt-pim-start")
|
||||
req.VeracryptPimStart = &value
|
||||
}
|
||||
if flags.Changed("veracrypt-pim-stop") {
|
||||
value, _ := flags.GetUint32("veracrypt-pim-stop")
|
||||
req.VeracryptPimStop = &value
|
||||
}
|
||||
|
||||
req.Benchmark, _ = flags.GetBool("benchmark")
|
||||
req.BenchmarkAll, _ = flags.GetBool("benchmark-all")
|
||||
req.BenchmarkMin, _ = flags.GetUint32("benchmark-min")
|
||||
if flags.Changed("benchmark-max") {
|
||||
value, _ := flags.GetUint32("benchmark-max")
|
||||
req.BenchmarkMax = &value
|
||||
}
|
||||
req.SpeedOnly, _ = flags.GetBool("speed-only")
|
||||
req.ProgressOnly, _ = flags.GetBool("progress-only")
|
||||
req.SegmentSize, _ = flags.GetUint32("segment-size")
|
||||
req.PipelineStats, _ = flags.GetBool("pipeline-stats")
|
||||
req.TaskTimeBreakdown, _ = flags.GetBool("task-time-breakdown")
|
||||
req.BitmapMin, _ = flags.GetUint32("bitmap-min")
|
||||
if flags.Changed("bitmap-min") {
|
||||
value := req.BitmapMin
|
||||
req.BitmapMinV7 = &value
|
||||
}
|
||||
req.BitmapMax, _ = flags.GetUint32("bitmap-max")
|
||||
if flags.Changed("bitmap-max") {
|
||||
value := req.BitmapMax
|
||||
req.BitmapMaxV7 = &value
|
||||
}
|
||||
req.CPUAffinity = uintSliceToUint32(flags, "cpu-affinity")
|
||||
req.HookThreads, _ = flags.GetUint32("hook-threads")
|
||||
req.HashInfo, _ = flags.GetBool("hash-info")
|
||||
if req.HashInfo {
|
||||
req.HashInfoLevel = 1
|
||||
}
|
||||
if flags.Changed("hash-info-level") {
|
||||
req.HashInfoLevel, _ = flags.GetUint32("hash-info-level")
|
||||
}
|
||||
req.BackendIgnoreCUDA, _ = flags.GetBool("backend-ignore-cuda")
|
||||
req.BackendIgnoreHip, _ = flags.GetBool("backend-ignore-hip")
|
||||
req.BackendIgnoreMetal, _ = flags.GetBool("backend-ignore-metal")
|
||||
req.BackendIgnoreOpenCL, _ = flags.GetBool("backend-ignore-opencl")
|
||||
req.BackendInfo, _ = flags.GetBool("backend-info")
|
||||
if req.BackendInfo {
|
||||
req.BackendInfoLevel = 1
|
||||
}
|
||||
if flags.Changed("backend-info-level") {
|
||||
req.BackendInfoLevel, _ = flags.GetUint32("backend-info-level")
|
||||
}
|
||||
req.BackendDevices = uintSliceToUint32(flags, "backend-devices")
|
||||
req.BackendDevicesVirtMulti, _ = flags.GetUint32("backend-devices-virtmulti")
|
||||
req.BackendDevicesVirtHost, _ = flags.GetUint32("backend-devices-virthost")
|
||||
req.OpenCLDeviceTypes = uintSliceToUint32(flags, "opencl-device-types")
|
||||
req.MetalCompilerRuntime, _ = flags.GetUint32("metal-compiler-runtime")
|
||||
req.OptimizedKernelEnable, _ = flags.GetBool("optimized-kernel-enable")
|
||||
req.MultiplyAccelDisabled, _ = flags.GetBool("multiply-accel-disabled")
|
||||
req.MultiplyAccelDisabled, _ = flags.GetBool("multiply-accel-disable")
|
||||
if !req.MultiplyAccelDisabled {
|
||||
req.MultiplyAccelDisabled, _ = flags.GetBool("multiply-accel-disabled")
|
||||
}
|
||||
|
||||
if flags.Changed("workload-profile") {
|
||||
raw, _ := flags.GetString("workload-profile")
|
||||
@@ -167,37 +315,114 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
req.SpinDamp, _ = flags.GetUint32("spin-damp")
|
||||
req.HwmonDisable, _ = flags.GetBool("hwmon-disable")
|
||||
req.HwmonTempAbort, _ = flags.GetUint32("hwmon-temp-abort")
|
||||
if flags.Changed("hwmon-temp-abort") {
|
||||
value := req.HwmonTempAbort
|
||||
req.HwmonTempAbortV7 = &value
|
||||
}
|
||||
req.ScryptTMTO, _ = flags.GetUint32("scrypt-tmto")
|
||||
if flags.Changed("scrypt-tmto") {
|
||||
value := req.ScryptTMTO
|
||||
req.ScryptTMTOV7 = &value
|
||||
}
|
||||
req.Skip, _ = flags.GetUint64("skip")
|
||||
req.Limit, _ = flags.GetUint64("limit")
|
||||
req.Keyspace, _ = flags.GetBool("keyspace")
|
||||
req.TotalCandidates, _ = flags.GetBool("total-candidates")
|
||||
req.Lookup, _ = flags.GetString("lookup")
|
||||
req.DynamicX, _ = flags.GetBool("dynamic-x")
|
||||
req.SeekDBPath, _ = flags.GetString("seekdb-path")
|
||||
|
||||
if raw, _ := flags.GetString("rules-file"); raw != "" {
|
||||
req.RulesFile = []byte(raw)
|
||||
req.RuleLeft, _ = flags.GetString("rule-left")
|
||||
req.RuleRight, _ = flags.GetString("rule-right")
|
||||
if values, _ := flags.GetStringArray("rules-file"); len(values) > 0 {
|
||||
req.RulesFile = []byte(values[0])
|
||||
for _, value := range values {
|
||||
req.RulesFilesV7 = append(req.RulesFilesV7, []byte(value))
|
||||
}
|
||||
}
|
||||
req.GenerateRules, _ = flags.GetUint32("generate-rules")
|
||||
req.GenerateRulesFunMin, _ = flags.GetUint32("generate-rules-fun-min")
|
||||
req.GenerateRulesFunMax, _ = flags.GetUint32("generate-rules-fun-max")
|
||||
req.GenerateRulesFunMin, _ = flags.GetUint32("generate-rules-func-min")
|
||||
if !flags.Changed("generate-rules-func-min") {
|
||||
req.GenerateRulesFunMin, _ = flags.GetUint32("generate-rules-fun-min")
|
||||
}
|
||||
if flags.Changed("generate-rules-func-min") || flags.Changed("generate-rules-fun-min") {
|
||||
value := req.GenerateRulesFunMin
|
||||
req.GenerateRulesFuncMinV7 = &value
|
||||
}
|
||||
req.GenerateRulesFunMax, _ = flags.GetUint32("generate-rules-func-max")
|
||||
if !flags.Changed("generate-rules-func-max") {
|
||||
req.GenerateRulesFunMax, _ = flags.GetUint32("generate-rules-fun-max")
|
||||
}
|
||||
if flags.Changed("generate-rules-func-max") || flags.Changed("generate-rules-fun-max") {
|
||||
value := req.GenerateRulesFunMax
|
||||
req.GenerateRulesFuncMaxV7 = &value
|
||||
}
|
||||
req.GenerateRulesFuncSel, _ = flags.GetString("generate-rules-func-sel")
|
||||
req.GenerateRulesSeed, _ = flags.GetInt32("generate-rules-seed")
|
||||
if flags.Changed("generate-rules-seed") && req.GenerateRulesSeed >= 0 {
|
||||
value := uint32(req.GenerateRulesSeed)
|
||||
req.GenerateRulesSeedV7 = &value
|
||||
}
|
||||
req.CustomCharset1, _ = flags.GetString("custom-charset1")
|
||||
req.CustomCharset2, _ = flags.GetString("custom-charset2")
|
||||
req.CustomCharset3, _ = flags.GetString("custom-charset3")
|
||||
req.CustomCharset4, _ = flags.GetString("custom-charset4")
|
||||
req.CustomCharset5, _ = flags.GetString("custom-charset5")
|
||||
req.CustomCharset6, _ = flags.GetString("custom-charset6")
|
||||
req.CustomCharset7, _ = flags.GetString("custom-charset7")
|
||||
req.CustomCharset8, _ = flags.GetString("custom-charset8")
|
||||
req.Identify, _ = flags.GetString("identify")
|
||||
req.IdentifyMode, _ = flags.GetBool("identify-mode")
|
||||
if req.IdentifyMode && (flags.Changed("hash-mode") || flags.Changed("hash-type")) {
|
||||
return nil, fmt.Errorf("--identify-mode and hash mode/type are mutually exclusive")
|
||||
}
|
||||
req.Increment, _ = flags.GetBool("increment")
|
||||
req.IncrementInverse, _ = flags.GetBool("increment-inverse")
|
||||
req.IncrementMin, _ = flags.GetUint32("increment-min")
|
||||
req.IncrementMax, _ = flags.GetUint32("increment-max")
|
||||
if flags.Changed("bypass-delay") {
|
||||
value, _ := flags.GetUint32("bypass-delay")
|
||||
req.BypassDelay = &value
|
||||
}
|
||||
if flags.Changed("bypass-threshold") {
|
||||
value, _ := flags.GetUint32("bypass-threshold")
|
||||
req.BypassThreshold = &value
|
||||
}
|
||||
req.SlowCandidates, _ = flags.GetBool("slow-candidates")
|
||||
req.BridgeParameter1, _ = flags.GetString("bridge-parameter1")
|
||||
req.BridgeParameter2, _ = flags.GetString("bridge-parameter2")
|
||||
req.BridgeParameter3, _ = flags.GetString("bridge-parameter3")
|
||||
req.BridgeParameter4, _ = flags.GetString("bridge-parameter4")
|
||||
req.BrainServer, _ = flags.GetBool("brain-server")
|
||||
req.BrainServerTimer, _ = flags.GetUint32("brain-server-timer")
|
||||
if flags.Changed("brain-server-timer") {
|
||||
value := req.BrainServerTimer
|
||||
req.BrainServerTimerV7 = &value
|
||||
}
|
||||
req.BrainClient, _ = flags.GetBool("brain-client")
|
||||
req.BrainClientFeatures, _ = flags.GetString("brain-client-features")
|
||||
if value, ok := parseUint32(req.BrainClientFeatures); ok {
|
||||
req.BrainClientFeaturesV7 = value
|
||||
}
|
||||
req.BrainHost, _ = flags.GetString("brain-host")
|
||||
req.BrainPort, _ = flags.GetUint32("brain-port")
|
||||
req.BrainPassword, _ = flags.GetString("brain-password")
|
||||
if flags.Changed("brain-password") {
|
||||
value := req.BrainPassword
|
||||
req.BrainPasswordV7 = &value
|
||||
}
|
||||
req.BrainSession, _ = flags.GetString("brain-session")
|
||||
if value, ok := parseHexUint32(req.BrainSession); ok {
|
||||
req.BrainSessionV7 = &value
|
||||
}
|
||||
req.BrainSessionWhitelist, _ = flags.GetString("brain-session-whitelist")
|
||||
if values, ok := parseHexUint32List(req.BrainSessionWhitelist); ok {
|
||||
req.BrainSessionWhitelistV7 = values
|
||||
}
|
||||
req.BrainFeed, _ = flags.GetBool("brain-feed")
|
||||
req.ColorCracked, _ = flags.GetBool("color-cracked")
|
||||
req.HashCopy, _ = flags.GetBool("hash-copy")
|
||||
req.EncryptWithPubkey, _ = flags.GetString("encrypt-with-pubkey")
|
||||
|
||||
return req, nil
|
||||
}
|
||||
@@ -214,6 +439,42 @@ func uintSliceToUint32(flags *pflag.FlagSet, name string) []uint32 {
|
||||
return out
|
||||
}
|
||||
|
||||
func parseUint32(raw string) (uint32, bool) {
|
||||
if raw == "" {
|
||||
return 0, false
|
||||
}
|
||||
value, err := strconv.ParseUint(raw, 10, 32)
|
||||
return uint32(value), err == nil
|
||||
}
|
||||
|
||||
func parseHexUint32(raw string) (uint32, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if strings.HasPrefix(raw, "0x") || strings.HasPrefix(raw, "0X") {
|
||||
raw = raw[2:]
|
||||
}
|
||||
if raw == "" {
|
||||
return 0, false
|
||||
}
|
||||
value, err := strconv.ParseUint(raw, 16, 32)
|
||||
return uint32(value), err == nil
|
||||
}
|
||||
|
||||
func parseHexUint32List(raw string) ([]uint32, bool) {
|
||||
if raw == "" {
|
||||
return nil, false
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
values := make([]uint32, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
value, ok := parseHexUint32(part)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values, true
|
||||
}
|
||||
|
||||
func parseCrackAttackMode(raw string) (clientpb.CrackAttackMode, error) {
|
||||
if raw == "" {
|
||||
return clientpb.CrackAttackMode_STRAIGHT, nil
|
||||
@@ -231,14 +492,20 @@ func parseCrackAttackMode(raw string) (clientpb.CrackAttackMode, error) {
|
||||
return clientpb.CrackAttackMode_COMBINATION, nil
|
||||
case "bruteforce", "brute_force", "brute-force":
|
||||
return clientpb.CrackAttackMode_BRUTEFORCE, nil
|
||||
case "pcfg":
|
||||
return clientpb.CrackAttackMode_PCFG, nil
|
||||
case "hybrid_wordlist_mask":
|
||||
return clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK, nil
|
||||
case "hybrid_mask_wordlist":
|
||||
return clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST, nil
|
||||
case "generic":
|
||||
return clientpb.CrackAttackMode_GENERIC, nil
|
||||
case "association":
|
||||
return clientpb.CrackAttackMode_ASSOCIATION, nil
|
||||
case "no_attack", "none":
|
||||
return clientpb.CrackAttackMode_NO_ATTACK, nil
|
||||
case "hybrid":
|
||||
return clientpb.CrackAttackMode_HYBRID, nil
|
||||
default:
|
||||
return clientpb.CrackAttackMode_STRAIGHT, fmt.Errorf("unknown attack mode: %s", raw)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestBuildCrackCommandReadsKeyboardLayoutMapping(t *testing.T) {
|
||||
mapping := []byte("a A\nb B\n")
|
||||
path := filepath.Join(t.TempDir(), "keyboard.hckmap")
|
||||
if err := os.WriteFile(path, mapping, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--keyboard-layout-mapping=" + path}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand() error = %v", err)
|
||||
}
|
||||
if !bytes.Equal(got.KeyboardLayoutMapping, mapping) {
|
||||
t.Fatalf("KeyboardLayoutMapping = %q, want file contents %q", got.KeyboardLayoutMapping, mapping)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandRejectsInvalidKeyboardLayoutMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path func(*testing.T) string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "missing",
|
||||
path: func(t *testing.T) string { return filepath.Join(t.TempDir(), "missing.hckmap") },
|
||||
want: "open keyboard layout mapping",
|
||||
},
|
||||
{
|
||||
name: "directory",
|
||||
path: func(t *testing.T) string { return t.TempDir() },
|
||||
want: "is not a regular file",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
path: func(t *testing.T) string {
|
||||
path := filepath.Join(t.TempDir(), "empty.hckmap")
|
||||
if err := os.WriteFile(path, nil, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
},
|
||||
want: "is empty",
|
||||
},
|
||||
{
|
||||
name: "too large",
|
||||
path: func(t *testing.T) string {
|
||||
path := filepath.Join(t.TempDir(), "large.hckmap")
|
||||
if err := os.WriteFile(path, make([]byte, maxKeyboardLayoutMappingBytes+1), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
},
|
||||
want: "exceeds",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--keyboard-layout-mapping=" + test.path(t)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := buildCrackCommand(cmd, nil); err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("buildCrackCommand() error = %v, want substring %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCrackAttackMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want clientpb.CrackAttackMode
|
||||
}{
|
||||
{name: "straight", want: clientpb.CrackAttackMode_STRAIGHT},
|
||||
{name: "combination", want: clientpb.CrackAttackMode_COMBINATION},
|
||||
{name: "bruteforce", want: clientpb.CrackAttackMode_BRUTEFORCE},
|
||||
{name: "pcfg", want: clientpb.CrackAttackMode_PCFG},
|
||||
{name: "hybrid-wordlist-mask", want: clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK},
|
||||
{name: "hybrid-mask-wordlist", want: clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST},
|
||||
{name: "generic", want: clientpb.CrackAttackMode_GENERIC},
|
||||
{name: "association", want: clientpb.CrackAttackMode_ASSOCIATION},
|
||||
{name: "no-attack", want: clientpb.CrackAttackMode_NO_ATTACK},
|
||||
{name: "hybrid", want: clientpb.CrackAttackMode_HYBRID},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := parseCrackAttackMode(test.name)
|
||||
if err != nil {
|
||||
t.Fatalf("parseCrackAttackMode(%q) returned error: %v", test.name, err)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Fatalf("parseCrackAttackMode(%q) = %v, want %v", test.name, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandHashcatShortFlags(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"-a", "3", "-m", "1000"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := buildCrackCommand(cmd, []string{"hash"})
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand() error = %v", err)
|
||||
}
|
||||
if got.AttackMode != clientpb.CrackAttackMode_BRUTEFORCE {
|
||||
t.Fatalf("AttackMode = %s, want BRUTEFORCE", got.AttackMode)
|
||||
}
|
||||
if got.HashType != clientpb.HashType(1000) {
|
||||
t.Fatalf("HashType = %d, want 1000", got.HashType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandHashcatV7Options(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
args := []string{
|
||||
"--attack-mode=hybrid",
|
||||
"--hash-mode=0",
|
||||
"--input=first-input",
|
||||
"--input=second-input",
|
||||
"--stdin=stdin-data",
|
||||
"--advice-disable",
|
||||
"--status-timer=0",
|
||||
"--stdin-timeout-abort=0",
|
||||
"--restore-position",
|
||||
"--outfile=outfile.txt",
|
||||
"--outfile-json",
|
||||
"--debug-file=debug.txt",
|
||||
"--induction-dir=induction",
|
||||
"--outfile-check-dir=outfile-check",
|
||||
"--outfile-check-timer=0",
|
||||
"--truecrypt-keyfiles=truecrypt.keys",
|
||||
"--veracrypt-keyfiles=veracrypt.keys",
|
||||
"--veracrypt-pim-start=11",
|
||||
"--veracrypt-pim-stop=12",
|
||||
"--encoding-from=windows-1252",
|
||||
"--encoding-to=utf-8",
|
||||
"--hccapx-message-pair=0",
|
||||
"--nonce-error-corrections=0",
|
||||
"--benchmark-min=13",
|
||||
"--benchmark-max=14",
|
||||
"--pipeline-stats",
|
||||
"--task-time-breakdown",
|
||||
"--bitmap-min=0",
|
||||
"--bitmap-max=0",
|
||||
"--hash-info",
|
||||
"--hash-info-level=2",
|
||||
"--backend-info",
|
||||
"--backend-info-level=2",
|
||||
"--backend-devices-virtmulti=15",
|
||||
"--backend-devices-virthost=16",
|
||||
"--metal-compiler-runtime=17",
|
||||
"--multiply-accel-disable",
|
||||
"--hwmon-temp-abort=0",
|
||||
"--total-candidates",
|
||||
"--lookup=lookup-value",
|
||||
"--crackstation= station-one ",
|
||||
"--dynamic-x",
|
||||
"--seekdb-path=seekdb",
|
||||
"--rule-left=l",
|
||||
"--rule-right=r",
|
||||
"--rules-file=:",
|
||||
"--rules-file=$1",
|
||||
"--custom-charset5=cs5",
|
||||
"--custom-charset6=cs6",
|
||||
"--custom-charset7=cs7",
|
||||
"--custom-charset8=cs8",
|
||||
"--identify=legacy-input",
|
||||
"--increment-inverse",
|
||||
"--bypass-delay=18",
|
||||
"--bypass-threshold=19",
|
||||
"--bridge-parameter1=bridge1",
|
||||
"--bridge-parameter2=bridge2",
|
||||
"--bridge-parameter3=bridge3",
|
||||
"--bridge-parameter4=bridge4",
|
||||
"--scrypt-tmto=0",
|
||||
"--generate-rules-func-min=4",
|
||||
"--generate-rules-func-max=8",
|
||||
"--generate-rules-seed=0",
|
||||
"--brain-client-features=3",
|
||||
"--brain-password=",
|
||||
"--brain-server-timer=0",
|
||||
"--brain-session=deadbeef",
|
||||
"--brain-session-whitelist=1,2,0x3",
|
||||
"--brain-feed",
|
||||
"--color-cracked",
|
||||
"--hash-copy",
|
||||
"--encrypt-with-pubkey=key.pub",
|
||||
}
|
||||
if err := cmd.Flags().Parse(args); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
zero := uint32(0)
|
||||
want := &clientpb.CrackCommand{
|
||||
AttackMode: clientpb.CrackAttackMode_HYBRID,
|
||||
HashType: clientpb.HashType_INVALID,
|
||||
HashMode: &zero,
|
||||
Stdin: []byte("stdin-data"),
|
||||
AdviceDisable: true,
|
||||
StatusTimerV7: &zero,
|
||||
StdinTimeoutAbortV7: &zero,
|
||||
Outfile: "outfile.txt",
|
||||
DebugFile: "debug.txt",
|
||||
InductionDir: "induction",
|
||||
OutfileCheckDir: "outfile-check",
|
||||
OutfileCheckTimerV7: &zero,
|
||||
TruecryptKeyfiles: "truecrypt.keys",
|
||||
VeracryptKeyfiles: "veracrypt.keys",
|
||||
VeracryptPimStart: uint32Pointer(11),
|
||||
VeracryptPimStop: uint32Pointer(12),
|
||||
EncodingFromName: "windows-1252",
|
||||
EncodingToName: "utf-8",
|
||||
HccapxMessagePairV7: &zero,
|
||||
NonceErrorCorrectionsV7: &zero,
|
||||
RuleLeft: "l",
|
||||
RuleRight: "r",
|
||||
RulesFile: []byte(":"),
|
||||
RulesFilesV7: [][]byte{[]byte(":"), []byte("$1")},
|
||||
PipelineStats: true,
|
||||
TaskTimeBreakdown: true,
|
||||
BitmapMinV7: &zero,
|
||||
BitmapMaxV7: &zero,
|
||||
HashInfo: true,
|
||||
HashInfoLevel: 2,
|
||||
BackendInfo: true,
|
||||
BackendInfoLevel: 2,
|
||||
MetalCompilerRuntime: 17,
|
||||
RestorePosition: true,
|
||||
OutfileJSON: true,
|
||||
DynamicX: true,
|
||||
SeekDBPath: "seekdb",
|
||||
BenchmarkMin: 13,
|
||||
BenchmarkMax: uint32Pointer(14),
|
||||
BridgeParameter1: "bridge1",
|
||||
BridgeParameter2: "bridge2",
|
||||
BridgeParameter3: "bridge3",
|
||||
BridgeParameter4: "bridge4",
|
||||
BackendDevicesVirtMulti: 15,
|
||||
BackendDevicesVirtHost: 16,
|
||||
MultiplyAccelDisabled: true,
|
||||
HwmonTempAbortV7: &zero,
|
||||
TotalCandidates: true,
|
||||
Lookup: "lookup-value",
|
||||
Crackstation: "station-one",
|
||||
CustomCharset5: "cs5",
|
||||
CustomCharset6: "cs6",
|
||||
CustomCharset7: "cs7",
|
||||
CustomCharset8: "cs8",
|
||||
IncrementInverse: true,
|
||||
BypassDelay: uint32Pointer(18),
|
||||
BypassThreshold: uint32Pointer(19),
|
||||
ScryptTMTOV7: &zero,
|
||||
GenerateRulesFunMin: 4,
|
||||
GenerateRulesFunMax: 8,
|
||||
GenerateRulesFuncMinV7: uint32Pointer(4),
|
||||
GenerateRulesFuncMaxV7: uint32Pointer(8),
|
||||
GenerateRulesSeedV7: &zero,
|
||||
BrainClientFeatures: "3",
|
||||
BrainClientFeaturesV7: 3,
|
||||
BrainPasswordV7: stringPointer(""),
|
||||
BrainSession: "deadbeef",
|
||||
BrainSessionV7: uint32Pointer(0xdeadbeef),
|
||||
BrainServerTimerV7: &zero,
|
||||
BrainSessionWhitelist: "1,2,0x3",
|
||||
BrainSessionWhitelistV7: []uint32{1, 2, 3},
|
||||
BrainFeed: true,
|
||||
ColorCracked: true,
|
||||
HashCopy: true,
|
||||
EncryptWithPubkey: "key.pub",
|
||||
Identify: "legacy-input", //nolint:staticcheck // Verify the legacy positional operand remains populated for wire compatibility.
|
||||
PositionalArguments: []string{"first-input", "second-input"},
|
||||
}
|
||||
if !proto.Equal(got, want) {
|
||||
t.Fatal("buildCrackCommand() did not map the Hashcat v7 options as expected")
|
||||
}
|
||||
}
|
||||
|
||||
func uint32Pointer(value uint32) *uint32 {
|
||||
return &value
|
||||
}
|
||||
|
||||
func stringPointer(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandBrainSessionsAreHexadecimal(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--brain-session=10",
|
||||
"--brain-session-whitelist=10,20",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
if got.BrainSessionV7 == nil || *got.BrainSessionV7 != 0x10 {
|
||||
t.Fatalf("BrainSessionV7 = %v, want 0x10", got.BrainSessionV7)
|
||||
}
|
||||
wantWhitelist := []uint32{0x10, 0x20}
|
||||
if !slices.Equal(got.BrainSessionWhitelistV7, wantWhitelist) {
|
||||
t.Fatalf("BrainSessionWhitelistV7 = %#v, want %#v", got.BrainSessionWhitelistV7, wantWhitelist)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandPreservesCommasInPositionalInputs(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--input=hashes,2026.txt",
|
||||
"--input=?d?d,?l?l",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
want := []string{"hashes,2026.txt", "?d?d,?l?l"}
|
||||
if !slices.Equal(got.PositionalArguments, want) {
|
||||
t.Fatalf("PositionalArguments = %#v, want %#v", got.PositionalArguments, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandPreservesCommasInHashes(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--hash=digest:salt,with,commas",
|
||||
"--hash=second:digest,salt",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
want := []string{"digest:salt,with,commas", "second:digest,salt"}
|
||||
if !slices.Equal(got.Hashes, want) {
|
||||
t.Fatalf("Hashes = %#v, want %#v", got.Hashes, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandRejectsConflictingRestoreModes(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--restore-position", "--restore-show-command"}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
_, err := buildCrackCommand(cmd, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
|
||||
t.Fatalf("buildCrackCommand() error = %v, want restore conflict error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandIdentifyHasNoImplicitHashType(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--identify-mode", "--input=hashes.txt"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.HashType != clientpb.HashType_INVALID || got.HashMode != nil {
|
||||
t.Fatalf("identify command inherited hash mode/type: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandLegacyFlagAliases(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--multiply-accel-disabled",
|
||||
"--generate-rules-fun-min=2",
|
||||
"--generate-rules-fun-max=6",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
if !got.MultiplyAccelDisabled || got.GenerateRulesFunMin != 2 || got.GenerateRulesFunMax != 6 {
|
||||
t.Fatalf("legacy aliases were not preserved: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandRejectsSegmentSize(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Set("segment-size", "32"); err != nil {
|
||||
t.Fatalf("set segment-size: %v", err)
|
||||
}
|
||||
|
||||
_, err := buildCrackCommand(cmd, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "removed in Hashcat v7") {
|
||||
t.Fatalf("buildCrackCommand() error = %v, want Hashcat v7 removal error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandCredentialSelectors(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--credential= first-id ",
|
||||
"--credential=second-id",
|
||||
"--credential-collection= domain-admins ",
|
||||
"--include-cracked-credentials",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
if !slices.Equal(got.CredentialIDs, []string{"first-id", "second-id"}) {
|
||||
t.Fatalf("CredentialIDs = %#v", got.CredentialIDs)
|
||||
}
|
||||
if got.CredentialCollection != "domain-admins" {
|
||||
t.Fatalf("CredentialCollection = %q", got.CredentialCollection)
|
||||
}
|
||||
if !got.IncludeCrackedCredentials {
|
||||
t.Fatal("IncludeCrackedCredentials is false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandRejectsEmptyCredential(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--credential= "}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
if _, err := buildCrackCommand(cmd, nil); err == nil || !strings.Contains(err.Error(), "cannot be empty") {
|
||||
t.Fatalf("buildCrackCommand error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandIncludeCrackedRequiresSelector(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--include-cracked-credentials"}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
if _, err := buildCrackCommand(cmd, nil); err == nil || !strings.Contains(err.Error(), "requires --credential") {
|
||||
t.Fatalf("buildCrackCommand error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
const (
|
||||
maxCrackBackendInfoLevel uint32 = 2
|
||||
maxCrackHashInfoLevel uint32 = 2
|
||||
)
|
||||
|
||||
type crackInvocationMode uint8
|
||||
|
||||
const (
|
||||
crackInvocationDashboard crackInvocationMode = iota
|
||||
crackInvocationJob
|
||||
crackInvocationBackendInfo
|
||||
crackInvocationKeyspace
|
||||
crackInvocationTotalCandidates
|
||||
crackInvocationLookup
|
||||
crackInvocationIdentify
|
||||
crackInvocationHashInfo
|
||||
)
|
||||
|
||||
type crackInvocation struct {
|
||||
mode crackInvocationMode
|
||||
backendInfoLevel uint32
|
||||
hashInfoLevel uint32
|
||||
crackstation string
|
||||
}
|
||||
|
||||
type crackQueryModeSelection struct {
|
||||
mode crackInvocationMode
|
||||
flagName string
|
||||
}
|
||||
|
||||
var incompatibleCandidateQueryFlags = map[string]struct{}{
|
||||
"benchmark": {},
|
||||
"benchmark-all": {},
|
||||
"benchmark-min": {},
|
||||
"benchmark-max": {},
|
||||
"brain-client": {},
|
||||
"brain-client-features": {},
|
||||
"brain-feed": {},
|
||||
"brain-host": {},
|
||||
"brain-password": {},
|
||||
"brain-port": {},
|
||||
"brain-server": {},
|
||||
"brain-server-timer": {},
|
||||
"brain-session": {},
|
||||
"brain-session-whitelist": {},
|
||||
"left": {},
|
||||
"progress-only": {},
|
||||
"restore": {},
|
||||
"restore-file": {},
|
||||
"restore-position": {},
|
||||
"restore-show-command": {},
|
||||
"show": {},
|
||||
"speed-only": {},
|
||||
}
|
||||
|
||||
// classifyCrackInvocation separates terminal command modes from work that
|
||||
// belongs in the distributed crack queue. Backend information comes from the
|
||||
// registration snapshots; the remaining query modes execute synchronously on
|
||||
// one idle crackstation and never create a CrackJob.
|
||||
//
|
||||
//nolint:gocyclo // The ordered flag matrix keeps mutually exclusive query modes and their validation in one decision point.
|
||||
func classifyCrackInvocation(cmd *cobra.Command, args []string) (crackInvocation, error) {
|
||||
flags := cmd.Flags()
|
||||
backendInfo, _ := flags.GetBool("backend-info")
|
||||
backendInfoLevel, _ := flags.GetUint32("backend-info-level")
|
||||
backendInfoLevelChanged := flags.Changed("backend-info-level")
|
||||
hashInfo, _ := flags.GetBool("hash-info")
|
||||
hashInfoLevel, _ := flags.GetUint32("hash-info-level")
|
||||
hashInfoLevelChanged := flags.Changed("hash-info-level")
|
||||
keyspace, _ := flags.GetBool("keyspace")
|
||||
totalCandidates, _ := flags.GetBool("total-candidates")
|
||||
lookup, _ := flags.GetString("lookup")
|
||||
lookupChanged := flags.Changed("lookup")
|
||||
identify, _ := flags.GetBool("identify-mode")
|
||||
crackstation, _ := flags.GetString("crackstation")
|
||||
crackstation = strings.TrimSpace(crackstation)
|
||||
|
||||
if flags.Changed("crackstation") && crackstation == "" {
|
||||
return crackInvocation{}, fmt.Errorf("--crackstation cannot be empty")
|
||||
}
|
||||
if lookupChanged && lookup == "" {
|
||||
return crackInvocation{}, fmt.Errorf("--lookup cannot be empty")
|
||||
}
|
||||
|
||||
selections := make([]crackQueryModeSelection, 0, 6)
|
||||
if backendInfo || backendInfoLevelChanged {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationBackendInfo, flagName: "--backend-info"})
|
||||
}
|
||||
if keyspace {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationKeyspace, flagName: "--keyspace"})
|
||||
}
|
||||
if totalCandidates {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationTotalCandidates, flagName: "--total-candidates"})
|
||||
}
|
||||
if lookupChanged {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationLookup, flagName: "--lookup"})
|
||||
}
|
||||
if identify {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationIdentify, flagName: "--identify-mode"})
|
||||
}
|
||||
if hashInfo || hashInfoLevelChanged {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationHashInfo, flagName: "--hash-info"})
|
||||
}
|
||||
if len(selections) > 1 {
|
||||
names := make([]string, 0, len(selections))
|
||||
for _, selection := range selections {
|
||||
names = append(names, selection.flagName)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return crackInvocation{}, fmt.Errorf("crack query modes cannot be combined: %s", strings.Join(names, ", "))
|
||||
}
|
||||
if len(selections) == 0 {
|
||||
if flags.Changed("crackstation") {
|
||||
return crackInvocation{}, fmt.Errorf("--crackstation requires --keyspace, --total-candidates, --lookup, --identify-mode, or --hash-info")
|
||||
}
|
||||
if shouldRunCrack(cmd, args) {
|
||||
return crackInvocation{mode: crackInvocationJob}, nil
|
||||
}
|
||||
return crackInvocation{mode: crackInvocationDashboard}, nil
|
||||
}
|
||||
|
||||
invocation := crackInvocation{mode: selections[0].mode, crackstation: crackstation}
|
||||
switch invocation.mode {
|
||||
case crackInvocationBackendInfo:
|
||||
if !backendInfoLevelChanged {
|
||||
backendInfoLevel = 1
|
||||
}
|
||||
if backendInfoLevel == 0 || backendInfoLevel > maxCrackBackendInfoLevel {
|
||||
return crackInvocation{}, fmt.Errorf("--backend-info-level must be 1 or %d", maxCrackBackendInfoLevel)
|
||||
}
|
||||
conflicts := changedCrackFlagConflicts(flags, map[string]struct{}{
|
||||
"backend-info": {}, "backend-info-level": {}, "timeout": {},
|
||||
})
|
||||
if len(args) != 0 {
|
||||
conflicts = append(conflicts, "positional hash arguments")
|
||||
}
|
||||
if len(conflicts) != 0 {
|
||||
sort.Strings(conflicts)
|
||||
return crackInvocation{}, fmt.Errorf("backend-info query cannot be combined with %s", strings.Join(conflicts, ", "))
|
||||
}
|
||||
invocation.backendInfoLevel = backendInfoLevel
|
||||
case crackInvocationHashInfo:
|
||||
if !hashInfoLevelChanged {
|
||||
hashInfoLevel = 1
|
||||
}
|
||||
if hashInfoLevel == 0 || hashInfoLevel > maxCrackHashInfoLevel {
|
||||
return crackInvocation{}, fmt.Errorf("--hash-info-level must be 1 or %d", maxCrackHashInfoLevel)
|
||||
}
|
||||
conflicts := changedCrackFlagConflicts(flags, map[string]struct{}{
|
||||
"crackstation": {}, "hash-info": {}, "hash-info-level": {}, "hash-mode": {}, "hash-type": {}, "timeout": {},
|
||||
})
|
||||
if len(args) != 0 {
|
||||
conflicts = append(conflicts, "positional hash arguments")
|
||||
}
|
||||
if len(conflicts) != 0 {
|
||||
sort.Strings(conflicts)
|
||||
return crackInvocation{}, fmt.Errorf("hash-info query cannot be combined with %s", strings.Join(conflicts, ", "))
|
||||
}
|
||||
invocation.hashInfoLevel = hashInfoLevel
|
||||
case crackInvocationIdentify:
|
||||
conflicts := changedCrackFlagConflicts(flags, map[string]struct{}{
|
||||
"crackstation": {}, "hash": {}, "identify-mode": {}, "timeout": {},
|
||||
})
|
||||
if len(conflicts) != 0 {
|
||||
return crackInvocation{}, fmt.Errorf("identify query cannot be combined with %s", strings.Join(conflicts, ", "))
|
||||
}
|
||||
if !hasCrackIdentifyInput(flags, args) {
|
||||
return crackInvocation{}, fmt.Errorf("--identify-mode requires a positional hash or --hash")
|
||||
}
|
||||
case crackInvocationKeyspace, crackInvocationTotalCandidates, crackInvocationLookup:
|
||||
if conflicts := candidateQueryConflicts(flags, args, invocation.mode); len(conflicts) != 0 {
|
||||
return crackInvocation{}, fmt.Errorf("%s query cannot be combined with %s", crackInvocationName(invocation.mode), strings.Join(conflicts, ", "))
|
||||
}
|
||||
}
|
||||
return invocation, nil
|
||||
}
|
||||
|
||||
func crackInvocationName(mode crackInvocationMode) string {
|
||||
switch mode {
|
||||
case crackInvocationBackendInfo:
|
||||
return "backend-info"
|
||||
case crackInvocationKeyspace:
|
||||
return "keyspace"
|
||||
case crackInvocationTotalCandidates:
|
||||
return "total-candidates"
|
||||
case crackInvocationLookup:
|
||||
return "lookup"
|
||||
case crackInvocationIdentify:
|
||||
return "identify"
|
||||
case crackInvocationHashInfo:
|
||||
return "hash-info"
|
||||
default:
|
||||
return "crack"
|
||||
}
|
||||
}
|
||||
|
||||
func changedCrackFlagConflicts(flags *pflag.FlagSet, allowed map[string]struct{}) []string {
|
||||
conflicts := make([]string, 0)
|
||||
flags.Visit(func(flag *pflag.Flag) {
|
||||
if _, ok := allowed[flag.Name]; ok || changedBoolFlagIsFalse(flags, flag) {
|
||||
return
|
||||
}
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
})
|
||||
sort.Strings(conflicts)
|
||||
return conflicts
|
||||
}
|
||||
|
||||
func changedBoolFlagIsFalse(flags *pflag.FlagSet, flag *pflag.Flag) bool {
|
||||
if flag.Value.Type() != "bool" {
|
||||
return false
|
||||
}
|
||||
value, err := flags.GetBool(flag.Name)
|
||||
return err == nil && !value
|
||||
}
|
||||
|
||||
func hasCrackIdentifyInput(flags *pflag.FlagSet, args []string) bool {
|
||||
if len(args) != 0 {
|
||||
return true
|
||||
}
|
||||
hashes, _ := flags.GetStringArray("hash")
|
||||
return len(hashes) != 0
|
||||
}
|
||||
|
||||
func candidateQueryConflicts(flags *pflag.FlagSet, args []string, mode crackInvocationMode) []string {
|
||||
conflicts := make([]string, 0)
|
||||
if len(args) != 0 {
|
||||
conflicts = append(conflicts, "positional hash arguments")
|
||||
}
|
||||
flags.Visit(func(flag *pflag.Flag) {
|
||||
if changedBoolFlagIsFalse(flags, flag) {
|
||||
return
|
||||
}
|
||||
switch flag.Name {
|
||||
case "hash", "credential", "credential-collection":
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
return
|
||||
case "include-cracked-credentials":
|
||||
includeCracked, _ := flags.GetBool(flag.Name)
|
||||
if includeCracked {
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
}
|
||||
return
|
||||
case "skip", "limit":
|
||||
if mode != crackInvocationLookup {
|
||||
value, _ := flags.GetUint64(flag.Name)
|
||||
if value != 0 {
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, incompatible := incompatibleCandidateQueryFlags[flag.Name]; incompatible {
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
}
|
||||
})
|
||||
sort.Strings(conflicts)
|
||||
return conflicts
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
func newCrackDispatchCommand(t *testing.T, flags ...string) *cobra.Command {
|
||||
t.Helper()
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
cmd.SetContext(context.Background())
|
||||
cmd.Flags().Int64("timeout", 0, "test timeout")
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse(flags); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func TestClassifyCrackInvocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
args []string
|
||||
wantMode crackInvocationMode
|
||||
wantBackendLevel uint32
|
||||
wantHashLevel uint32
|
||||
wantCrackstation string
|
||||
wantError string
|
||||
}{
|
||||
{name: "dashboard", wantMode: crackInvocationDashboard},
|
||||
{name: "timeout only", flags: []string{"--timeout=5"}, wantMode: crackInvocationDashboard},
|
||||
{name: "positional hash job", args: []string{"hash"}, wantMode: crackInvocationJob},
|
||||
{name: "job flag", flags: []string{"--attack-mode=3"}, wantMode: crackInvocationJob},
|
||||
{name: "explicit false is a no-op", flags: []string{"--backend-info=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "explicit false with job flag", flags: []string{"--backend-info=false", "--attack-mode=3"}, wantMode: crackInvocationJob},
|
||||
{name: "keyspace false is a no-op", flags: []string{"--keyspace=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "keyspace false with job flag", flags: []string{"--keyspace=false", "--attack-mode=3"}, wantMode: crackInvocationJob},
|
||||
{name: "keyspace", flags: []string{"--keyspace"}, wantMode: crackInvocationKeyspace},
|
||||
{name: "keyspace candidate flags", flags: []string{"--keyspace", "--attack-mode=3", "--input=?d?d", "--increment"}, wantMode: crackInvocationKeyspace},
|
||||
{name: "keyspace selected station", flags: []string{"--keyspace", "--crackstation= station-one "}, wantMode: crackInvocationKeyspace, wantCrackstation: "station-one"},
|
||||
{name: "keyspace timeout", flags: []string{"--keyspace", "--timeout=5"}, wantMode: crackInvocationKeyspace},
|
||||
{name: "keyspace skip conflict", flags: []string{"--keyspace", "--skip=1"}, wantError: "--skip"},
|
||||
{name: "keyspace zero skip no-op", flags: []string{"--keyspace", "--skip=0"}, wantMode: crackInvocationKeyspace},
|
||||
{name: "keyspace positional hash conflict", flags: []string{"--keyspace"}, args: []string{"hash"}, wantError: "positional hash arguments"},
|
||||
{name: "keyspace hash flag conflict", flags: []string{"--keyspace", "--hash=digest"}, wantError: "--hash"},
|
||||
{name: "keyspace credential conflict", flags: []string{"--keyspace", "--credential=deadbeef"}, wantError: "--credential"},
|
||||
{name: "keyspace collection conflict", flags: []string{"--keyspace", "--credential-collection=secrets"}, wantError: "--credential-collection"},
|
||||
{name: "keyspace include cracked conflict", flags: []string{"--keyspace", "--include-cracked-credentials"}, wantError: "--include-cracked-credentials"},
|
||||
{name: "keyspace and backend info conflict", flags: []string{"--keyspace", "--backend-info"}, wantError: "--keyspace"},
|
||||
{name: "backend info ignores disabled keyspace", flags: []string{"--backend-info", "--keyspace=false"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
|
||||
{name: "backend info", flags: []string{"--backend-info"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
|
||||
{name: "backend info and timeout", flags: []string{"--backend-info", "--timeout=5"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
|
||||
{name: "level one", flags: []string{"--backend-info-level=1"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
|
||||
{name: "level two", flags: []string{"--backend-info-level=2"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 2},
|
||||
{name: "explicit level wins", flags: []string{"--backend-info", "--backend-info-level=2"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 2},
|
||||
{name: "level independently enables mode", flags: []string{"--backend-info=false", "--backend-info-level=2"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 2},
|
||||
{name: "zero level", flags: []string{"--backend-info-level=0"}, wantError: "must be 1 or 2"},
|
||||
{name: "unsupported level", flags: []string{"--backend-info-level=3"}, wantError: "must be 1 or 2"},
|
||||
{name: "positional conflict", flags: []string{"--backend-info"}, args: []string{"hash"}, wantError: "positional hash arguments"},
|
||||
{name: "job flag conflict", flags: []string{"--backend-info", "--benchmark"}, wantError: "--benchmark"},
|
||||
{name: "input conflict", flags: []string{"--backend-info-level=2", "--input=wordlist"}, wantError: "--input"},
|
||||
{name: "backend info station conflict", flags: []string{"--backend-info", "--crackstation=station-one"}, wantError: "--crackstation"},
|
||||
{name: "total candidates", flags: []string{"--total-candidates", "--attack-mode=3", "--input=?d"}, wantMode: crackInvocationTotalCandidates},
|
||||
{name: "total candidates false", flags: []string{"--total-candidates=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "total candidates hash conflict", flags: []string{"--total-candidates", "--hash=digest"}, wantError: "--hash"},
|
||||
{name: "total candidates limit conflict", flags: []string{"--total-candidates", "--limit=1"}, wantError: "--limit"},
|
||||
{name: "lookup", flags: []string{"--lookup=candidate", "--attack-mode=3", "--input=?d"}, wantMode: crackInvocationLookup},
|
||||
{name: "lookup allows window", flags: []string{"--lookup=candidate", "--skip=1", "--limit=2"}, wantMode: crackInvocationLookup},
|
||||
{name: "lookup empty", flags: []string{"--lookup="}, wantError: "cannot be empty"},
|
||||
{name: "query modes conflict", flags: []string{"--keyspace", "--total-candidates"}, wantError: "cannot be combined"},
|
||||
{name: "candidate query terminal conflict", flags: []string{"--keyspace", "--show"}, wantError: "--show"},
|
||||
{name: "identify positional", flags: []string{"--identify-mode"}, args: []string{"digest"}, wantMode: crackInvocationIdentify},
|
||||
{name: "identify hash flag", flags: []string{"--identify-mode", "--hash=digest"}, wantMode: crackInvocationIdentify},
|
||||
{name: "identify false", flags: []string{"--identify-mode=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "identify missing hash", flags: []string{"--identify-mode"}, wantError: "requires a positional hash or --hash"},
|
||||
{name: "identify credential conflict", flags: []string{"--identify-mode", "--credential=deadbeef"}, wantError: "--credential"},
|
||||
{name: "identify hash mode conflict", flags: []string{"--identify-mode", "--hash=digest", "--hash-mode=0"}, wantError: "--hash-mode"},
|
||||
{name: "hash info", flags: []string{"--hash-info"}, wantMode: crackInvocationHashInfo, wantHashLevel: 1},
|
||||
{name: "hash info false", flags: []string{"--hash-info=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "hash info level", flags: []string{"--hash-info=false", "--hash-info-level=2", "--hash-mode=0", "--crackstation=station-one"}, wantMode: crackInvocationHashInfo, wantHashLevel: 2, wantCrackstation: "station-one"},
|
||||
{name: "hash info zero level", flags: []string{"--hash-info-level=0"}, wantError: "must be 1 or 2"},
|
||||
{name: "hash info unsupported level", flags: []string{"--hash-info-level=3"}, wantError: "must be 1 or 2"},
|
||||
{name: "hash info hash conflict", flags: []string{"--hash-info", "--hash=digest"}, wantError: "--hash"},
|
||||
{name: "crackstation requires query", flags: []string{"--crackstation=station-one"}, wantError: "requires --keyspace"},
|
||||
{name: "crackstation cannot be empty", flags: []string{"--keyspace", "--crackstation= "}, wantError: "cannot be empty"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
invocation, err := classifyCrackInvocation(newCrackDispatchCommand(t, test.flags...), test.args)
|
||||
if test.wantError != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantError) {
|
||||
t.Fatalf("classifyCrackInvocation() error = %v, want substring %q", err, test.wantError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("classifyCrackInvocation() error = %v", err)
|
||||
}
|
||||
if invocation.mode != test.wantMode || invocation.backendInfoLevel != test.wantBackendLevel ||
|
||||
invocation.hashInfoLevel != test.wantHashLevel || invocation.crackstation != test.wantCrackstation {
|
||||
t.Fatalf("classifyCrackInvocation() = %#v, want mode %d backend level %d hash level %d station %q",
|
||||
invocation, test.wantMode, test.wantBackendLevel, test.wantHashLevel, test.wantCrackstation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type crackDispatchRPCCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
crackstationsCalls int
|
||||
crackCalls int
|
||||
crackFilesCalls int
|
||||
hadDeadline bool
|
||||
crackHadDeadline bool
|
||||
err error
|
||||
response *clientpb.Crackstations
|
||||
crackResponse *clientpb.CrackResponse
|
||||
crackFilesResponse *clientpb.CrackFiles
|
||||
crackRequest *clientpb.CrackCommand
|
||||
}
|
||||
|
||||
func (capture *crackDispatchRPCCapture) Crackstations(ctx context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.Crackstations, error) {
|
||||
capture.crackstationsCalls++
|
||||
_, capture.hadDeadline = ctx.Deadline()
|
||||
if capture.err != nil {
|
||||
return nil, capture.err
|
||||
}
|
||||
if capture.response != nil {
|
||||
return capture.response, nil
|
||||
}
|
||||
return &clientpb.Crackstations{}, nil
|
||||
}
|
||||
|
||||
func (capture *crackDispatchRPCCapture) Crack(ctx context.Context, request *clientpb.CrackCommand, _ ...grpc.CallOption) (*clientpb.CrackResponse, error) {
|
||||
capture.crackCalls++
|
||||
_, capture.crackHadDeadline = ctx.Deadline()
|
||||
capture.crackRequest = request
|
||||
if capture.crackResponse != nil {
|
||||
return capture.crackResponse, nil
|
||||
}
|
||||
return &clientpb.CrackResponse{}, nil
|
||||
}
|
||||
|
||||
func (capture *crackDispatchRPCCapture) CrackFilesList(_ context.Context, _ *clientpb.CrackFile, _ ...grpc.CallOption) (*clientpb.CrackFiles, error) {
|
||||
capture.crackFilesCalls++
|
||||
if capture.crackFilesResponse != nil {
|
||||
return capture.crackFilesResponse, nil
|
||||
}
|
||||
return &clientpb.CrackFiles{}, nil
|
||||
}
|
||||
|
||||
func TestCrackBackendInfoQueriesStationsWithoutCreatingJob(t *testing.T) {
|
||||
capture := &crackDispatchRPCCapture{}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
CrackCmd(newCrackDispatchCommand(t, "--backend-info", "--timeout=1"), con, nil)
|
||||
if capture.crackstationsCalls != 1 {
|
||||
t.Fatalf("Crackstations calls = %d, want 1", capture.crackstationsCalls)
|
||||
}
|
||||
if capture.crackCalls != 0 || capture.crackFilesCalls != 0 {
|
||||
t.Fatalf("backend-info used job path: Crack=%d CrackFilesList=%d", capture.crackCalls, capture.crackFilesCalls)
|
||||
}
|
||||
if !capture.hadDeadline {
|
||||
t.Fatal("Crackstations context did not inherit --timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackKeyspaceUsesSynchronousCrackResponse(t *testing.T) {
|
||||
digest := strings.Repeat("a", 64)
|
||||
capture := &crackDispatchRPCCapture{
|
||||
crackResponse: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE, Value: "100",
|
||||
}},
|
||||
crackFilesResponse: &clientpb.CrackFiles{
|
||||
Files: []*clientpb.CrackFile{{
|
||||
ID: "wordlist-id", Name: "words.txt", Type: clientpb.CrackFileType_WORDLIST, Sha2_256: digest,
|
||||
}},
|
||||
},
|
||||
}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
CrackCmd(newCrackDispatchCommand(t, "--keyspace", "--input=words.txt", "--crackstation=station-one", "--timeout=1"), con, nil)
|
||||
if capture.crackCalls != 1 {
|
||||
t.Fatalf("Crack calls = %d, want 1", capture.crackCalls)
|
||||
}
|
||||
if capture.crackstationsCalls != 0 {
|
||||
t.Fatalf("keyspace queried crackstations directly %d times", capture.crackstationsCalls)
|
||||
}
|
||||
if capture.crackFilesCalls != 1 {
|
||||
t.Fatalf("CrackFilesList calls = %d, want 1", capture.crackFilesCalls)
|
||||
}
|
||||
if !capture.crackHadDeadline {
|
||||
t.Fatal("Crack context did not inherit --timeout")
|
||||
}
|
||||
if capture.crackRequest == nil || !capture.crackRequest.Keyspace {
|
||||
t.Fatalf("Crack request = %#v, want keyspace mode", capture.crackRequest)
|
||||
}
|
||||
if capture.crackRequest.Crackstation != "station-one" {
|
||||
t.Fatalf("Crack request station = %q, want station-one", capture.crackRequest.Crackstation)
|
||||
}
|
||||
wantInput := "crackfile://wordlist/" + digest
|
||||
if len(capture.crackRequest.PositionalArguments) != 1 || capture.crackRequest.PositionalArguments[0] != wantInput {
|
||||
t.Fatalf("resolved keyspace inputs = %#v, want %q", capture.crackRequest.PositionalArguments, wantInput)
|
||||
}
|
||||
if len(capture.crackRequest.Hashes) != 0 || len(capture.crackRequest.CredentialIDs) != 0 {
|
||||
t.Fatalf("keyspace request contains hash selectors: %#v", capture.crackRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackSynchronousQueriesUseCrackRPCWithoutCreatingJob(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
args []string
|
||||
resultMode clientpb.CrackQueryMode
|
||||
result string
|
||||
check func(*testing.T, *clientpb.CrackCommand)
|
||||
}{
|
||||
{
|
||||
name: "total candidates",
|
||||
flags: []string{"--total-candidates", "--attack-mode=3", "--input=?d", "--crackstation=station-one"},
|
||||
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_TOTAL_CANDIDATES,
|
||||
result: "100",
|
||||
check: func(t *testing.T, command *clientpb.CrackCommand) {
|
||||
if !command.TotalCandidates || command.Crackstation != "station-one" {
|
||||
t.Fatalf("total-candidates request = %#v", command)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lookup",
|
||||
flags: []string{"--lookup=candidate", "--attack-mode=3", "--input=?d"},
|
||||
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP,
|
||||
result: "lookup output",
|
||||
check: func(t *testing.T, command *clientpb.CrackCommand) {
|
||||
if command.Lookup != "candidate" {
|
||||
t.Fatalf("lookup request = %#v", command)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "identify",
|
||||
flags: []string{"--identify-mode"},
|
||||
args: []string{"digest"},
|
||||
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_IDENTIFY,
|
||||
result: "identify output",
|
||||
check: func(t *testing.T, command *clientpb.CrackCommand) {
|
||||
if !command.IdentifyMode || len(command.Hashes) != 1 || command.Hashes[0] != "digest" {
|
||||
t.Fatalf("identify request = %#v", command)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hash info",
|
||||
flags: []string{"--hash-info-level=2", "--hash-mode=1000"},
|
||||
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_HASH_INFO,
|
||||
result: "hash-info output",
|
||||
check: func(t *testing.T, command *clientpb.CrackCommand) {
|
||||
if command.HashInfoLevel != 2 || command.HashMode == nil || *command.HashMode != 1000 {
|
||||
t.Fatalf("hash-info request = %#v", command)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
capture := &crackDispatchRPCCapture{crackResponse: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: test.resultMode, Value: test.result,
|
||||
}}}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
CrackCmd(newCrackDispatchCommand(t, test.flags...), con, test.args)
|
||||
if capture.crackCalls != 1 || capture.crackstationsCalls != 0 {
|
||||
t.Fatalf("RPC calls: Crack=%d Crackstations=%d", capture.crackCalls, capture.crackstationsCalls)
|
||||
}
|
||||
if capture.crackRequest == nil {
|
||||
t.Fatal("Crack request was not captured")
|
||||
}
|
||||
test.check(t, capture.crackRequest)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalCrackKeyspace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
response *clientpb.CrackResponse
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "canonical", response: &clientpb.CrackResponse{Keyspace: "100"}, want: "100"},
|
||||
{name: "normalizes", response: &clientpb.CrackResponse{Keyspace: " 00100 "}, want: "100"},
|
||||
{name: "zero", response: &clientpb.CrackResponse{Keyspace: "0"}, want: "0"},
|
||||
{name: "missing response", wantErr: true},
|
||||
{name: "missing value", response: &clientpb.CrackResponse{}, wantErr: true},
|
||||
{name: "negative", response: &clientpb.CrackResponse{Keyspace: "-1"}, wantErr: true},
|
||||
{name: "overflow", response: &clientpb.CrackResponse{Keyspace: "18446744073709551616"}, wantErr: true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := canonicalCrackKeyspace(test.response)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("canonicalCrackKeyspace() error = %v, wantErr %v", err, test.wantErr)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Fatalf("canonicalCrackKeyspace() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackQueryResponseValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
invocation crackInvocation
|
||||
response *clientpb.CrackResponse
|
||||
want string
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "typed keyspace",
|
||||
invocation: crackInvocation{mode: crackInvocationKeyspace},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE, Value: " 00100 ",
|
||||
}},
|
||||
want: "100",
|
||||
},
|
||||
{
|
||||
name: "typed total candidates",
|
||||
invocation: crackInvocation{mode: crackInvocationTotalCandidates},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_TOTAL_CANDIDATES, Value: "42\n",
|
||||
}},
|
||||
want: "42",
|
||||
},
|
||||
{
|
||||
name: "text remains verbatim",
|
||||
invocation: crackInvocation{mode: crackInvocationLookup},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP, Value: "line one\nline two\n",
|
||||
}},
|
||||
want: "line one\nline two\n",
|
||||
},
|
||||
{
|
||||
name: "wrong typed mode",
|
||||
invocation: crackInvocation{mode: crackInvocationLookup},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_IDENTIFY, Value: "output",
|
||||
}},
|
||||
wantError: "for lookup query",
|
||||
},
|
||||
{
|
||||
name: "new mode cannot use legacy keyspace",
|
||||
invocation: crackInvocation{mode: crackInvocationTotalCandidates},
|
||||
response: &clientpb.CrackResponse{Keyspace: "42"},
|
||||
wantError: "empty total-candidates query result",
|
||||
},
|
||||
{
|
||||
name: "selected keyspace cannot use legacy response",
|
||||
invocation: crackInvocation{mode: crackInvocationKeyspace, crackstation: "station-one"},
|
||||
response: &clientpb.CrackResponse{Keyspace: "42"},
|
||||
wantError: "empty keyspace query result",
|
||||
},
|
||||
{
|
||||
name: "empty text",
|
||||
invocation: crackInvocation{mode: crackInvocationHashInfo},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_HASH_INFO, Value: " \n",
|
||||
}},
|
||||
wantError: "empty hash-info query result",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := crackQueryResponseValue(test.response, test.invocation)
|
||||
if test.wantError != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantError) {
|
||||
t.Fatalf("crackQueryResponseValue() error = %v, want substring %q", err, test.wantError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Fatalf("crackQueryResponseValue() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackQueryResponsePreservesStderr(t *testing.T) {
|
||||
response := &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP,
|
||||
Value: "lookup output",
|
||||
Stderr: "hashcat warning\n",
|
||||
}}
|
||||
value, stderr, err := crackQueryResponse(response, crackInvocation{mode: crackInvocationLookup})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value != "lookup output" || stderr != "hashcat warning\n" {
|
||||
t.Fatalf("crackQueryResponse() = (%q, %q), want preserved value and stderr", value, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackResponseKeyspaceFieldNumberAndRoundTrip(t *testing.T) {
|
||||
field := (&clientpb.CrackResponse{}).ProtoReflect().Descriptor().Fields().ByName("Keyspace")
|
||||
if field == nil {
|
||||
t.Fatal("CrackResponse is missing field Keyspace")
|
||||
}
|
||||
if got, want := field.Number(), protoreflect.FieldNumber(2); got != want {
|
||||
t.Fatalf("CrackResponse.Keyspace tag = %d, want %d", got, want)
|
||||
}
|
||||
queryField := (&clientpb.CrackResponse{}).ProtoReflect().Descriptor().Fields().ByName("Query")
|
||||
if queryField == nil {
|
||||
t.Fatal("CrackResponse is missing field Query")
|
||||
}
|
||||
if got, want := queryField.Number(), protoreflect.FieldNumber(3); got != want {
|
||||
t.Fatalf("CrackResponse.Query tag = %d, want %d", got, want)
|
||||
}
|
||||
stderrField := (&clientpb.CrackQueryResult{}).ProtoReflect().Descriptor().Fields().ByName("Stderr")
|
||||
if stderrField == nil {
|
||||
t.Fatal("CrackQueryResult is missing field Stderr")
|
||||
}
|
||||
if got, want := stderrField.Number(), protoreflect.FieldNumber(6); got != want {
|
||||
t.Fatalf("CrackQueryResult.Stderr tag = %d, want %d", got, want)
|
||||
}
|
||||
|
||||
want := &clientpb.CrackResponse{
|
||||
Job: &clientpb.CrackJob{ID: "job-id"},
|
||||
Keyspace: "18446744073709551615",
|
||||
Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE, CrackstationHostUUID: "host-id", Value: "42", Stderr: "warning\n",
|
||||
},
|
||||
}
|
||||
raw, err := proto.Marshal(want)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal crack response: %v", err)
|
||||
}
|
||||
got := &clientpb.CrackResponse{}
|
||||
if err := proto.Unmarshal(raw, got); err != nil {
|
||||
t.Fatalf("unmarshal crack response: %v", err)
|
||||
}
|
||||
if !proto.Equal(got, want) {
|
||||
t.Fatalf("crack response round trip = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectedCrackstationsReturnsRPCError(t *testing.T) {
|
||||
want := errors.New("query failed")
|
||||
capture := &crackDispatchRPCCapture{err: want}
|
||||
if _, err := connectedCrackstations(context.Background(), capture); !errors.Is(err, want) {
|
||||
t.Fatalf("connectedCrackstations() error = %v, want %v", err, want)
|
||||
}
|
||||
if capture.crackstationsCalls != 1 {
|
||||
t.Fatalf("Crackstations calls = %d, want 1", capture.crackstationsCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectedCrackstationsReturnsEveryNonNilStation(t *testing.T) {
|
||||
capture := &crackDispatchRPCCapture{response: &clientpb.Crackstations{Crackstations: []*clientpb.Crackstation{
|
||||
nil,
|
||||
{Name: "one"},
|
||||
{Name: "two"},
|
||||
}}}
|
||||
stations, err := connectedCrackstations(context.Background(), capture)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(stations) != 2 || stations[0].Name != "one" || stations[1].Name != "two" {
|
||||
t.Fatalf("connectedCrackstations() = %#v, want one and two", stations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackHashcatFlagsAreRootLocal(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
if root.Flags().Lookup("backend-info") == nil {
|
||||
t.Fatal("root crack command is missing --backend-info")
|
||||
}
|
||||
if root.Flags().Lookup("crackstation") == nil {
|
||||
t.Fatal("root crack command is missing --crackstation")
|
||||
}
|
||||
if root.PersistentFlags().Lookup("backend-info") != nil {
|
||||
t.Fatal("--backend-info must not be inherited by crack subcommands")
|
||||
}
|
||||
if root.Flags().Lookup("stdout") != nil {
|
||||
t.Fatal("root crack command unexpectedly exposes removed --stdout")
|
||||
}
|
||||
|
||||
stations, _, err := root.Find([]string{"stations"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack stations: %v", err)
|
||||
}
|
||||
if stations.InheritedFlags().Lookup("backend-info") != nil {
|
||||
t.Fatal("crack stations unexpectedly inherits --backend-info")
|
||||
}
|
||||
if stations.InheritedFlags().Lookup("timeout") == nil {
|
||||
t.Fatal("crack stations must continue to inherit --timeout")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
const crackFileScheme = "crackfile://"
|
||||
|
||||
type crackFilesListFunc func(context.Context, *clientpb.CrackFile) (*clientpb.CrackFiles, error)
|
||||
|
||||
// resolveManagedCrackFileReferences replaces exact server-side crack file
|
||||
// names, IDs, and digests with the content-addressed URI understood by a
|
||||
// crackstation. Inputs that do not match a managed file remain ordinary
|
||||
// Hashcat operands, masks, paths, or inline rule content.
|
||||
//
|
||||
//nolint:gocyclo // Resolve every managed-file role together in one deterministic pass.
|
||||
func resolveManagedCrackFileReferences(ctx context.Context, command *clientpb.CrackCommand, list crackFilesListFunc) error {
|
||||
if command == nil {
|
||||
return fmt.Errorf("missing crack command")
|
||||
}
|
||||
if list == nil {
|
||||
return fmt.Errorf("missing crack file lister")
|
||||
}
|
||||
|
||||
wordlistIndexes := make([]int, 0, len(command.PositionalArguments))
|
||||
wordlistInputs := make([]string, 0, len(command.PositionalArguments))
|
||||
for index, argument := range command.PositionalArguments {
|
||||
// Only wordlist-role operands may be resolved by alias. An inline mask can
|
||||
// legitimately have the same text as a managed file name and must remain
|
||||
// an inline mask. Explicit crackfile URIs are still validated here so a
|
||||
// mistyped managed reference fails at the client boundary.
|
||||
if !crackOperandIsWordlist(command.AttackMode, index) && !strings.HasPrefix(argument, crackFileScheme) {
|
||||
continue
|
||||
}
|
||||
wordlistIndexes = append(wordlistIndexes, index)
|
||||
wordlistInputs = append(wordlistInputs, argument)
|
||||
}
|
||||
if len(wordlistInputs) > 0 {
|
||||
resolved, err := resolveCrackFileValues(ctx, wordlistInputs, clientpb.CrackFileType_WORDLIST, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for index, argument := range resolved {
|
||||
command.PositionalArguments[wordlistIndexes[index]] = argument
|
||||
}
|
||||
}
|
||||
legacyIdentify := command.Identify //nolint:staticcheck // Identify remains required for legacy wire compatibility.
|
||||
if len(command.PositionalArguments) == 0 && legacyIdentify != "" &&
|
||||
(crackOperandIsWordlist(command.AttackMode, 0) || strings.HasPrefix(legacyIdentify, crackFileScheme)) {
|
||||
resolved, err := resolveCrackFileValues(ctx, []string{legacyIdentify}, clientpb.CrackFileType_WORDLIST, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command.Identify = resolved[0] //nolint:staticcheck // Keep the resolved legacy operand synchronized for older servers.
|
||||
}
|
||||
|
||||
rules := command.RulesFilesV7
|
||||
if len(rules) == 0 && len(command.RulesFile) > 0 {
|
||||
rules = [][]byte{command.RulesFile}
|
||||
}
|
||||
if len(rules) > 0 {
|
||||
values := make([]string, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
values = append(values, string(rule))
|
||||
}
|
||||
resolved, err := resolveCrackFileValues(ctx, values, clientpb.CrackFileType_RULES, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command.RulesFilesV7 = make([][]byte, 0, len(resolved))
|
||||
for _, rule := range resolved {
|
||||
command.RulesFilesV7 = append(command.RulesFilesV7, []byte(rule))
|
||||
}
|
||||
command.RulesFile = append([]byte(nil), command.RulesFilesV7[0]...)
|
||||
}
|
||||
|
||||
if len(command.MarkovHcstat2) > 0 {
|
||||
resolved, err := resolveCrackFileValues(ctx, []string{string(command.MarkovHcstat2)}, clientpb.CrackFileType_MARKOV_HCSTAT2, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command.MarkovHcstat2 = []byte(resolved[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func crackOperandIsWordlist(attackMode clientpb.CrackAttackMode, index int) bool {
|
||||
switch attackMode {
|
||||
case clientpb.CrackAttackMode_STRAIGHT, clientpb.CrackAttackMode_COMBINATION:
|
||||
return true
|
||||
case clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK:
|
||||
return index == 0
|
||||
case clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST:
|
||||
return index == 1
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Validation and alias disambiguation intentionally share the manifest lookup and preserve input ordering.
|
||||
func resolveCrackFileValues(ctx context.Context, values []string, fileType clientpb.CrackFileType, list crackFilesListFunc) ([]string, error) {
|
||||
needsManifest := false
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, crackFileScheme) {
|
||||
if err := validateCrackFileURI(value, fileType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
needsManifest = true
|
||||
}
|
||||
if !needsManifest {
|
||||
return append([]string(nil), values...), nil
|
||||
}
|
||||
|
||||
manifest, err := list(ctx, &clientpb.CrackFile{Type: fileType})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list managed %s files: %w", crackFileTypeToken(fileType), err)
|
||||
}
|
||||
files := []*clientpb.CrackFile(nil)
|
||||
if manifest != nil {
|
||||
files = manifest.Files
|
||||
}
|
||||
resolved := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, crackFileScheme) {
|
||||
resolved = append(resolved, value)
|
||||
continue
|
||||
}
|
||||
matches := make([]*clientpb.CrackFile, 0, 1)
|
||||
for _, file := range files {
|
||||
if file == nil || file.Type != fileType {
|
||||
continue
|
||||
}
|
||||
if value == file.Name || value == file.ID || strings.EqualFold(value, file.Sha2_256) {
|
||||
matches = append(matches, file)
|
||||
}
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
return nil, fmt.Errorf("managed %s file reference %q is ambiguous", crackFileTypeToken(fileType), value)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
resolved = append(resolved, value)
|
||||
continue
|
||||
}
|
||||
uri, err := crackFileURI(matches[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved = append(resolved, uri)
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func crackFileURI(file *clientpb.CrackFile) (string, error) {
|
||||
if file == nil {
|
||||
return "", fmt.Errorf("missing managed crack file")
|
||||
}
|
||||
digest := strings.ToLower(strings.TrimSpace(file.Sha2_256))
|
||||
if err := validateCrackFileDigest(digest); err != nil {
|
||||
return "", fmt.Errorf("managed crack file %q has invalid SHA-256: %w", file.Name, err)
|
||||
}
|
||||
fileType := crackFileTypeToken(file.Type)
|
||||
if fileType == "" {
|
||||
return "", fmt.Errorf("managed crack file %q has unsupported type %s", file.Name, file.Type.String())
|
||||
}
|
||||
return crackFileScheme + fileType + "/" + digest, nil
|
||||
}
|
||||
|
||||
func validateCrackFileURI(value string, expectedType clientpb.CrackFileType) error {
|
||||
remainder := strings.TrimPrefix(value, crackFileScheme)
|
||||
parts := strings.Split(remainder, "/")
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return fmt.Errorf("invalid managed crack file URI %q", value)
|
||||
}
|
||||
if parts[0] != crackFileTypeToken(expectedType) {
|
||||
return fmt.Errorf("managed crack file URI %q has type %q, expected %q", value, parts[0], crackFileTypeToken(expectedType))
|
||||
}
|
||||
if parts[1] != strings.ToLower(parts[1]) {
|
||||
return fmt.Errorf("managed crack file URI %q must use a lowercase SHA-256", value)
|
||||
}
|
||||
if err := validateCrackFileDigest(parts[1]); err != nil {
|
||||
return fmt.Errorf("invalid managed crack file URI %q: %w", value, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCrackFileDigest(value string) error {
|
||||
if len(value) != 64 {
|
||||
return fmt.Errorf("SHA-256 must contain 64 hexadecimal characters")
|
||||
}
|
||||
decoded, err := hex.DecodeString(value)
|
||||
if err != nil || len(decoded) != 32 {
|
||||
return fmt.Errorf("SHA-256 must contain 64 hexadecimal characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func crackFileTypeToken(fileType clientpb.CrackFileType) string {
|
||||
switch fileType {
|
||||
case clientpb.CrackFileType_WORDLIST:
|
||||
return "wordlist"
|
||||
case clientpb.CrackFileType_RULES:
|
||||
return "rules"
|
||||
case clientpb.CrackFileType_MARKOV_HCSTAT2:
|
||||
return "hcstat2"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestResolveManagedCrackFileReferences(t *testing.T) {
|
||||
wordlistDigest := strings.Repeat("a", 64)
|
||||
rulesDigest := strings.Repeat("b", 64)
|
||||
hcstat2Digest := strings.Repeat("c", 64)
|
||||
files := map[clientpb.CrackFileType][]*clientpb.CrackFile{
|
||||
clientpb.CrackFileType_WORDLIST: {{ID: "wordlist-id", Name: "rockyou.txt", Sha2_256: wordlistDigest, Type: clientpb.CrackFileType_WORDLIST}},
|
||||
clientpb.CrackFileType_RULES: {{ID: "rules-id", Name: "best64.rule", Sha2_256: rulesDigest, Type: clientpb.CrackFileType_RULES}},
|
||||
clientpb.CrackFileType_MARKOV_HCSTAT2: {{
|
||||
ID: "hcstat2-id", Name: "hashcat.hcstat2", Sha2_256: hcstat2Digest, Type: clientpb.CrackFileType_MARKOV_HCSTAT2,
|
||||
}},
|
||||
}
|
||||
requested := make([]clientpb.CrackFileType, 0)
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
requested = append(requested, request.Type)
|
||||
return &clientpb.CrackFiles{Files: files[request.Type]}, nil
|
||||
}
|
||||
command := &clientpb.CrackCommand{
|
||||
AttackMode: clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK,
|
||||
PositionalArguments: []string{"rockyou.txt", "?d?d?d"},
|
||||
RulesFile: []byte("best64.rule"),
|
||||
RulesFilesV7: [][]byte{[]byte("best64.rule"), []byte(":")},
|
||||
MarkovHcstat2: []byte("hcstat2-id"),
|
||||
}
|
||||
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatalf("resolveManagedCrackFileReferences: %v", err)
|
||||
}
|
||||
if want := []string{"crackfile://wordlist/" + wordlistDigest, "?d?d?d"}; !slices.Equal(command.PositionalArguments, want) {
|
||||
t.Fatalf("PositionalArguments = %#v, want %#v", command.PositionalArguments, want)
|
||||
}
|
||||
wantRules := [][]byte{[]byte("crackfile://rules/" + rulesDigest), []byte(":")}
|
||||
if !slices.EqualFunc(command.RulesFilesV7, wantRules, func(left, right []byte) bool { return slices.Equal(left, right) }) {
|
||||
t.Fatalf("RulesFilesV7 = %#v, want %#v", command.RulesFilesV7, wantRules)
|
||||
}
|
||||
if !slices.Equal(command.RulesFile, wantRules[0]) {
|
||||
t.Fatalf("RulesFile = %q", command.RulesFile)
|
||||
}
|
||||
if want := "crackfile://hcstat2/" + hcstat2Digest; string(command.MarkovHcstat2) != want {
|
||||
t.Fatalf("MarkovHcstat2 = %q, want %q", command.MarkovHcstat2, want)
|
||||
}
|
||||
if want := []clientpb.CrackFileType{clientpb.CrackFileType_WORDLIST, clientpb.CrackFileType_RULES, clientpb.CrackFileType_MARKOV_HCSTAT2}; !slices.Equal(requested, want) {
|
||||
t.Fatalf("requested types = %#v, want %#v", requested, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRespectsAttackOperandRoles(t *testing.T) {
|
||||
digestA := strings.Repeat("a", 64)
|
||||
digestB := strings.Repeat("b", 64)
|
||||
files := []*clientpb.CrackFile{
|
||||
{ID: "wordlist-a", Name: "first", Sha2_256: digestA, Type: clientpb.CrackFileType_WORDLIST},
|
||||
{ID: "wordlist-b", Name: "second", Sha2_256: digestB, Type: clientpb.CrackFileType_WORDLIST},
|
||||
}
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
if request.Type != clientpb.CrackFileType_WORDLIST {
|
||||
t.Fatalf("requested file type = %s, want WORDLIST", request.Type)
|
||||
}
|
||||
return &clientpb.CrackFiles{Files: files}, nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
attackMode clientpb.CrackAttackMode
|
||||
inputs []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "bruteforce mask collision",
|
||||
attackMode: clientpb.CrackAttackMode_BRUTEFORCE,
|
||||
inputs: []string{"first"},
|
||||
want: []string{"first"},
|
||||
},
|
||||
{
|
||||
name: "wordlist mask hybrid",
|
||||
attackMode: clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"crackfile://wordlist/" + digestA, "second"},
|
||||
},
|
||||
{
|
||||
name: "mask wordlist hybrid",
|
||||
attackMode: clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"first", "crackfile://wordlist/" + digestB},
|
||||
},
|
||||
{
|
||||
name: "straight wordlists",
|
||||
attackMode: clientpb.CrackAttackMode_STRAIGHT,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"crackfile://wordlist/" + digestA, "crackfile://wordlist/" + digestB},
|
||||
},
|
||||
{
|
||||
name: "combination wordlists",
|
||||
attackMode: clientpb.CrackAttackMode_COMBINATION,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"crackfile://wordlist/" + digestA, "crackfile://wordlist/" + digestB},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{
|
||||
AttackMode: test.attackMode,
|
||||
PositionalArguments: append([]string(nil), test.inputs...),
|
||||
}
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatalf("resolveManagedCrackFileReferences: %v", err)
|
||||
}
|
||||
if !slices.Equal(command.PositionalArguments, test.want) {
|
||||
t.Fatalf("PositionalArguments = %#v, want %#v", command.PositionalArguments, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesPreservesInlineBytes(t *testing.T) {
|
||||
inline := []byte{0, 1, 2, 0xff}
|
||||
command := &clientpb.CrackCommand{RulesFilesV7: [][]byte{append([]byte(nil), inline...)}}
|
||||
list := func(_ context.Context, _ *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return &clientpb.CrackFiles{}, nil
|
||||
}
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(command.RulesFilesV7) != 1 || !slices.Equal(command.RulesFilesV7[0], inline) {
|
||||
t.Fatalf("inline rules changed: %#v", command.RulesFilesV7)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesAcceptsCanonicalURIWithoutListing(t *testing.T) {
|
||||
uri := "crackfile://wordlist/" + strings.Repeat("d", 64)
|
||||
command := &clientpb.CrackCommand{PositionalArguments: []string{uri}}
|
||||
list := func(_ context.Context, _ *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
t.Fatal("canonical URI should not require a manifest lookup")
|
||||
return nil, nil
|
||||
}
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if command.PositionalArguments[0] != uri {
|
||||
t.Fatalf("URI changed to %q", command.PositionalArguments[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRejectsWrongURIType(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{RulesFilesV7: [][]byte{[]byte("crackfile://wordlist/" + strings.Repeat("e", 64))}}
|
||||
err := resolveManagedCrackFileReferences(context.Background(), command, func(context.Context, *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return nil, errors.New("must not be called")
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "expected \"rules\"") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRejectsAmbiguousName(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{PositionalArguments: []string{"duplicate.txt"}}
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return &clientpb.CrackFiles{Files: []*clientpb.CrackFile{
|
||||
{Name: "duplicate.txt", Sha2_256: strings.Repeat("a", 64), Type: request.Type},
|
||||
{Name: "duplicate.txt", Sha2_256: strings.Repeat("b", 64), Type: request.Type},
|
||||
}}, nil
|
||||
}
|
||||
err := resolveManagedCrackFileReferences(context.Background(), command, list)
|
||||
if err == nil || !strings.Contains(err.Error(), "ambiguous") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRejectsInvalidDigest(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{MarkovHcstat2: []byte("broken.hcstat2")}
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return &clientpb.CrackFiles{Files: []*clientpb.CrackFile{{
|
||||
Name: "broken.hcstat2", Sha2_256: "not-a-digest", Type: request.Type,
|
||||
}}}, nil
|
||||
}
|
||||
err := resolveManagedCrackFileReferences(context.Background(), command, list)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid SHA-256") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -22,18 +22,36 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func localCrackFileInfo(path string) (os.FileInfo, error) {
|
||||
return localCrackFileInfoWith(path, os.Stat)
|
||||
}
|
||||
|
||||
func localCrackFileInfoWith(path string, stat func(string) (os.FileInfo, error)) (os.FileInfo, error) {
|
||||
info, err := stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat local crack file %q: %w", path, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("local crack file %q is a directory", path)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// CrackWordlistsCmd - Manage GPU cracking stations.
|
||||
func CrackWordlistsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
wordlists, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_WORDLIST})
|
||||
@@ -173,9 +191,9 @@ func CrackWordlistsAddCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
con.PrintErrorf("No path specified, see --help\n")
|
||||
return
|
||||
}
|
||||
wordlistStat, err := os.Stat(localPath)
|
||||
if os.IsNotExist(err) || wordlistStat.IsDir() {
|
||||
con.PrintErrorf("File does not exist: %s\n", localPath)
|
||||
wordlistStat, err := localCrackFileInfo(localPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid wordlist file: %s\n", err)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
@@ -217,9 +235,9 @@ func CrackRulesAddCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
con.PrintErrorf("No path specified, see --help\n")
|
||||
return
|
||||
}
|
||||
rulesStat, err := os.Stat(localPath)
|
||||
if os.IsNotExist(err) || rulesStat.IsDir() {
|
||||
con.PrintErrorf("File does not exist: %s\n", localPath)
|
||||
rulesStat, err := localCrackFileInfo(localPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid rules file: %s\n", err)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
@@ -261,9 +279,9 @@ func CrackHcstat2AddCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
con.PrintErrorf("No path specified, see --help\n")
|
||||
return
|
||||
}
|
||||
hcstat2Stat, err := os.Stat(localPath)
|
||||
if os.IsNotExist(err) || hcstat2Stat.IsDir() {
|
||||
con.PrintErrorf("File does not exist: %s\n", localPath)
|
||||
hcstat2Stat, err := localCrackFileInfo(localPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid hcstat2 file: %s\n", err)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
@@ -294,102 +312,176 @@ func CrackHcstat2AddCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
}
|
||||
|
||||
func addCrackFile(localFile *os.File, crackFile *clientpb.CrackFile, con *console.SliverClient) {
|
||||
digest := sha256.New()
|
||||
wordlistReader := io.TeeReader(localFile, digest)
|
||||
|
||||
chunks := make(chan []byte, 1) // Chunks are in-memory!
|
||||
|
||||
var chunkReaderErr error
|
||||
go func() {
|
||||
chunkReaderErr = chunkReader(wordlistReader, crackFile.ChunkSize, chunks)
|
||||
}()
|
||||
n := uint32(0)
|
||||
total := int64(0)
|
||||
errors := []error{}
|
||||
for chunk := range chunks {
|
||||
total += int64(len(chunk))
|
||||
total, err := uploadCrackFile(localFile, crackFile, con.Rpc, func(total int64, n uint32, chunkSize int) {
|
||||
con.PrintInfof("Uploading %s (chunk %d - %s) ...",
|
||||
util.ByteCountBinary(total),
|
||||
n,
|
||||
util.ByteCountBinary(int64(len(chunk))),
|
||||
util.ByteCountBinary(int64(chunkSize)),
|
||||
)
|
||||
_, err := con.Rpc.CrackFileChunkUpload(context.Background(), &clientpb.CrackFileChunk{
|
||||
CrackFileID: crackFile.ID,
|
||||
N: n,
|
||||
Data: chunk,
|
||||
})
|
||||
n++
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
con.Printf(console.Clearln + "\r")
|
||||
if chunkReaderErr != nil {
|
||||
con.PrintErrorf("Failed to read file: %s\n", chunkReaderErr)
|
||||
return
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
for _, err := range errors {
|
||||
con.PrintErrorf("Failed to upload chunk: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
_, err := con.Rpc.CrackFileComplete(context.Background(), &clientpb.CrackFile{
|
||||
ID: crackFile.ID,
|
||||
Sha2_256: hex.EncodeToString(digest.Sum(nil)),
|
||||
})
|
||||
con.Printf(console.Clearln + "\r")
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to complete file upload: %s\n", err)
|
||||
con.PrintErrorf("Failed to upload crack file: %s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Upload completed (compressed: %s)\n", util.ByteCountBinary(total))
|
||||
}
|
||||
|
||||
func chunkReader(wordlistReader io.Reader, chunkSize int64, chunks chan []byte) error {
|
||||
func uploadCrackFile(localFile io.Reader, crackFile *clientpb.CrackFile, rpc rpcpb.SliverRPCClient, progress func(total int64, n uint32, chunkSize int)) (total int64, retErr error) {
|
||||
if localFile == nil {
|
||||
return 0, errors.New("local crack file reader is nil")
|
||||
}
|
||||
if crackFile == nil || crackFile.ID == "" {
|
||||
return 0, errors.New("crack file reservation is missing an ID")
|
||||
}
|
||||
if rpc == nil {
|
||||
return 0, errors.New("crack file RPC client is nil")
|
||||
}
|
||||
|
||||
completed := false
|
||||
defer func() {
|
||||
if retErr == nil || completed {
|
||||
return
|
||||
}
|
||||
_, cleanupErr := rpc.CrackFileDelete(context.Background(), &clientpb.CrackFile{ID: crackFile.ID})
|
||||
if cleanupErr != nil {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("failed to remove crack file reservation %s: %w", crackFile.ID, cleanupErr))
|
||||
}
|
||||
}()
|
||||
|
||||
digest := sha256.New()
|
||||
wordlistReader := io.TeeReader(localFile, digest)
|
||||
|
||||
chunks, chunkReaderErrors := readCrackFileChunks(wordlistReader, crackFile.ChunkSize)
|
||||
n := uint32(0)
|
||||
var uploadErr error
|
||||
for chunk := range chunks {
|
||||
total += int64(len(chunk))
|
||||
if uploadErr == nil {
|
||||
if progress != nil {
|
||||
progress(total, n, len(chunk))
|
||||
}
|
||||
_, err := rpc.CrackFileChunkUpload(context.Background(), &clientpb.CrackFileChunk{
|
||||
CrackFileID: crackFile.ID,
|
||||
N: n,
|
||||
Data: chunk,
|
||||
})
|
||||
if err != nil {
|
||||
uploadErr = fmt.Errorf("upload chunk %d: %w", n, err)
|
||||
}
|
||||
}
|
||||
n++
|
||||
}
|
||||
chunkReaderErr := <-chunkReaderErrors
|
||||
if chunkReaderErr != nil {
|
||||
uploadErr = errors.Join(uploadErr, fmt.Errorf("read and compress crack file: %w", chunkReaderErr))
|
||||
}
|
||||
if uploadErr != nil {
|
||||
return total, uploadErr
|
||||
}
|
||||
_, err := rpc.CrackFileComplete(context.Background(), &clientpb.CrackFile{
|
||||
ID: crackFile.ID,
|
||||
Sha2_256: hex.EncodeToString(digest.Sum(nil)),
|
||||
})
|
||||
if err != nil {
|
||||
return total, fmt.Errorf("complete crack file upload: %w", err)
|
||||
}
|
||||
completed = true
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func readCrackFileChunks(reader io.Reader, chunkSize int64) (<-chan []byte, <-chan error) {
|
||||
chunks := make(chan []byte, 1) // Chunks are in-memory!
|
||||
errors := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(errors)
|
||||
errors <- chunkReader(reader, chunkSize, chunks)
|
||||
}()
|
||||
return chunks, errors
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Compression, fixed-size emission, final remainder handling, and cleanup form one streaming state machine.
|
||||
func chunkReader(wordlistReader io.Reader, chunkSize int64, chunks chan []byte) (retErr error) {
|
||||
defer close(chunks)
|
||||
if chunkSize < 1 {
|
||||
return fmt.Errorf("invalid crack file chunk size %d", chunkSize)
|
||||
}
|
||||
start := int64(0)
|
||||
tmpFile, err := os.CreateTemp("", "sliver-wordlist")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
compressor, _ := zstd.NewWriter(tmpFile, zstd.WithEncoderLevel(zstd.SpeedBetterCompression))
|
||||
for {
|
||||
readBuf := make([]byte, 32*1024*1024) // 32MB
|
||||
readN, readErr := wordlistReader.Read(readBuf)
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
return err
|
||||
tmpPath := tmpFile.Name()
|
||||
defer func() {
|
||||
if err := tmpFile.Close(); retErr == nil && err != nil {
|
||||
retErr = err
|
||||
}
|
||||
_ = os.Remove(tmpPath)
|
||||
}()
|
||||
compressor, err := zstd.NewWriter(tmpFile, zstd.WithEncoderLevel(zstd.SpeedBetterCompression))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compressorClosed := false
|
||||
defer func() {
|
||||
if !compressorClosed {
|
||||
_ = compressor.Close()
|
||||
}
|
||||
}()
|
||||
readBuf := make([]byte, 32*1024*1024) // 32MB
|
||||
for {
|
||||
readN, readErr := wordlistReader.Read(readBuf)
|
||||
if readN != 0 {
|
||||
_, err := compressor.Write(readBuf[:readN])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compressor.Flush()
|
||||
tmpFileStat, err := os.Stat(tmpFile.Name())
|
||||
if err := compressor.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
tmpFileStat, err := tmpFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tmpFileStat.Size()-start >= chunkSize {
|
||||
stop := start + chunkSize
|
||||
for tmpFileStat.Size()-start >= chunkSize {
|
||||
chunk, err := readChunkAt(tmpFile, start, chunkSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chunks <- chunk
|
||||
start = stop
|
||||
start += int64(len(chunk))
|
||||
}
|
||||
}
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
return readErr
|
||||
}
|
||||
if readErr == io.EOF {
|
||||
chunk, err := readChunkAt(tmpFile, start, chunkSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chunks <- chunk
|
||||
return nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := compressor.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
compressorClosed = true
|
||||
tmpFileStat, err := tmpFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for start < tmpFileStat.Size() {
|
||||
remaining := tmpFileStat.Size() - start
|
||||
readSize := chunkSize
|
||||
if remaining < readSize {
|
||||
readSize = remaining
|
||||
}
|
||||
chunk, err := readChunkAt(tmpFile, start, readSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chunks <- chunk
|
||||
start += int64(len(chunk))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readChunkAt(tmpFile *os.File, offset int64, chunkSize int64) ([]byte, error) {
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestLocalCrackFileInfoHandlesStatErrors(t *testing.T) {
|
||||
wantErr := errors.New("permission denied")
|
||||
info, err := localCrackFileInfoWith("/restricted/file", func(string) (os.FileInfo, error) {
|
||||
return nil, wantErr
|
||||
})
|
||||
if info != nil {
|
||||
t.Fatalf("localCrackFileInfoWith info = %#v, want nil", info)
|
||||
}
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("localCrackFileInfoWith error = %v, want wrapped %v", err, wantErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "/restricted/file") {
|
||||
t.Fatalf("localCrackFileInfoWith error = %q, want path context", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalCrackFileInfoRejectsDirectories(t *testing.T) {
|
||||
_, err := localCrackFileInfo(t.TempDir())
|
||||
if err == nil || !strings.Contains(err.Error(), "is a directory") {
|
||||
t.Fatalf("localCrackFileInfo directory error = %v, want directory error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalCrackFileInfoAcceptsRegularFile(t *testing.T) {
|
||||
file, err := os.CreateTemp(t.TempDir(), "crack-file-")
|
||||
if err != nil {
|
||||
t.Fatalf("create local crack file: %v", err)
|
||||
}
|
||||
const payload = "hashcat input"
|
||||
if _, err := file.WriteString(payload); err != nil {
|
||||
t.Fatalf("write local crack file: %v", err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatalf("close local crack file: %v", err)
|
||||
}
|
||||
|
||||
info, err := localCrackFileInfo(file.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("localCrackFileInfo: %v", err)
|
||||
}
|
||||
if info.Size() != int64(len(payload)) {
|
||||
t.Fatalf("localCrackFileInfo size = %d, want %d", info.Size(), len(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadCrackFileCleansUpPostCreateFailures(t *testing.T) {
|
||||
readErr := errors.New("read failed")
|
||||
chunkErr := errors.New("chunk upload failed")
|
||||
completeErr := errors.New("complete failed")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reader io.Reader
|
||||
configureRPC func(*fakeCrackFileUploadRPC)
|
||||
wantErr error
|
||||
wantChunkCalls int
|
||||
wantCompleteCalls int
|
||||
}{
|
||||
{
|
||||
name: "read failure",
|
||||
reader: errorReader{err: readErr},
|
||||
wantErr: readErr,
|
||||
},
|
||||
{
|
||||
name: "chunk upload failure",
|
||||
reader: bytes.NewReader([]byte("payload")),
|
||||
configureRPC: func(rpc *fakeCrackFileUploadRPC) {
|
||||
rpc.chunkErr = chunkErr
|
||||
},
|
||||
wantErr: chunkErr,
|
||||
wantChunkCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "completion failure",
|
||||
reader: bytes.NewReader([]byte("payload")),
|
||||
configureRPC: func(rpc *fakeCrackFileUploadRPC) {
|
||||
rpc.completeErr = completeErr
|
||||
},
|
||||
wantErr: completeErr,
|
||||
wantChunkCalls: 1,
|
||||
wantCompleteCalls: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rpc := &fakeCrackFileUploadRPC{}
|
||||
if test.configureRPC != nil {
|
||||
test.configureRPC(rpc)
|
||||
}
|
||||
reservation := &clientpb.CrackFile{ID: "reserved-file", ChunkSize: 1024}
|
||||
|
||||
_, err := uploadCrackFile(test.reader, reservation, rpc, nil)
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Fatalf("uploadCrackFile error = %v, want wrapped %v", err, test.wantErr)
|
||||
}
|
||||
if rpc.chunkCalls != test.wantChunkCalls {
|
||||
t.Fatalf("CrackFileChunkUpload calls = %d, want %d", rpc.chunkCalls, test.wantChunkCalls)
|
||||
}
|
||||
if rpc.completeCalls != test.wantCompleteCalls {
|
||||
t.Fatalf("CrackFileComplete calls = %d, want %d", rpc.completeCalls, test.wantCompleteCalls)
|
||||
}
|
||||
if rpc.deleteCalls != 1 {
|
||||
t.Fatalf("CrackFileDelete calls = %d, want 1", rpc.deleteCalls)
|
||||
}
|
||||
if rpc.deleted == nil || rpc.deleted.ID != reservation.ID {
|
||||
t.Fatalf("CrackFileDelete request = %#v, want ID %q", rpc.deleted, reservation.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadCrackFileReportsCleanupFailure(t *testing.T) {
|
||||
uploadErr := errors.New("chunk upload failed")
|
||||
cleanupErr := errors.New("delete failed")
|
||||
rpc := &fakeCrackFileUploadRPC{chunkErr: uploadErr, deleteErr: cleanupErr}
|
||||
reservation := &clientpb.CrackFile{ID: "reserved-file", ChunkSize: 1024}
|
||||
|
||||
_, err := uploadCrackFile(bytes.NewReader([]byte("payload")), reservation, rpc, nil)
|
||||
if !errors.Is(err, uploadErr) {
|
||||
t.Fatalf("uploadCrackFile error = %v, want wrapped upload error %v", err, uploadErr)
|
||||
}
|
||||
if !errors.Is(err, cleanupErr) {
|
||||
t.Fatalf("uploadCrackFile error = %v, want wrapped cleanup error %v", err, cleanupErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), reservation.ID) {
|
||||
t.Fatalf("uploadCrackFile error = %q, want reservation ID", err)
|
||||
}
|
||||
if rpc.deleteCalls != 1 {
|
||||
t.Fatalf("CrackFileDelete calls = %d, want 1", rpc.deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadCrackFileSuccessCompletesWithoutCleanup(t *testing.T) {
|
||||
payload := []byte("correct horse battery staple\n")
|
||||
rpc := &fakeCrackFileUploadRPC{}
|
||||
reservation := &clientpb.CrackFile{ID: "reserved-file", ChunkSize: 8}
|
||||
progressCalls := 0
|
||||
|
||||
total, err := uploadCrackFile(bytes.NewReader(payload), reservation, rpc, func(int64, uint32, int) {
|
||||
progressCalls++
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("uploadCrackFile: %v", err)
|
||||
}
|
||||
if total == 0 {
|
||||
t.Fatal("uploadCrackFile compressed byte count = 0, want non-zero")
|
||||
}
|
||||
if rpc.chunkCalls == 0 {
|
||||
t.Fatal("CrackFileChunkUpload calls = 0, want at least 1")
|
||||
}
|
||||
if progressCalls != rpc.chunkCalls {
|
||||
t.Fatalf("progress calls = %d, want %d", progressCalls, rpc.chunkCalls)
|
||||
}
|
||||
if rpc.completeCalls != 1 {
|
||||
t.Fatalf("CrackFileComplete calls = %d, want 1", rpc.completeCalls)
|
||||
}
|
||||
if rpc.deleteCalls != 0 {
|
||||
t.Fatalf("CrackFileDelete calls = %d, want 0", rpc.deleteCalls)
|
||||
}
|
||||
wantDigest := sha256.Sum256(payload)
|
||||
if rpc.completed == nil || rpc.completed.ID != reservation.ID {
|
||||
t.Fatalf("CrackFileComplete request = %#v, want ID %q", rpc.completed, reservation.ID)
|
||||
}
|
||||
if rpc.completed.Sha2_256 != hex.EncodeToString(wantDigest[:]) {
|
||||
t.Fatalf("CrackFileComplete digest = %q, want %q", rpc.completed.Sha2_256, hex.EncodeToString(wantDigest[:]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderZstdRoundTrip(t *testing.T) {
|
||||
largePayload := make([]byte, 256*1024)
|
||||
if _, err := rand.New(rand.NewSource(42)).Read(largePayload); err != nil {
|
||||
t.Fatalf("create test payload: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
chunkSize int64
|
||||
wantMinChunks int
|
||||
}{
|
||||
{
|
||||
name: "single chunk",
|
||||
payload: []byte("correct horse battery staple\n"),
|
||||
chunkSize: 1024,
|
||||
wantMinChunks: 1,
|
||||
},
|
||||
{
|
||||
name: "many chunks from one read",
|
||||
payload: largePayload,
|
||||
chunkSize: 1024,
|
||||
wantMinChunks: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
compressed, chunkCount, err := collectCompressedCrackFile(bytes.NewReader(test.payload), test.chunkSize)
|
||||
if err != nil {
|
||||
t.Fatalf("chunkReader: %v", err)
|
||||
}
|
||||
if chunkCount < test.wantMinChunks {
|
||||
t.Fatalf("chunk count = %d, want at least %d", chunkCount, test.wantMinChunks)
|
||||
}
|
||||
|
||||
decoder, err := zstd.NewReader(bytes.NewReader(compressed))
|
||||
if err != nil {
|
||||
t.Fatalf("open zstd stream: %v", err)
|
||||
}
|
||||
decoded, err := io.ReadAll(decoder)
|
||||
decoder.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("decode zstd stream: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decoded, test.payload) {
|
||||
t.Fatalf("decoded payload does not match input: got %d bytes, want %d", len(decoded), len(test.payload))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderHandlesDataWithEOF(t *testing.T) {
|
||||
payload := []byte("reader returns data and EOF together")
|
||||
compressed, _, err := collectCompressedCrackFile(&dataEOFReader{data: payload}, 8)
|
||||
if err != nil {
|
||||
t.Fatalf("chunkReader: %v", err)
|
||||
}
|
||||
decoder, err := zstd.NewReader(bytes.NewReader(compressed))
|
||||
if err != nil {
|
||||
t.Fatalf("open zstd stream: %v", err)
|
||||
}
|
||||
decoded, err := io.ReadAll(decoder)
|
||||
decoder.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("decode zstd stream: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decoded, payload) {
|
||||
t.Fatalf("decoded payload = %q, want %q", decoded, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderRejectsInvalidChunkSize(t *testing.T) {
|
||||
for _, chunkSize := range []int64{0, -1} {
|
||||
compressed, chunkCount, err := collectCompressedCrackFile(bytes.NewReader([]byte("payload")), chunkSize)
|
||||
if err == nil {
|
||||
t.Fatalf("chunkReader accepted chunk size %d", chunkSize)
|
||||
}
|
||||
if chunkCount != 0 || len(compressed) != 0 {
|
||||
t.Fatalf("invalid chunk size %d emitted %d chunks (%d bytes)", chunkSize, chunkCount, len(compressed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderPropagatesReadError(t *testing.T) {
|
||||
wantErr := errors.New("read failed")
|
||||
compressed, chunkCount, err := collectCompressedCrackFile(errorReader{err: wantErr}, 1024)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("chunkReader error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if chunkCount != 0 || len(compressed) != 0 {
|
||||
t.Fatalf("failed read emitted %d chunks (%d bytes)", chunkCount, len(compressed))
|
||||
}
|
||||
}
|
||||
|
||||
func collectCompressedCrackFile(reader io.Reader, chunkSize int64) ([]byte, int, error) {
|
||||
chunks, errCh := readCrackFileChunks(reader, chunkSize)
|
||||
|
||||
compressed := bytes.Buffer{}
|
||||
chunkCount := 0
|
||||
invalidChunk := false
|
||||
for chunk := range chunks {
|
||||
if len(chunk) == 0 || int64(len(chunk)) > chunkSize {
|
||||
invalidChunk = true
|
||||
}
|
||||
_, _ = compressed.Write(chunk)
|
||||
chunkCount++
|
||||
}
|
||||
readerErr := <-errCh
|
||||
if invalidChunk {
|
||||
return nil, chunkCount, errors.New("chunkReader emitted an invalid chunk size")
|
||||
}
|
||||
return compressed.Bytes(), chunkCount, readerErr
|
||||
}
|
||||
|
||||
type dataEOFReader struct {
|
||||
data []byte
|
||||
done bool
|
||||
}
|
||||
|
||||
func (r *dataEOFReader) Read(buffer []byte) (int, error) {
|
||||
if r.done {
|
||||
return 0, io.EOF
|
||||
}
|
||||
r.done = true
|
||||
return copy(buffer, r.data), io.EOF
|
||||
}
|
||||
|
||||
type errorReader struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (r errorReader) Read([]byte) (int, error) {
|
||||
return 0, r.err
|
||||
}
|
||||
|
||||
type fakeCrackFileUploadRPC struct {
|
||||
rpcpb.SliverRPCClient
|
||||
|
||||
chunkErr error
|
||||
completeErr error
|
||||
deleteErr error
|
||||
|
||||
chunkCalls int
|
||||
completeCalls int
|
||||
deleteCalls int
|
||||
|
||||
uploaded []*clientpb.CrackFileChunk
|
||||
completed *clientpb.CrackFile
|
||||
deleted *clientpb.CrackFile
|
||||
}
|
||||
|
||||
func (f *fakeCrackFileUploadRPC) CrackFileChunkUpload(_ context.Context, request *clientpb.CrackFileChunk, _ ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
f.chunkCalls++
|
||||
f.uploaded = append(f.uploaded, &clientpb.CrackFileChunk{
|
||||
CrackFileID: request.CrackFileID,
|
||||
N: request.N,
|
||||
Data: append([]byte(nil), request.Data...),
|
||||
})
|
||||
if f.chunkErr != nil {
|
||||
return nil, f.chunkErr
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeCrackFileUploadRPC) CrackFileComplete(_ context.Context, request *clientpb.CrackFile, _ ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
f.completeCalls++
|
||||
f.completed = &clientpb.CrackFile{ID: request.ID, Sha2_256: request.Sha2_256}
|
||||
if f.completeErr != nil {
|
||||
return nil, f.completeErr
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeCrackFileUploadRPC) CrackFileDelete(_ context.Context, request *clientpb.CrackFile, _ ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
f.deleteCalls++
|
||||
f.deleted = &clientpb.CrackFile{ID: request.ID}
|
||||
if f.deleteErr != nil {
|
||||
return nil, f.deleteErr
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
@@ -3,15 +3,22 @@ package crack
|
||||
import "github.com/spf13/pflag"
|
||||
|
||||
func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.String("attack-mode", "", "crack attack mode (straight, combination, bruteforce, hybrid-wordlist-mask, hybrid-mask-wordlist, association, no-attack, or numeric)")
|
||||
f.String("hash-type", "", "hashcat hash type (number or enum name)")
|
||||
f.StringSlice("hash", nil, "hashes to crack (repeatable)")
|
||||
f.StringP("attack-mode", "a", "", "crack attack mode (straight, combination, bruteforce, pcfg, hybrid-wordlist-mask, hybrid-mask-wordlist, generic, association, no-attack, hybrid, or numeric)")
|
||||
f.StringP("hash-type", "m", "", "hashcat hash type (number or enum name)")
|
||||
f.Uint32("hash-mode", 0, "canonical numeric hashcat mode (overrides hash-type)")
|
||||
f.StringArray("hash", nil, "hashes to crack (repeatable)")
|
||||
f.StringArray("credential", nil, "server credential ID or ID prefix to crack (repeatable)")
|
||||
f.String("credential-collection", "", "server credential collection to crack")
|
||||
f.Bool("include-cracked-credentials", false, "include credentials already marked as cracked")
|
||||
f.StringArray("input", nil, "ordered positional hashcat inputs (repeatable)")
|
||||
f.String("stdin", "", "data supplied to hashcat standard input")
|
||||
f.Bool("quiet", false, "quiet mode")
|
||||
f.Bool("hex-charset", false, "treat charset as hex")
|
||||
f.Bool("hex-salt", false, "treat salt as hex")
|
||||
f.Bool("hex-wordlist", false, "treat wordlist as hex")
|
||||
f.Bool("force", false, "ignore warnings")
|
||||
f.Bool("deprecated-check-disable", false, "disable deprecated check")
|
||||
f.Bool("advice-disable", false, "disable hashcat advice")
|
||||
f.Bool("status", false, "enable status")
|
||||
f.Bool("status-json", false, "emit status in JSON")
|
||||
f.Uint32("status-timer", 0, "status timer interval (seconds)")
|
||||
@@ -30,12 +37,15 @@ func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.Bool("restore", false, "restore session")
|
||||
f.Bool("restore-disable", false, "disable restore")
|
||||
f.String("restore-file", "", "restore file path or identifier")
|
||||
f.Bool("restore-position", false, "restore from the stored position")
|
||||
f.Bool("restore-show-command", false, "display the stored restore command and stop")
|
||||
f.String("outfile", "", "outfile path or identifier")
|
||||
f.StringSlice("outfile-format", nil, "outfile formats (hash-salt, plain, hex-plain, crack-pos, timestamp-absolute, timestamp-relative, or numeric)")
|
||||
f.Bool("outfile-json", false, "write outfile records as JSON")
|
||||
f.Bool("outfile-autohex-disable", false, "disable outfile autohex")
|
||||
f.Uint32("outfile-check-timer", 0, "outfile check timer interval (seconds)")
|
||||
f.Bool("wordlist-autohex-disable", false, "disable wordlist autohex")
|
||||
f.String("separator", "", "separator character")
|
||||
f.Bool("stdout", false, "stdout output")
|
||||
f.Bool("show", false, "show cracked hashes")
|
||||
f.Bool("left", false, "show uncracked hashes")
|
||||
f.Bool("username", false, "enable username handling")
|
||||
@@ -43,33 +53,52 @@ func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.Uint32("remove-timer", 0, "remove timer interval (seconds)")
|
||||
f.Bool("potfile-disable", false, "disable potfile")
|
||||
f.String("potfile", "", "potfile path or identifier")
|
||||
f.String("encoding-from", "", "input encoding (iso-8859-15, utf-32le, or numeric)")
|
||||
f.String("encoding-to", "", "output encoding (iso-8859-15, utf-32le, or numeric)")
|
||||
f.String("encoding-from", "", "input encoding name (legacy enum names and numbers are also accepted)")
|
||||
f.String("encoding-to", "", "output encoding name (legacy enum names and numbers are also accepted)")
|
||||
f.Uint32("debug-mode", 0, "debug mode mask")
|
||||
f.String("debug-file", "", "debug file path or identifier")
|
||||
f.String("induction-dir", "", "induction directory path or identifier")
|
||||
f.String("outfile-check-dir", "", "outfile-check directory path or identifier")
|
||||
f.Bool("logfile-disable", false, "disable logfile")
|
||||
f.Uint32("hccapx-message-pair", 0, "hccapx message pair")
|
||||
f.Uint32("nonce-error-corrections", 0, "nonce error corrections")
|
||||
f.String("keyboard-layout-mapping", "", "keyboard layout mapping file or identifier")
|
||||
f.String("keyboard-layout-mapping", "", "local keyboard layout mapping file")
|
||||
f.String("truecrypt-keyfiles", "", "TrueCrypt keyfiles path or identifier")
|
||||
f.String("veracrypt-keyfiles", "", "VeraCrypt keyfiles path or identifier")
|
||||
f.Uint32("veracrypt-pim-start", 0, "VeraCrypt PIM start")
|
||||
f.Uint32("veracrypt-pim-stop", 0, "VeraCrypt PIM stop")
|
||||
f.Bool("benchmark", false, "run benchmark")
|
||||
f.Bool("benchmark-all", false, "run full benchmark")
|
||||
f.Uint32("benchmark-min", 0, "minimum benchmark runtime")
|
||||
f.Uint32("benchmark-max", 0, "maximum benchmark runtime")
|
||||
f.Bool("speed-only", false, "show speed only")
|
||||
f.Bool("progress-only", false, "show progress only")
|
||||
f.Uint32("segment-size", 0, "segment size")
|
||||
f.Uint32("segment-size", 0, "deprecated: removed in Hashcat v7")
|
||||
f.Bool("pipeline-stats", false, "show pipeline statistics")
|
||||
f.Bool("task-time-breakdown", false, "show task time breakdown")
|
||||
f.Uint32("bitmap-min", 0, "bitmap min")
|
||||
f.Uint32("bitmap-max", 0, "bitmap max")
|
||||
f.UintSlice("cpu-affinity", nil, "CPU affinity list")
|
||||
f.Uint32("hook-threads", 0, "hook threads")
|
||||
f.Bool("hash-info", false, "show hash information")
|
||||
f.Bool("hash-info", false, "show hash information from an idle crackstation (does not create a job)")
|
||||
f.Uint32("hash-info-level", 0, "hash information detail level: 1 or 2 (does not create a job)")
|
||||
f.Bool("backend-ignore-cuda", false, "ignore CUDA backend")
|
||||
f.Bool("backend-ignore-hip", false, "ignore HIP backend")
|
||||
f.Bool("backend-ignore-metal", false, "ignore Metal backend")
|
||||
f.Bool("backend-ignore-opencl", false, "ignore OpenCL backend")
|
||||
f.Bool("backend-info", false, "show backend info")
|
||||
f.Bool("backend-info", false, "show registered backend info for all connected crackstations (does not create a job)")
|
||||
f.Uint32("backend-info-level", 0, "registered backend information detail level: 1 or 2 (does not create a job)")
|
||||
f.UintSlice("backend-devices", nil, "backend device IDs")
|
||||
f.Uint32("backend-devices-virtmulti", 0, "virtual backend device multiplier")
|
||||
f.Uint32("backend-devices-virthost", 0, "virtual backend host count")
|
||||
f.UintSlice("opencl-device-types", nil, "OpenCL device types")
|
||||
f.Uint32("metal-compiler-runtime", 0, "Metal compiler runtime")
|
||||
f.Bool("optimized-kernel-enable", false, "enable optimized kernels")
|
||||
f.Bool("multiply-accel-disabled", false, "disable multiply accel")
|
||||
f.String("workload-profile", "", "workload profile (low, default, high, nightmare, or numeric)")
|
||||
f.Bool("multiply-accel-disable", false, "disable multiply accel")
|
||||
f.Bool("multiply-accel-disabled", false, "deprecated alias for multiply-accel-disable")
|
||||
_ = f.MarkDeprecated("multiply-accel-disabled", "use --multiply-accel-disable")
|
||||
_ = f.MarkHidden("multiply-accel-disabled")
|
||||
f.String("workload-profile", "", "deprecated: accepted but ignored by Hashcat v7")
|
||||
f.Uint32("kernel-accel", 0, "kernel acceleration")
|
||||
f.Uint32("kernel-loops", 0, "kernel loops")
|
||||
f.Uint32("kernel-threads", 0, "kernel threads")
|
||||
@@ -80,22 +109,47 @@ func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.Uint32("scrypt-tmto", 0, "scrypt tmto")
|
||||
f.Uint64("skip", 0, "skip count")
|
||||
f.Uint64("limit", 0, "limit count")
|
||||
f.Bool("keyspace", false, "show keyspace")
|
||||
f.String("rules-file", "", "rules file path or identifier")
|
||||
f.Bool("keyspace", false, "calculate keyspace on an idle crackstation and return it (does not create a job)")
|
||||
f.Bool("total-candidates", false, "calculate the total candidate count on an idle crackstation (does not create a job)")
|
||||
f.String("lookup", "", "locate a candidate in the attack on an idle crackstation (does not create a job)")
|
||||
f.String("crackstation", "", "target an exact crackstation name or host UUID for a synchronous query")
|
||||
f.Bool("dynamic-x", false, "enable dynamic-X mode")
|
||||
f.String("seekdb-path", "", "seek database path or identifier")
|
||||
f.String("rule-left", "", "left combinator rule")
|
||||
f.String("rule-right", "", "right combinator rule")
|
||||
f.StringArray("rules-file", nil, "rules file content or identifier (repeatable)")
|
||||
f.Uint32("generate-rules", 0, "generate rules")
|
||||
f.Uint32("generate-rules-fun-min", 0, "generate rules function min")
|
||||
f.Uint32("generate-rules-fun-max", 0, "generate rules function max")
|
||||
f.Uint32("generate-rules-func-min", 0, "generate rules function min")
|
||||
f.Uint32("generate-rules-func-max", 0, "generate rules function max")
|
||||
f.Uint32("generate-rules-fun-min", 0, "deprecated alias for generate-rules-func-min")
|
||||
f.Uint32("generate-rules-fun-max", 0, "deprecated alias for generate-rules-func-max")
|
||||
_ = f.MarkDeprecated("generate-rules-fun-min", "use --generate-rules-func-min")
|
||||
_ = f.MarkHidden("generate-rules-fun-min")
|
||||
_ = f.MarkDeprecated("generate-rules-fun-max", "use --generate-rules-func-max")
|
||||
_ = f.MarkHidden("generate-rules-fun-max")
|
||||
f.String("generate-rules-func-sel", "", "generate rules function selection")
|
||||
f.Int32("generate-rules-seed", 0, "generate rules seed")
|
||||
f.String("custom-charset1", "", "custom charset 1")
|
||||
f.String("custom-charset2", "", "custom charset 2")
|
||||
f.String("custom-charset3", "", "custom charset 3")
|
||||
f.String("custom-charset4", "", "custom charset 4")
|
||||
f.String("identify", "", "identify hash")
|
||||
f.String("custom-charset5", "", "custom charset 5")
|
||||
f.String("custom-charset6", "", "custom charset 6")
|
||||
f.String("custom-charset7", "", "custom charset 7")
|
||||
f.String("custom-charset8", "", "custom charset 8")
|
||||
f.String("identify", "", "deprecated: legacy positional input")
|
||||
f.Bool("identify-mode", false, "identify hash types on an idle crackstation (does not create a job)")
|
||||
f.Bool("increment", false, "incremental mode")
|
||||
f.Bool("increment-inverse", false, "inverse incremental mode")
|
||||
f.Uint32("increment-min", 0, "increment min")
|
||||
f.Uint32("increment-max", 0, "increment max")
|
||||
f.Uint32("bypass-delay", 0, "bypass delay")
|
||||
f.Uint32("bypass-threshold", 0, "bypass threshold")
|
||||
f.Bool("slow-candidates", false, "slow candidates")
|
||||
f.String("bridge-parameter1", "", "bridge parameter 1")
|
||||
f.String("bridge-parameter2", "", "bridge parameter 2")
|
||||
f.String("bridge-parameter3", "", "bridge parameter 3")
|
||||
f.String("bridge-parameter4", "", "bridge parameter 4")
|
||||
f.Bool("brain-server", false, "brain server mode")
|
||||
f.Uint32("brain-server-timer", 0, "brain server timer")
|
||||
f.Bool("brain-client", false, "brain client mode")
|
||||
@@ -105,4 +159,8 @@ func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.String("brain-password", "", "brain password")
|
||||
f.String("brain-session", "", "brain session")
|
||||
f.String("brain-session-whitelist", "", "brain session whitelist")
|
||||
f.Bool("brain-feed", false, "brain feed mode")
|
||||
f.Bool("color-cracked", false, "color cracked output")
|
||||
f.Bool("hash-copy", false, "copy hashes")
|
||||
f.String("encrypt-with-pubkey", "", "public key path or identifier used to encrypt output")
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestCrackJobLifecycleCommandsAreRegisteredAtExactHierarchy(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
for _, name := range []string{"cancel", "pause", "resume"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
command, args, err := root.Find([]string{"job", name, "job-id"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job %s: %v", name, err)
|
||||
}
|
||||
if got, want := command.CommandPath(), "crack job "+name; got != want {
|
||||
t.Fatalf("command path = %q, want %q", got, want)
|
||||
}
|
||||
if !reflect.DeepEqual(args, []string{"job-id"}) {
|
||||
t.Fatalf("remaining args = %#v, want [job-id]", args)
|
||||
}
|
||||
if got, want := command.Use, name+" [id]"; got != want {
|
||||
t.Fatalf("use = %q, want %q", got, want)
|
||||
}
|
||||
if err := command.Args(command, nil); err != nil {
|
||||
t.Fatalf("interactive selection rejected: %v", err)
|
||||
}
|
||||
if err := command.Args(command, []string{"job-id"}); err != nil {
|
||||
t.Fatalf("one job ID rejected: %v", err)
|
||||
}
|
||||
if err := command.Args(command, []string{"first", "second"}); err == nil {
|
||||
t.Fatal("more than one job ID was accepted")
|
||||
}
|
||||
if command.ValidArgsFunction == nil {
|
||||
t.Fatal("job ID completion is missing")
|
||||
}
|
||||
if command.InheritedFlags().Lookup("timeout") == nil {
|
||||
t.Fatal("--timeout is not inherited")
|
||||
}
|
||||
if command.InheritedFlags().Lookup("watch") != nil || command.InheritedFlags().Lookup("poll-interval") != nil {
|
||||
t.Fatal("crack job view flags leaked into lifecycle command")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobLifecycleCompletionOnlyOffersEligibleFullIDs(t *testing.T) {
|
||||
activeID := "aaaaaaaa-1111-4111-8111-111111111111"
|
||||
pausedID := "bbbbbbbb-2222-4222-8222-222222222222"
|
||||
completedID := "cccccccc-3333-4333-8333-333333333333"
|
||||
cancelledID := "dddddddd-4444-4444-8444-444444444444"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
want []string
|
||||
}{
|
||||
{name: "cancel", want: []string{activeID, pausedID}},
|
||||
{name: "pause", want: []string{activeID}},
|
||||
{name: "resume", want: []string{pausedID}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
capture := &crackJobsListCapture{jobs: &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{
|
||||
{ID: activeID, Status: clientpb.CrackJobStatus_IN_PROGRESS},
|
||||
{ID: pausedID, Status: clientpb.CrackJobStatus_PAUSED},
|
||||
{ID: completedID, Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
{ID: cancelledID, Status: clientpb.CrackJobStatus_CANCELLED},
|
||||
nil,
|
||||
{},
|
||||
}}}
|
||||
root := Commands(&console.SliverClient{Rpc: capture})[0]
|
||||
command, _, err := root.Find([]string{"job", test.name})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job %s: %v", test.name, err)
|
||||
}
|
||||
|
||||
values, directive := command.ValidArgsFunction(command, nil, "")
|
||||
if directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("completion directive = %v, want no-file-completion", directive)
|
||||
}
|
||||
got := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
got = append(got, strings.SplitN(value, "\t", 2)[0])
|
||||
}
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("completion IDs = %#v, want %#v", got, test.want)
|
||||
}
|
||||
if capture.calls != 1 {
|
||||
t.Fatalf("CrackJobs calls = %d, want 1", capture.calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobLifecycleEligibility(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
job *clientpb.CrackJob
|
||||
pausable bool
|
||||
resumable bool
|
||||
cancellable bool
|
||||
}{
|
||||
{name: "nil"},
|
||||
{name: "in progress", job: &clientpb.CrackJob{Status: clientpb.CrackJobStatus_IN_PROGRESS}, pausable: true, cancellable: true},
|
||||
{name: "paused", job: &clientpb.CrackJob{Status: clientpb.CrackJobStatus_PAUSED}, resumable: true, cancellable: true},
|
||||
{name: "completed", job: &clientpb.CrackJob{Status: clientpb.CrackJobStatus_COMPLETED}},
|
||||
{name: "failed", job: &clientpb.CrackJob{Status: clientpb.CrackJobStatus_FAILED}},
|
||||
{name: "cancelled", job: &clientpb.CrackJob{Status: clientpb.CrackJobStatus_CANCELLED}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := crackJobPausable(test.job); got != test.pausable {
|
||||
t.Errorf("pausable = %v, want %v", got, test.pausable)
|
||||
}
|
||||
if got := crackJobResumable(test.job); got != test.resumable {
|
||||
t.Errorf("resumable = %v, want %v", got, test.resumable)
|
||||
}
|
||||
if got := crackJobCancellable(test.job); got != test.cancellable {
|
||||
t.Errorf("cancellable = %v, want %v", got, test.cancellable)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobLifecycleEmptySelectionMessagesAreActionSpecific(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
action crackJobLifecycleAction
|
||||
want string
|
||||
}{
|
||||
{name: "cancel", action: newCrackJobCancelAction(nil), want: "No cancellable crack jobs"},
|
||||
{name: "pause", action: newCrackJobPauseAction(nil), want: "No pausable crack jobs"},
|
||||
{name: "resume", action: newCrackJobResumeAction(nil), want: "No resumable crack jobs"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.action.noEligibleJobsMessage != test.want {
|
||||
t.Fatalf("empty-selection message = %q, want %q", test.action.noEligibleJobsMessage, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCrackJobLifecycleActionsConfirmAndMutateByID(t *testing.T) {
|
||||
type contextKey struct{}
|
||||
ctx := context.WithValue(t.Context(), contextKey{}, "expected")
|
||||
jobID := "aaaaaaaa-1111-4111-8111-111111111111"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
status clientpb.CrackJobStatus
|
||||
updatedStatus clientpb.CrackJobStatus
|
||||
newAction func(crackJobMutationFunc) crackJobLifecycleAction
|
||||
wantConfirmation bool
|
||||
promptParts []string
|
||||
}{
|
||||
{
|
||||
name: "cancel active",
|
||||
status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
updatedStatus: clientpb.CrackJobStatus_CANCELLED,
|
||||
newAction: newCrackJobCancelAction,
|
||||
wantConfirmation: true,
|
||||
promptParts: []string{"Cancel crack job", jobID, "Unfinished work will be discarded"},
|
||||
},
|
||||
{
|
||||
name: "cancel paused",
|
||||
status: clientpb.CrackJobStatus_PAUSED,
|
||||
updatedStatus: clientpb.CrackJobStatus_CANCELLED,
|
||||
newAction: newCrackJobCancelAction,
|
||||
wantConfirmation: true,
|
||||
promptParts: []string{"Cancel crack job", jobID, "PAUSED"},
|
||||
},
|
||||
{
|
||||
name: "pause",
|
||||
status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
updatedStatus: clientpb.CrackJobStatus_PAUSED,
|
||||
newAction: newCrackJobPauseAction,
|
||||
wantConfirmation: true,
|
||||
promptParts: []string{"Pause crack job", jobID, "stopped", "beginning of their shards when resumed"},
|
||||
},
|
||||
{
|
||||
name: "resume",
|
||||
status: clientpb.CrackJobStatus_PAUSED,
|
||||
updatedStatus: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
newAction: newCrackJobResumeAction,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
mutateCalls := 0
|
||||
confirmCalls := 0
|
||||
action := test.newAction(func(gotCtx context.Context, request *clientpb.CrackJob) (*clientpb.CrackJob, error) {
|
||||
mutateCalls++
|
||||
if gotCtx.Value(contextKey{}) != "expected" {
|
||||
t.Fatal("mutation did not receive the caller context")
|
||||
}
|
||||
if request.GetID() != jobID || request.GetCommand() != nil || len(request.GetTasks()) != 0 || len(request.GetResults()) != 0 {
|
||||
t.Fatalf("mutation request = %#v, want ID-only request", request)
|
||||
}
|
||||
return &clientpb.CrackJob{ID: jobID, Status: test.updatedStatus, UpdatedAt: 1}, nil
|
||||
})
|
||||
updated, changed, err := applyCrackJobLifecycleAction(ctx, &clientpb.CrackJob{ID: " " + jobID + " ", Status: test.status}, action,
|
||||
func(prompt string) (bool, error) {
|
||||
confirmCalls++
|
||||
for _, part := range test.promptParts {
|
||||
if !strings.Contains(prompt, part) {
|
||||
t.Errorf("confirmation prompt %q does not contain %q", prompt, part)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("apply lifecycle action: %v", err)
|
||||
}
|
||||
if !changed || updated.GetID() != jobID || updated.GetStatus() != test.updatedStatus {
|
||||
t.Fatalf("updated=%#v changed=%v", updated, changed)
|
||||
}
|
||||
wantConfirmCalls := 0
|
||||
if test.wantConfirmation {
|
||||
wantConfirmCalls = 1
|
||||
}
|
||||
if confirmCalls != wantConfirmCalls || mutateCalls != 1 {
|
||||
t.Fatalf("confirm calls=%d mutate calls=%d, want %d/1", confirmCalls, mutateCalls, wantConfirmCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyFetchedCrackJobLifecycleActionRejectsMismatchedIDBeforeConfirmation(t *testing.T) {
|
||||
requestedID := "aaaaaaaa-1111-4111-8111-111111111111"
|
||||
returnedID := "bbbbbbbb-2222-4222-8222-222222222222"
|
||||
confirmCalls := 0
|
||||
mutationCalls := 0
|
||||
action := newCrackJobPauseAction(func(context.Context, *clientpb.CrackJob) (*clientpb.CrackJob, error) {
|
||||
mutationCalls++
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
updated, changed, err := applyFetchedCrackJobLifecycleAction(
|
||||
t.Context(),
|
||||
requestedID,
|
||||
&clientpb.CrackJob{ID: returnedID, Status: clientpb.CrackJobStatus_IN_PROGRESS},
|
||||
action,
|
||||
func(string) (bool, error) {
|
||||
confirmCalls++
|
||||
return true, nil
|
||||
},
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), requestedID) || !strings.Contains(err.Error(), returnedID) {
|
||||
t.Fatalf("mismatched detail error = %v, want both job IDs", err)
|
||||
}
|
||||
if updated != nil || changed || confirmCalls != 0 || mutationCalls != 0 {
|
||||
t.Fatalf("updated=%#v changed=%v confirm calls=%d mutation calls=%d, want nil/false/0/0", updated, changed, confirmCalls, mutationCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCrackJobLifecycleActionDeclineAndValidationErrorsDoNotMutate(t *testing.T) {
|
||||
sentinel := errors.New("sentinel")
|
||||
mutationCalls := 0
|
||||
mutation := func(context.Context, *clientpb.CrackJob) (*clientpb.CrackJob, error) {
|
||||
mutationCalls++
|
||||
return nil, sentinel
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
job *clientpb.CrackJob
|
||||
action crackJobLifecycleAction
|
||||
confirm crackJobConfirmFunc
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "cancel declined",
|
||||
job: &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS},
|
||||
action: newCrackJobCancelAction(mutation),
|
||||
confirm: func(string) (bool, error) { return false, nil },
|
||||
},
|
||||
{
|
||||
name: "pause confirmation unavailable",
|
||||
job: &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS},
|
||||
action: newCrackJobPauseAction(mutation),
|
||||
wantErr: "confirmation is unavailable",
|
||||
},
|
||||
{
|
||||
name: "cannot pause completed",
|
||||
job: &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
action: newCrackJobPauseAction(mutation),
|
||||
confirm: func(string) (bool, error) { return true, nil },
|
||||
wantErr: "only in-progress jobs can be paused",
|
||||
},
|
||||
{
|
||||
name: "cannot resume active",
|
||||
job: &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS},
|
||||
action: newCrackJobResumeAction(mutation),
|
||||
wantErr: "only paused jobs can be resumed",
|
||||
},
|
||||
{
|
||||
name: "cannot cancel terminal",
|
||||
job: &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_CANCELLED},
|
||||
action: newCrackJobCancelAction(mutation),
|
||||
confirm: func(string) (bool, error) { return true, nil },
|
||||
wantErr: "only in-progress or paused jobs can be cancelled",
|
||||
},
|
||||
{name: "empty job", action: newCrackJobResumeAction(mutation), wantErr: "empty crack job"},
|
||||
{name: "empty ID", job: &clientpb.CrackJob{Status: clientpb.CrackJobStatus_PAUSED}, action: newCrackJobResumeAction(mutation), wantErr: "ID cannot be empty"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
before := mutationCalls
|
||||
updated, changed, err := applyCrackJobLifecycleAction(t.Context(), test.job, test.action, test.confirm)
|
||||
if updated != nil || changed {
|
||||
t.Fatalf("updated=%#v changed=%v, want nil/false", updated, changed)
|
||||
}
|
||||
if test.wantErr == "" && err != nil {
|
||||
t.Fatalf("error = %v, want nil", err)
|
||||
}
|
||||
if test.wantErr != "" && (err == nil || !strings.Contains(err.Error(), test.wantErr)) {
|
||||
t.Fatalf("error = %v, want substring %q", err, test.wantErr)
|
||||
}
|
||||
if mutationCalls != before {
|
||||
t.Fatal("mutation ran despite decline or validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCrackJobLifecycleActionValidatesAuthoritativeResponse(t *testing.T) {
|
||||
job := &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_PAUSED}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
result *clientpb.CrackJob
|
||||
resultErr error
|
||||
wantErr string
|
||||
}{
|
||||
{name: "RPC error", resultErr: errors.New("sentinel"), wantErr: "sentinel"},
|
||||
{name: "nil", wantErr: "empty crack job after resume"},
|
||||
{name: "empty ID", result: &clientpb.CrackJob{Status: clientpb.CrackJobStatus_IN_PROGRESS}, wantErr: "without an ID after resume"},
|
||||
{name: "wrong ID", result: &clientpb.CrackJob{ID: "another-job", Status: clientpb.CrackJobStatus_IN_PROGRESS, UpdatedAt: 1}, wantErr: "after resume request for job-id"},
|
||||
{name: "missing timestamp", result: &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS}, wantErr: "without an update timestamp after resume"},
|
||||
{name: "wrong status", result: &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_PAUSED, UpdatedAt: 1}, wantErr: "unexpected crack job status PAUSED after resume"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
action := newCrackJobResumeAction(func(context.Context, *clientpb.CrackJob) (*clientpb.CrackJob, error) {
|
||||
return test.result, test.resultErr
|
||||
})
|
||||
updated, changed, err := applyCrackJobLifecycleAction(t.Context(), job, action, nil)
|
||||
if updated != nil || changed || err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
||||
t.Fatalf("updated=%#v changed=%v error=%v, want error containing %q", updated, changed, err, test.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobLifecycleMutationUsesCommandTimeout(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
pause, _, err := root.Find([]string{"job", "pause"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job pause: %v", err)
|
||||
}
|
||||
if err := pause.InheritedFlags().Set("timeout", "2"); err != nil {
|
||||
t.Fatalf("set timeout: %v", err)
|
||||
}
|
||||
started := time.Now()
|
||||
var deadline time.Time
|
||||
action := newCrackJobPauseAction(func(parent context.Context, request *clientpb.CrackJob) (*clientpb.CrackJob, error) {
|
||||
ctx, cancel := crackCommandContext(parent, pause)
|
||||
defer cancel()
|
||||
var ok bool
|
||||
deadline, ok = ctx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("mutation context has no deadline")
|
||||
}
|
||||
return &clientpb.CrackJob{ID: request.GetID(), Status: clientpb.CrackJobStatus_PAUSED, UpdatedAt: 1}, nil
|
||||
})
|
||||
if _, changed, err := applyCrackJobLifecycleAction(t.Context(), &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS}, action,
|
||||
func(string) (bool, error) { return true, nil }); err != nil || !changed {
|
||||
t.Fatalf("apply lifecycle action changed=%v error=%v", changed, err)
|
||||
}
|
||||
if deadline.Before(started.Add(1500*time.Millisecond)) || deadline.After(started.Add(2500*time.Millisecond)) {
|
||||
t.Fatalf("mutation deadline = %s, want about two seconds after %s", deadline, started)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestCrackJobRmCommandIsRegisteredAtExactHierarchy(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
rm, args, err := root.Find([]string{"job", "rm", "job-id"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job rm: %v", err)
|
||||
}
|
||||
if got := rm.CommandPath(); got != "crack job rm" {
|
||||
t.Fatalf("command path = %q, want %q", got, "crack job rm")
|
||||
}
|
||||
if !reflect.DeepEqual(args, []string{"job-id"}) {
|
||||
t.Fatalf("remaining args = %#v, want [job-id]", args)
|
||||
}
|
||||
if rm.Use != "rm [id]" {
|
||||
t.Fatalf("crack job rm use = %q, want %q", rm.Use, "rm [id]")
|
||||
}
|
||||
if err := rm.Args(rm, nil); err != nil {
|
||||
t.Fatalf("crack job rm rejected interactive selection: %v", err)
|
||||
}
|
||||
if err := rm.Args(rm, []string{"job-id"}); err != nil {
|
||||
t.Fatalf("crack job rm rejected one job ID: %v", err)
|
||||
}
|
||||
if err := rm.Args(rm, []string{"first", "second"}); err == nil {
|
||||
t.Fatal("crack job rm accepted more than one job ID")
|
||||
}
|
||||
if rm.ValidArgsFunction == nil {
|
||||
t.Fatal("crack job rm is missing ID completion")
|
||||
}
|
||||
if rm.InheritedFlags().Lookup("timeout") == nil {
|
||||
t.Fatal("crack job rm must inherit --timeout")
|
||||
}
|
||||
if rm.InheritedFlags().Lookup("watch") != nil || rm.InheritedFlags().Lookup("poll-interval") != nil {
|
||||
t.Fatal("crack job view flags leaked into crack job rm")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobRmCompletionOnlyOffersTerminalJobs(t *testing.T) {
|
||||
activeID := "aaaaaaaa-1111-4111-8111-111111111111"
|
||||
completedID := "bbbbbbbb-2222-4222-8222-222222222222"
|
||||
capture := &crackJobsListCapture{jobs: &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{
|
||||
{ID: activeID, Status: clientpb.CrackJobStatus_IN_PROGRESS},
|
||||
{ID: completedID, Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
}}}
|
||||
root := Commands(&console.SliverClient{Rpc: capture})[0]
|
||||
rm, _, err := root.Find([]string{"job", "rm"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job rm: %v", err)
|
||||
}
|
||||
|
||||
values, directive := rm.ValidArgsFunction(rm, nil, "")
|
||||
if len(values) != 1 || !strings.HasPrefix(values[0], completedID+"\t") {
|
||||
t.Fatalf("completion values = %#v, want only terminal job %q", values, completedID)
|
||||
}
|
||||
if directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("completion directive = %v, want no-file-completion", directive)
|
||||
}
|
||||
if capture.calls != 1 {
|
||||
t.Fatalf("CrackJobs calls = %d, want 1", capture.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveCrackJobConfirmsCascadeAndDeletesByID(t *testing.T) {
|
||||
type contextKey struct{}
|
||||
ctx := context.WithValue(t.Context(), contextKey{}, "expected")
|
||||
job := &clientpb.CrackJob{
|
||||
ID: " aaaaaaaa-1111-4111-8111-111111111111 ",
|
||||
Status: clientpb.CrackJobStatus_COMPLETED,
|
||||
Tasks: []*clientpb.CrackTask{{ID: "one"}, {ID: "two"}},
|
||||
ResultCount: 1,
|
||||
}
|
||||
confirmCalls := 0
|
||||
deleteCalls := 0
|
||||
removed, err := removeCrackJob(ctx, job, func(prompt string) (bool, error) {
|
||||
confirmCalls++
|
||||
want := "Permanently delete crack job \"aaaaaaaa-1111-4111-8111-111111111111\" (COMPLETED), including 2 tasks and 1 result?"
|
||||
if prompt != want {
|
||||
t.Fatalf("confirmation prompt = %q, want %q", prompt, want)
|
||||
}
|
||||
return true, nil
|
||||
}, func(gotCtx context.Context, request *clientpb.CrackJob) (*commonpb.Empty, error) {
|
||||
deleteCalls++
|
||||
if gotCtx.Value(contextKey{}) != "expected" {
|
||||
t.Fatal("delete did not receive the caller context")
|
||||
}
|
||||
if request.GetID() != "aaaaaaaa-1111-4111-8111-111111111111" || request.GetCommand() != nil || len(request.GetTasks()) != 0 || len(request.GetResults()) != 0 {
|
||||
t.Fatalf("delete request = %#v, want ID-only request", request)
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("removeCrackJob: %v", err)
|
||||
}
|
||||
if !removed || confirmCalls != 1 || deleteCalls != 1 {
|
||||
t.Fatalf("removed=%v confirm calls=%d delete calls=%d, want true/1/1", removed, confirmCalls, deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveFetchedCrackJobRejectsMismatchedIDBeforeConfirmation(t *testing.T) {
|
||||
requestedID := "aaaaaaaa-1111-4111-8111-111111111111"
|
||||
returnedID := "bbbbbbbb-2222-4222-8222-222222222222"
|
||||
confirmCalls := 0
|
||||
deleteCalls := 0
|
||||
|
||||
removed, err := removeFetchedCrackJob(
|
||||
t.Context(),
|
||||
requestedID,
|
||||
&clientpb.CrackJob{ID: returnedID, Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
func(string) (bool, error) {
|
||||
confirmCalls++
|
||||
return true, nil
|
||||
},
|
||||
func(context.Context, *clientpb.CrackJob) (*commonpb.Empty, error) {
|
||||
deleteCalls++
|
||||
return &commonpb.Empty{}, nil
|
||||
},
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), requestedID) || !strings.Contains(err.Error(), returnedID) {
|
||||
t.Fatalf("mismatched detail error = %v, want both job IDs", err)
|
||||
}
|
||||
if removed || confirmCalls != 0 || deleteCalls != 0 {
|
||||
t.Fatalf("removed=%v confirm calls=%d delete calls=%d, want false/0/0", removed, confirmCalls, deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveCrackJobDeclineAndErrorsDoNotReportRemoval(t *testing.T) {
|
||||
sentinel := errors.New("sentinel")
|
||||
tests := []struct {
|
||||
name string
|
||||
job *clientpb.CrackJob
|
||||
confirm crackJobConfirmFunc
|
||||
deleteJob crackJobDeleteFunc
|
||||
wantErr string
|
||||
wantConfirm int
|
||||
wantDelete int
|
||||
}{
|
||||
{
|
||||
name: "active job rejected before confirmation",
|
||||
job: &clientpb.CrackJob{ID: "active", Status: clientpb.CrackJobStatus_IN_PROGRESS},
|
||||
wantErr: "only completed, failed, or cancelled jobs can be removed",
|
||||
},
|
||||
{
|
||||
name: "declined",
|
||||
job: &clientpb.CrackJob{ID: "completed", Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
confirm: func(string) (bool, error) { return false, nil },
|
||||
wantConfirm: 1,
|
||||
},
|
||||
{
|
||||
name: "confirmation error",
|
||||
job: &clientpb.CrackJob{ID: "failed", Status: clientpb.CrackJobStatus_FAILED},
|
||||
confirm: func(string) (bool, error) { return false, sentinel },
|
||||
wantErr: "sentinel",
|
||||
wantConfirm: 1,
|
||||
},
|
||||
{
|
||||
name: "delete error",
|
||||
job: &clientpb.CrackJob{ID: "cancelled", Status: clientpb.CrackJobStatus_CANCELLED},
|
||||
confirm: func(string) (bool, error) { return true, nil },
|
||||
deleteJob: func(context.Context, *clientpb.CrackJob) (*commonpb.Empty, error) { return nil, sentinel },
|
||||
wantErr: "sentinel",
|
||||
wantConfirm: 1,
|
||||
wantDelete: 1,
|
||||
},
|
||||
{
|
||||
name: "empty response",
|
||||
wantErr: "server returned an empty crack job",
|
||||
},
|
||||
{
|
||||
name: "empty ID",
|
||||
job: &clientpb.CrackJob{Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
wantErr: "crack job ID cannot be empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
confirmCalls := 0
|
||||
deleteCalls := 0
|
||||
confirm := test.confirm
|
||||
if confirm == nil {
|
||||
confirm = func(string) (bool, error) {
|
||||
confirmCalls++
|
||||
return true, nil
|
||||
}
|
||||
} else {
|
||||
wrapped := confirm
|
||||
confirm = func(prompt string) (bool, error) {
|
||||
confirmCalls++
|
||||
return wrapped(prompt)
|
||||
}
|
||||
}
|
||||
deleteJob := test.deleteJob
|
||||
if deleteJob == nil {
|
||||
deleteJob = func(context.Context, *clientpb.CrackJob) (*commonpb.Empty, error) {
|
||||
deleteCalls++
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
} else {
|
||||
wrapped := deleteJob
|
||||
deleteJob = func(ctx context.Context, job *clientpb.CrackJob) (*commonpb.Empty, error) {
|
||||
deleteCalls++
|
||||
return wrapped(ctx, job)
|
||||
}
|
||||
}
|
||||
|
||||
removed, err := removeCrackJob(t.Context(), test.job, confirm, deleteJob)
|
||||
if removed {
|
||||
t.Fatal("removeCrackJob reported removal")
|
||||
}
|
||||
if test.wantErr == "" && err != nil {
|
||||
t.Fatalf("removeCrackJob error = %v, want nil", err)
|
||||
}
|
||||
if test.wantErr != "" && (err == nil || !strings.Contains(err.Error(), test.wantErr)) {
|
||||
t.Fatalf("removeCrackJob error = %v, want substring %q", err, test.wantErr)
|
||||
}
|
||||
if confirmCalls != test.wantConfirm || deleteCalls != test.wantDelete {
|
||||
t.Fatalf("confirm calls=%d delete calls=%d, want %d/%d", confirmCalls, deleteCalls, test.wantConfirm, test.wantDelete)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminalCrackJobsFiltersWithoutReordering(t *testing.T) {
|
||||
jobs := []*clientpb.CrackJob{
|
||||
{ID: "active", Status: clientpb.CrackJobStatus_IN_PROGRESS},
|
||||
nil,
|
||||
{ID: "failed", Status: clientpb.CrackJobStatus_FAILED},
|
||||
{ID: "completed", Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
{ID: "cancelled", Status: clientpb.CrackJobStatus_CANCELLED},
|
||||
}
|
||||
got := terminalCrackJobs(jobs)
|
||||
want := []*clientpb.CrackJob{jobs[2], jobs[3], jobs[4]}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("terminal jobs = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,463 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
func TestCrackJobCommandsAreRegistered(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
jobs, _, err := root.Find([]string{"jobs"})
|
||||
if err != nil || jobs == root {
|
||||
t.Fatalf("find crack jobs: command=%v error=%v", jobs, err)
|
||||
}
|
||||
top, _, err := root.Find([]string{"top"})
|
||||
if err != nil || top == root {
|
||||
t.Fatalf("find crack top: command=%v error=%v", top, err)
|
||||
}
|
||||
if top.Use != "top" {
|
||||
t.Fatalf("crack top use = %q, want top", top.Use)
|
||||
}
|
||||
if err := top.Args(top, nil); err != nil {
|
||||
t.Fatalf("crack top rejected no arguments: %v", err)
|
||||
}
|
||||
if err := top.Args(top, []string{"unexpected"}); err == nil {
|
||||
t.Fatal("crack top accepted a positional argument")
|
||||
}
|
||||
pollInterval := top.Flags().Lookup("poll-interval")
|
||||
if pollInterval == nil {
|
||||
t.Fatal("crack top is missing --poll-interval")
|
||||
}
|
||||
if got, err := top.Flags().GetDuration("poll-interval"); err != nil || got != time.Second {
|
||||
t.Fatalf("crack top --poll-interval = %s, %v; want %s", got, err, time.Second)
|
||||
}
|
||||
if top.PersistentFlags().Lookup("poll-interval") != nil || root.PersistentFlags().Lookup("poll-interval") != nil {
|
||||
t.Fatal("--poll-interval must remain local to crack top")
|
||||
}
|
||||
if top.InheritedFlags().Lookup("timeout") == nil {
|
||||
t.Fatal("crack top must inherit --timeout")
|
||||
}
|
||||
job, _, err := root.Find([]string{"job", "job-id"})
|
||||
if err != nil || job == root {
|
||||
t.Fatalf("find crack job: command=%v error=%v", job, err)
|
||||
}
|
||||
if job.Flags().Lookup("watch") == nil || job.Flags().Lookup("poll-interval") == nil {
|
||||
t.Fatal("crack job is missing watch flags")
|
||||
}
|
||||
if job.Use != "job [id]" {
|
||||
t.Fatalf("crack job use = %q, want %q", job.Use, "job [id]")
|
||||
}
|
||||
if job.ValidArgsFunction == nil {
|
||||
t.Fatal("crack job is missing ID completion")
|
||||
}
|
||||
if err := job.Args(job, nil); err != nil {
|
||||
t.Fatalf("crack job rejected interactive selection: %v", err)
|
||||
}
|
||||
if err := job.Args(job, []string{"job-id"}); err != nil {
|
||||
t.Fatalf("crack job rejected one job ID: %v", err)
|
||||
}
|
||||
if err := job.Args(job, []string{"first", "second"}); err == nil {
|
||||
t.Fatal("crack job accepted more than one job ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDUsesExplicitIDWithoutListing(t *testing.T) {
|
||||
listed := false
|
||||
selected := false
|
||||
jobID, err := resolveCrackJobID([]string{" full-job-id "}, func() (*clientpb.CrackJobs, error) {
|
||||
listed = true
|
||||
return nil, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
selected = true
|
||||
return "", nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resolveCrackJobID: %v", err)
|
||||
}
|
||||
if jobID != "full-job-id" {
|
||||
t.Fatalf("job ID = %q, want full-job-id", jobID)
|
||||
}
|
||||
if listed || selected {
|
||||
t.Fatalf("explicit ID unexpectedly listed=%v selected=%v", listed, selected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDSelectsFromListedJobs(t *testing.T) {
|
||||
wantJobs := []*clientpb.CrackJob{{ID: "first"}, {ID: "selected-full-id"}}
|
||||
listCalls := 0
|
||||
selectCalls := 0
|
||||
jobID, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
listCalls++
|
||||
return &clientpb.CrackJobs{Jobs: wantJobs}, nil
|
||||
}, func(jobs []*clientpb.CrackJob) (string, error) {
|
||||
selectCalls++
|
||||
if len(jobs) != len(wantJobs) || jobs[1] != wantJobs[1] {
|
||||
t.Fatalf("selector jobs = %#v, want %#v", jobs, wantJobs)
|
||||
}
|
||||
return "selected-full-id", nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resolveCrackJobID: %v", err)
|
||||
}
|
||||
if jobID != "selected-full-id" {
|
||||
t.Fatalf("job ID = %q, want selected-full-id", jobID)
|
||||
}
|
||||
if listCalls != 1 || selectCalls != 1 {
|
||||
t.Fatalf("list calls = %d, select calls = %d; want 1 each", listCalls, selectCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDHandlesEmptyAndInvalidSelections(t *testing.T) {
|
||||
selectorCalled := false
|
||||
for _, listedJobs := range [][]*clientpb.CrackJob{nil, {nil, {ID: " "}}} {
|
||||
_, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
return &clientpb.CrackJobs{Jobs: listedJobs}, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
selectorCalled = true
|
||||
return "unused", nil
|
||||
})
|
||||
if !errors.Is(err, errNoCrackJobs) {
|
||||
t.Fatalf("empty job error = %v, want %v", err, errNoCrackJobs)
|
||||
}
|
||||
}
|
||||
if selectorCalled {
|
||||
t.Fatal("selector called with no jobs")
|
||||
}
|
||||
|
||||
_, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
return &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{{ID: "job-id"}}}, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
return " ", nil
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "no crack job selected") {
|
||||
t.Fatalf("blank selection error = %v", err)
|
||||
}
|
||||
|
||||
_, err = resolveCrackJobID([]string{" "}, nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "job ID cannot be empty") {
|
||||
t.Fatalf("blank explicit ID error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDPropagatesUserAbort(t *testing.T) {
|
||||
_, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
return &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{{ID: "job-id"}}}, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
return "", forms.ErrUserAborted
|
||||
})
|
||||
if !errors.Is(err, forms.ErrUserAborted) {
|
||||
t.Fatalf("selection error = %v, want user abort", err)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // One fixture verifies the complete newest-status telemetry contract and all presentation helpers.
|
||||
func TestParseCrackStatusJSONUsesNewestStatus(t *testing.T) {
|
||||
raw := []byte("{\"status\":1,\"progress\":[1,10]}\n" +
|
||||
"{\"session\":\"distributed-job\",\"status\":2,\"target\":\"hashes\",\"progress\":[5,10]," +
|
||||
"\"restore_point\":4,\"recovered_hashes\":[1,3],\"recovered_salts\":[1,2],\"rejected\":7," +
|
||||
"\"time_start\":1700000000,\"estimated_stop\":1700000100," +
|
||||
"\"guess\":{\"guess_mode\":3,\"guess_base\":\"?d?d\",\"guess_base_offset\":1,\"guess_base_count\":10," +
|
||||
"\"guess_base_percent\":25.5,\"guess_mask_length\":2,\"guess_mod\":\"rules\",\"guess_mod_offset\":2,\"guess_mod_count\":4,\"guess_mod_percent\":50},\"devices\":[" +
|
||||
"{\"device_id\":1,\"device_name\":\"GPU 0\",\"device_type\":\"GPU\",\"speed\":1500,\"temp\":65,\"util\":98,\"corespeed\":2100,\"memoryspeed\":9500,\"buslanes\":16}," +
|
||||
"{\"id\":2,\"name\":\"GPU 1\",\"type\":\"GPU\",\"speed\":500,\"temperature\":70,\"utilization\":97}]}\n")
|
||||
|
||||
status, err := parseCrackStatusJSON(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("parseCrackStatusJSON: %v", err)
|
||||
}
|
||||
if status.Status != "2" || status.ProgressCurrent != "5" || status.ProgressTotal != "10" {
|
||||
t.Fatalf("unexpected status: %#v", status)
|
||||
}
|
||||
if got := status.progressText(); got != "5/10 (50.0%)" {
|
||||
t.Fatalf("progressText = %q", got)
|
||||
}
|
||||
if got := status.speedText(); got != "2.00 kH/s" {
|
||||
t.Fatalf("speedText = %q", got)
|
||||
}
|
||||
if got := status.temperatureText(); got != "70 C" {
|
||||
t.Fatalf("temperatureText = %q", got)
|
||||
}
|
||||
if len(status.Devices) != 2 || status.Devices[1].Name != "GPU 1" {
|
||||
t.Fatalf("devices = %#v", status.Devices)
|
||||
}
|
||||
if status.Session != "distributed-job" || status.Target != "hashes" || status.RestorePoint != "4" ||
|
||||
status.RecoveredHashesCurrent != "1" || status.RecoveredHashesTotal != "3" ||
|
||||
status.RecoveredSaltsCurrent != "1" || status.RecoveredSaltsTotal != "2" || status.Rejected != "7" ||
|
||||
status.TimeStart != "1700000000" || status.EstimatedStop != "1700000100" {
|
||||
t.Fatalf("full status fields were not decoded: %#v", status)
|
||||
}
|
||||
if status.Guess.Mode != "3" || status.Guess.Base != "?d?d" || status.Guess.BasePercent != "25.5" || status.Guess.MaskLength != "2" || status.Guess.Mod != "rules" || status.Guess.ModPercent != "50" {
|
||||
t.Fatalf("guess = %#v", status.Guess)
|
||||
}
|
||||
if got := status.guessText(); !strings.Contains(got, "base=?d?d [1/10] (25.5%)") || !strings.Contains(got, "mask-length=2") || !strings.Contains(got, "mod=rules [2/4] (50%)") {
|
||||
t.Fatalf("guessText = %q", got)
|
||||
}
|
||||
if status.Devices[0].CoreSpeed != "2100" || status.Devices[0].MemorySpeed != "9500" || status.Devices[0].BusLanes != "16" {
|
||||
t.Fatalf("extended device telemetry = %#v", status.Devices[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCrackStatusJSONRejectsMalformedOutput(t *testing.T) {
|
||||
if _, err := parseCrackStatusJSON([]byte("hashcat is still starting")); err == nil {
|
||||
t.Fatal("malformed status output was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnavailableDeviceTelemetryRendersAsUnknown(t *testing.T) {
|
||||
status := &crackStatusView{Devices: []crackDeviceView{{Temperature: "-1"}}}
|
||||
if got := status.temperatureText(); got != "-" {
|
||||
t.Fatalf("temperatureText = %q, want -", got)
|
||||
}
|
||||
if got := unitValue("-1", " C"); got != "-" {
|
||||
t.Fatalf("unitValue = %q, want -", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackJobIncludesTasksProgressDevicesAndResults(t *testing.T) {
|
||||
status := []byte(`{"session":"job-session","status":2,"target":"hash-file","progress":[25,100],"restore_point":24,"recovered_hashes":[1,2],"recovered_salts":[1,1],"rejected":3,"time_start":1700000000,"estimated_stop":1700000100,"guess":{"guess_mode":3,"guess_base":"?d?d","guess_base_offset":0,"guess_base_count":1},"devices":[{"device_id":1,"device_name":"GPU 0","device_type":"GPU","speed":2500000,"temp":61,"util":99,"fanspeed":45,"corespeed":2100,"memoryspeed":9000,"buslanes":16,"power":125000}]}`)
|
||||
recovered := []byte(`[{"hash":"hash-one","plaintext":"cGxhaW4tb25l"}]`)
|
||||
task := &clientpb.CrackTask{
|
||||
ID: "task-id",
|
||||
HostUUID: "station-id",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
UpdatedAt: 1700000002,
|
||||
LatestStatusJSON: status,
|
||||
RecoveredJSON: recovered,
|
||||
ShardSkip: 100,
|
||||
ShardLimit: 50,
|
||||
}
|
||||
job := &clientpb.CrackJob{
|
||||
ID: "job-id",
|
||||
CreatedAt: "2026-09-09T00:00:00Z",
|
||||
UpdatedAt: 1700000003,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Keyspace: "1000000",
|
||||
Tasks: []*clientpb.CrackTask{task},
|
||||
Results: []*clientpb.CrackResult{{
|
||||
Hash: "hash-one",
|
||||
Plaintext: []byte("plain-one"),
|
||||
}, {
|
||||
CredentialID: "credential-id",
|
||||
Hash: "hash-two",
|
||||
Plaintext: []byte("plain-two"),
|
||||
}},
|
||||
}
|
||||
|
||||
rawOutput := renderCrackJob(job, settings.SliverDefault)
|
||||
output := ansi.Strip(rawOutput)
|
||||
for _, expected := range []string{
|
||||
"job-id", "1000000", "task-id", "station-id", "100+50", "25/100 (25.0%)",
|
||||
"2.50 MH/s", "61 C", "GPU 0", "credential-id", "hash-one", "plain-one", "hash-two", "plain-two",
|
||||
"Hashcat Statistics", "job-session", "hash-file", "mode=3", "base=?d?d", "1/2", "1/1",
|
||||
"2100 MHz", "9000 MHz", "16 lanes", "125000 mW", "2023-11-14T22:13:20Z", "2023-11-14T22:15:00Z",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("rendered job does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldPrimary.Render("Crack Job"),
|
||||
console.StyleBoldWarning.Render(clientpb.CrackJobStatus_IN_PROGRESS.String()),
|
||||
console.StyleBoldPrimary.Render("Tasks"),
|
||||
console.StyleBoldWarning.Render(clientpb.CrackTaskState_CRACK_TASK_RUNNING.String()),
|
||||
console.StyleBoldPrimary.Render("Hashcat Statistics"),
|
||||
console.StyleBoldPrimary.Render("Devices"),
|
||||
console.StyleBoldSuccess.Render("Recovered Credentials"),
|
||||
} {
|
||||
if !strings.Contains(rawOutput, styled) {
|
||||
t.Fatalf("rendered job is missing Lip Gloss styling %q:\n%s", styled, rawOutput)
|
||||
}
|
||||
}
|
||||
if strings.Contains(rawOutput, console.StylePrimary.Render("task-id")) {
|
||||
t.Fatalf("rendered task ID should use the default foreground color:\n%s", rawOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackJobsSummarizesExtendedFields(t *testing.T) {
|
||||
job := &clientpb.CrackJob{
|
||||
ID: "job-id",
|
||||
CreatedAt: "2026-09-09T00:00:00Z",
|
||||
UpdatedAt: 1700000003,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Keyspace: "1000",
|
||||
Tasks: []*clientpb.CrackTask{{ID: "task-id"}},
|
||||
Results: []*clientpb.CrackResult{{Hash: "hash", Plaintext: []byte("plain")}},
|
||||
}
|
||||
rawOutput := renderCrackJobs([]*clientpb.CrackJob{job}, settings.SliverDefault)
|
||||
output := ansi.Strip(rawOutput)
|
||||
for _, expected := range []string{"job-id", "IN_PROGRESS", "1000", "1"} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("rendered jobs do not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(rawOutput, console.StyleBoldPrimary.Render("job-id")) ||
|
||||
!strings.Contains(rawOutput, console.StyleBoldWarning.Render(clientpb.CrackJobStatus_IN_PROGRESS.String())) {
|
||||
t.Fatalf("rendered jobs are missing Lip Gloss ID or status styling:\n%s", rawOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobStatusStyleUsesSemanticPalette(t *testing.T) {
|
||||
tests := []struct {
|
||||
status string
|
||||
want console.TextStyle
|
||||
}{
|
||||
{status: "IN_PROGRESS", want: console.StyleBoldWarning},
|
||||
{status: "CRACK_TASK_RUNNING", want: console.StyleBoldWarning},
|
||||
{status: "CRACK_TASK_QUEUED", want: console.StyleBold},
|
||||
{status: "CRACK_TASK_LEASED", want: console.StyleBoldWarning},
|
||||
{status: "COMPLETED", want: console.StyleBoldSuccess},
|
||||
{status: "CRACK_TASK_COMPLETED", want: console.StyleBoldSuccess},
|
||||
{status: "CRACK_TASK_FAILED", want: console.StyleBoldDanger},
|
||||
{status: "CANCELLED", want: console.StyleBoldGray},
|
||||
{status: "CANCELED", want: console.StyleBoldGray},
|
||||
{status: "PAUSED", want: console.StyleBoldGray},
|
||||
{status: "CRACK_TASK_CANCELLED", want: console.StyleBoldGray},
|
||||
{status: "UNKNOWN", want: console.StyleBold},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.status, func(t *testing.T) {
|
||||
got := crackJobStatusStyle(test.status)
|
||||
if got.GetBold() != test.want.GetBold() {
|
||||
t.Fatalf("bold = %v, want %v", got.GetBold(), test.want.GetBold())
|
||||
}
|
||||
gotColor := got.GetForeground()
|
||||
wantColor := test.want.GetForeground()
|
||||
if (gotColor == nil) != (wantColor == nil) {
|
||||
t.Fatalf("foreground = %v, want %v", gotColor, wantColor)
|
||||
}
|
||||
if gotColor == nil {
|
||||
return
|
||||
}
|
||||
gotR, gotG, gotB, gotA := gotColor.RGBA()
|
||||
wantR, wantG, wantB, wantA := wantColor.RGBA()
|
||||
if gotR != wantR || gotG != wantG || gotB != wantB || gotA != wantA {
|
||||
t.Fatalf("foreground RGBA = (%d,%d,%d,%d), want (%d,%d,%d,%d)", gotR, gotG, gotB, gotA, wantR, wantG, wantB, wantA)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackJobStylesErrors(t *testing.T) {
|
||||
rawOutput := renderCrackJob(&clientpb.CrackJob{
|
||||
ID: "job-id",
|
||||
Status: clientpb.CrackJobStatus_FAILED,
|
||||
Err: "worker failed",
|
||||
}, settings.SliverDefault)
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldDanger.Render(clientpb.CrackJobStatus_FAILED.String()),
|
||||
console.StyleBoldDanger.Render("Error"),
|
||||
console.StyleDanger.Render("worker failed"),
|
||||
} {
|
||||
if !strings.Contains(rawOutput, styled) {
|
||||
t.Fatalf("rendered error is missing Lip Gloss styling %q:\n%s", styled, rawOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobResultCountUsesSummaryField(t *testing.T) {
|
||||
job := &clientpb.CrackJob{Results: []*clientpb.CrackResult{{Plaintext: []byte("not-a-summary-count")}}}
|
||||
field := findProtoField(job.ProtoReflect(), "ResultCount")
|
||||
want := "-"
|
||||
if field != nil {
|
||||
job.ProtoReflect().Set(field, protoreflect.ValueOfUint64(7))
|
||||
want = "7"
|
||||
}
|
||||
if got := crackJobResultCount(job); got != want {
|
||||
t.Fatalf("crackJobResultCount() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobResultsRenderAuthoritativePlaintextBytes(t *testing.T) {
|
||||
job := &clientpb.CrackJob{Results: []*clientpb.CrackResult{
|
||||
{Hash: "valid", Plaintext: []byte("line\ntext\x1b[31m")},
|
||||
{Hash: "binary", Plaintext: []byte{0xff, 0x00, 0x1b}},
|
||||
{Hash: "nul", Plaintext: []byte{'a', 0, 'b'}},
|
||||
}}
|
||||
results := crackJobResults(job)
|
||||
if len(results) != 3 {
|
||||
t.Fatalf("crackJobResults() = %#v", results)
|
||||
}
|
||||
if results[0].Plaintext != "linetext[31m" {
|
||||
t.Fatalf("valid UTF-8 plaintext = %q, want sanitized text", results[0].Plaintext)
|
||||
}
|
||||
if results[1].Plaintext != "0xff001b" {
|
||||
t.Fatalf("binary plaintext = %q, want 0xff001b", results[1].Plaintext)
|
||||
}
|
||||
if results[2].Plaintext != "0x610062" {
|
||||
t.Fatalf("NUL plaintext = %q, want 0x610062", results[2].Plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchCrackJobPrintsChangesAndStopsAtTerminalState(t *testing.T) {
|
||||
inProgress := &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS}
|
||||
completed := &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_COMPLETED}
|
||||
responses := []*clientpb.CrackJob{inProgress, inProgress, completed}
|
||||
requests := 0
|
||||
fetch := func(_ context.Context, id string) (*clientpb.CrackJob, error) {
|
||||
if id != "job-id" {
|
||||
t.Fatalf("job ID = %q", id)
|
||||
}
|
||||
response := responses[requests]
|
||||
requests++
|
||||
return response, nil
|
||||
}
|
||||
updates := make([]clientpb.CrackJobStatus, 0)
|
||||
err := watchCrackJob(context.Background(), "job-id", time.Millisecond, fetch, func(job *clientpb.CrackJob) {
|
||||
updates = append(updates, job.Status)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("watchCrackJob: %v", err)
|
||||
}
|
||||
if requests != 3 {
|
||||
t.Fatalf("requests = %d, want 3", requests)
|
||||
}
|
||||
if len(updates) != 2 || updates[0] != clientpb.CrackJobStatus_IN_PROGRESS || updates[1] != clientpb.CrackJobStatus_COMPLETED {
|
||||
t.Fatalf("updates = %#v", updates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchCrackJobCancellationAndValidation(t *testing.T) {
|
||||
fetch := func(_ context.Context, _ string) (*clientpb.CrackJob, error) {
|
||||
return &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS}, nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if err := watchCrackJob(ctx, "job-id", time.Hour, fetch, nil); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("watchCrackJob cancellation error = %v", err)
|
||||
}
|
||||
if err := watchCrackJob(context.Background(), "job-id", 0, fetch, nil); err == nil {
|
||||
t.Fatal("watchCrackJob accepted zero poll interval")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveredResultsStripTerminalControlCharacters(t *testing.T) {
|
||||
results := parseRecoveredResults([]byte(`[{"hash":"hash\u001b[31m","plaintext":"cGxhaW4KdGV4dA=="}]`))
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("results = %#v", results)
|
||||
}
|
||||
if strings.ContainsAny(results[0].Hash, "\x1b\n\r") || strings.ContainsAny(results[0].Plaintext, "\x1b\n\r") {
|
||||
t.Fatalf("control characters were not stripped: %#v", results[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveredResultsRenderNULPlaintextAsHex(t *testing.T) {
|
||||
results := parseRecoveredResults([]byte(`[{"hash":"hash","plaintext":"YQBi"}]`))
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("results = %#v", results)
|
||||
}
|
||||
if results[0].Plaintext != "0x610062" {
|
||||
t.Fatalf("plaintext = %q, want 0x610062", results[0].Plaintext)
|
||||
}
|
||||
}
|
||||
+241
-51
@@ -22,7 +22,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -31,23 +32,47 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
)
|
||||
|
||||
// CrackCmd - GPU password cracking interface
|
||||
func CrackCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if shouldRunCrack(cmd, args) {
|
||||
invocation, err := classifyCrackInvocation(cmd, args)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if invocation.mode == crackInvocationBackendInfo {
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
crackers, err := connectedCrackstations(ctx, con.Rpc)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if len(crackers) == 0 {
|
||||
PrintNoCrackstations(con)
|
||||
return
|
||||
}
|
||||
printCrackersWithBackendInfoLevel(crackers, con, false, invocation.backendInfoLevel)
|
||||
return
|
||||
}
|
||||
|
||||
if invocation.mode == crackInvocationJob || invocation.isSynchronousQuery() {
|
||||
crackCmd, err := buildCrackCommand(cmd, args)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
timeoutSeconds, _ := cmd.Flags().GetInt64("timeout")
|
||||
ctx := context.Background()
|
||||
if timeoutSeconds > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
if err := resolveManagedCrackFileReferences(ctx, crackCmd, func(ctx context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return con.Rpc.CrackFilesList(ctx, request)
|
||||
}); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := con.Rpc.Crack(ctx, crackCmd)
|
||||
@@ -55,6 +80,16 @@ func CrackCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if invocation.isSynchronousQuery() {
|
||||
value, stderr, err := crackQueryResponse(resp, invocation)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
printCrackQueryStderr(con, stderr)
|
||||
printCrackQueryValue(con, value)
|
||||
return
|
||||
}
|
||||
if resp == nil || resp.Job == nil {
|
||||
con.PrintInfof("Crack request submitted\n")
|
||||
return
|
||||
@@ -89,21 +124,139 @@ func CrackCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalCrackKeyspace(response *clientpb.CrackResponse) (string, error) {
|
||||
return crackQueryResponseValue(response, crackInvocation{mode: crackInvocationKeyspace})
|
||||
}
|
||||
|
||||
func (invocation crackInvocation) isSynchronousQuery() bool {
|
||||
switch invocation.mode {
|
||||
case crackInvocationKeyspace, crackInvocationTotalCandidates, crackInvocationLookup, crackInvocationIdentify, crackInvocationHashInfo:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (invocation crackInvocation) queryMode() clientpb.CrackQueryMode {
|
||||
switch invocation.mode {
|
||||
case crackInvocationKeyspace:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE
|
||||
case crackInvocationTotalCandidates:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_TOTAL_CANDIDATES
|
||||
case crackInvocationLookup:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP
|
||||
case crackInvocationIdentify:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_IDENTIFY
|
||||
case crackInvocationHashInfo:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_HASH_INFO
|
||||
default:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func crackQueryResponseValue(response *clientpb.CrackResponse, invocation crackInvocation) (string, error) {
|
||||
value, _, err := crackQueryResponse(response, invocation)
|
||||
return value, err
|
||||
}
|
||||
|
||||
func crackQueryResponse(response *clientpb.CrackResponse, invocation crackInvocation) (string, string, error) {
|
||||
if response == nil {
|
||||
return "", "", fmt.Errorf("server returned an empty %s response", crackInvocationName(invocation.mode))
|
||||
}
|
||||
expectedMode := invocation.queryMode()
|
||||
if expectedMode == clientpb.CrackQueryMode_CRACK_QUERY_UNSPECIFIED {
|
||||
return "", "", fmt.Errorf("invalid synchronous crack query mode")
|
||||
}
|
||||
query := response.GetQuery()
|
||||
if query == nil {
|
||||
// Servers predating typed query results returned only Keyspace. Do not use
|
||||
// that legacy field for a selected station because it cannot prove which
|
||||
// station produced the result.
|
||||
if invocation.mode == crackInvocationKeyspace && invocation.crackstation == "" {
|
||||
value, err := canonicalCrackDecimal(response.GetKeyspace(), "keyspace")
|
||||
return value, "", err
|
||||
}
|
||||
return "", "", fmt.Errorf("server returned an empty %s query result", crackInvocationName(invocation.mode))
|
||||
}
|
||||
if query.GetMode() != expectedMode {
|
||||
return "", "", fmt.Errorf("server returned %s for %s query", query.GetMode().String(), crackInvocationName(invocation.mode))
|
||||
}
|
||||
value := query.GetValue()
|
||||
switch invocation.mode {
|
||||
case crackInvocationKeyspace, crackInvocationTotalCandidates:
|
||||
value, err := canonicalCrackDecimal(value, crackInvocationName(invocation.mode))
|
||||
return value, query.GetStderr(), err
|
||||
default:
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "", "", fmt.Errorf("server returned an empty %s query result", crackInvocationName(invocation.mode))
|
||||
}
|
||||
return value, query.GetStderr(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalCrackDecimal(value string, name string) (string, error) {
|
||||
raw := strings.TrimSpace(value)
|
||||
keyspace, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("server returned invalid %s %q", name, value)
|
||||
}
|
||||
return strconv.FormatUint(keyspace, 10), nil
|
||||
}
|
||||
|
||||
func printCrackQueryValue(con *console.SliverClient, value string) {
|
||||
con.Printf("%s", value)
|
||||
if !strings.HasSuffix(value, "\n") {
|
||||
con.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func printCrackQueryStderr(con *console.SliverClient, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(value, "\n") {
|
||||
value += "\n"
|
||||
}
|
||||
con.PrintWarnf("%s", value)
|
||||
}
|
||||
|
||||
// CrackStationsCmd - Manage GPU cracking stations
|
||||
func CrackStationsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
crackers, err := con.Rpc.Crackstations(context.Background(), &commonpb.Empty{})
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
crackers, err := connectedCrackstations(ctx, con.Rpc)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
showBenchmarks, _ := cmd.Flags().GetBool("show-benchmarks")
|
||||
if len(crackers.Crackstations) == 0 {
|
||||
if len(crackers) == 0 {
|
||||
PrintNoCrackstations(con)
|
||||
} else {
|
||||
PrintCrackers(crackers.Crackstations, con, showBenchmarks)
|
||||
PrintCrackers(crackers, con, showBenchmarks)
|
||||
}
|
||||
}
|
||||
|
||||
func connectedCrackstations(ctx context.Context, rpc rpcpb.SliverRPCClient) ([]*clientpb.Crackstation, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
response, err := rpc.Crackstations(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
crackers := make([]*clientpb.Crackstation, 0, len(response.Crackstations))
|
||||
for _, cracker := range response.Crackstations {
|
||||
if cracker != nil {
|
||||
crackers = append(crackers, cracker)
|
||||
}
|
||||
}
|
||||
return crackers, nil
|
||||
}
|
||||
|
||||
func PrintNoCrackstations(con *console.SliverClient) {
|
||||
con.PrintInfof("No crackstations connected to server\n")
|
||||
}
|
||||
@@ -117,59 +270,96 @@ func AreCrackersOnline(con *console.SliverClient) bool {
|
||||
}
|
||||
|
||||
func PrintCrackers(crackers []*clientpb.Crackstation, con *console.SliverClient, showBenchmarks bool) {
|
||||
sort.Slice(crackers, func(i, j int) bool {
|
||||
return crackers[i].Name < crackers[j].Name
|
||||
printCrackersWithBackendInfoLevel(crackers, con, showBenchmarks, 1)
|
||||
}
|
||||
|
||||
func printCrackersWithBackendInfoLevel(crackers []*clientpb.Crackstation, con *console.SliverClient, showBenchmarks bool, backendInfoLevel uint32) {
|
||||
ordered := make([]*clientpb.Crackstation, 0, len(crackers))
|
||||
for _, cracker := range crackers {
|
||||
if cracker != nil {
|
||||
ordered = append(ordered, cracker)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
if ordered[i].Name == ordered[j].Name {
|
||||
return ordered[i].HostUUID < ordered[j].HostUUID
|
||||
}
|
||||
return ordered[i].Name < ordered[j].Name
|
||||
})
|
||||
for index, cracker := range crackers {
|
||||
printCracker(cracker, index, con, showBenchmarks)
|
||||
if index < len(crackers)-1 {
|
||||
for index, cracker := range ordered {
|
||||
printCracker(cracker, index, con, showBenchmarks, backendInfoLevel)
|
||||
if index < len(ordered)-1 {
|
||||
con.Println()
|
||||
con.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.StyleBoldOrange.Render(fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, cracker.Name, cracker.OperatorName)) + "\n")
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Operating System"), fmt.Sprintf("%s/%s", cracker.GOOS, cracker.GOARCH)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Hashcat Version"), cracker.HashcatVersion})
|
||||
if 0 < len(cracker.CUDA) {
|
||||
for _, cuda := range cracker.CUDA {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("CUDA Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", cuda.Name, cuda.Version))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", cuda.MemoryFree, cuda.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", cuda.Clock)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", cuda.Processors)})
|
||||
}
|
||||
}
|
||||
if 0 < len(cracker.Metal) {
|
||||
for _, metal := range cracker.Metal {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Metal Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", metal.Name, metal.Version))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", metal.MemoryFree, metal.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", metal.Clock)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", metal.Processors)})
|
||||
}
|
||||
}
|
||||
if 0 < len(cracker.OpenCL) {
|
||||
for _, openCL := range cracker.OpenCL {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("OpenCL Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", openCL.Name, openCL.Version))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", openCL.MemoryFree, openCL.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", openCL.Clock)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", openCL.Processors)})
|
||||
}
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool, backendInfoLevel uint32) {
|
||||
con.Printf("%s\n", renderCracker(cracker, index, settings.GetTableStyle(con), backendInfoLevel))
|
||||
if showBenchmarks {
|
||||
con.Println()
|
||||
printBenchmarks(cracker, con)
|
||||
}
|
||||
}
|
||||
|
||||
func renderCracker(cracker *clientpb.Crackstation, index int, style table.Style, backendInfoLevel uint32) string {
|
||||
if cracker == nil {
|
||||
return ""
|
||||
}
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(style)
|
||||
tw.SetTitle(console.StyleBoldOrange.Render(fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, safeCrackCell(cracker.Name), safeCrackCell(cracker.OperatorName))) + "\n")
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Operating System"), fmt.Sprintf("%s/%s", valueOrDash(cracker.GOOS), valueOrDash(cracker.GOARCH))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Hashcat Version"), valueOrDash(cracker.HashcatVersion)})
|
||||
if backendInfoLevel >= 2 {
|
||||
appendCrackstationDetailRows(tw, cracker)
|
||||
}
|
||||
for _, cuda := range cracker.CUDA {
|
||||
appendCUDABackendRows(tw, cuda, backendInfoLevel)
|
||||
}
|
||||
for _, hip := range cracker.HIP {
|
||||
appendHIPBackendRows(tw, hip, backendInfoLevel)
|
||||
}
|
||||
for _, metal := range cracker.Metal {
|
||||
appendMetalBackendRows(tw, metal, backendInfoLevel)
|
||||
}
|
||||
for _, openCL := range cracker.OpenCL {
|
||||
appendOpenCLBackendRows(tw, openCL, backendInfoLevel)
|
||||
}
|
||||
if !hasReportedBackendDevice(cracker) {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Backend Devices"), "No backend devices reported"})
|
||||
}
|
||||
return tw.Render()
|
||||
}
|
||||
|
||||
func formatCUDADevice(device *clientpb.CUDABackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetCUDAVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatHIPDevice(device *clientpb.HIPBackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetHIPVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatMetalDevice(device *clientpb.MetalBackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetMetalVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatOpenCLDevice(device *clientpb.OpenCLBackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetOpenCLVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatBackendDevice(name string, versions ...string) string {
|
||||
for _, version := range versions {
|
||||
if version != "" {
|
||||
return fmt.Sprintf("%s (%s)", name, version)
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func printBenchmarks(cracker *clientpb.Crackstation, con *console.SliverClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestFormatBackendDevice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
device string
|
||||
versions []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "specific runtime version wins",
|
||||
device: "Apple M5",
|
||||
versions: []string{"Metal 410.7", "legacy-generic"},
|
||||
want: "Apple M5 (Metal 410.7)",
|
||||
},
|
||||
{
|
||||
name: "legacy generic fallback",
|
||||
device: "NVIDIA GPU",
|
||||
versions: []string{"", "13.0"},
|
||||
want: "NVIDIA GPU (13.0)",
|
||||
},
|
||||
{
|
||||
name: "no empty version suffix",
|
||||
device: "AMD GPU",
|
||||
versions: []string{"", ""},
|
||||
want: "AMD GPU",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := formatBackendDevice(test.device, test.versions...); got != test.want {
|
||||
t.Fatalf("formatBackendDevice() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendSpecificDisplayVersions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "CUDA version",
|
||||
got: formatCUDADevice(&clientpb.CUDABackendInfo{
|
||||
Name: "NVIDIA GPU", CUDAVersion: "13.0", Version: "legacy",
|
||||
}),
|
||||
want: "NVIDIA GPU (13.0)",
|
||||
},
|
||||
{
|
||||
name: "HIP version",
|
||||
got: formatHIPDevice(&clientpb.HIPBackendInfo{
|
||||
Name: "AMD GPU", HIPVersion: "7.0.51831", Version: "legacy",
|
||||
}),
|
||||
want: "AMD GPU (7.0.51831)",
|
||||
},
|
||||
{
|
||||
name: "Metal version",
|
||||
got: formatMetalDevice(&clientpb.MetalBackendInfo{
|
||||
Name: "Apple M5", MetalVersion: "Metal 410.7", Version: "legacy",
|
||||
}),
|
||||
want: "Apple M5 (Metal 410.7)",
|
||||
},
|
||||
{
|
||||
name: "legacy CUDA fallback",
|
||||
got: formatCUDADevice(&clientpb.CUDABackendInfo{
|
||||
Name: "NVIDIA GPU", Version: "12.9",
|
||||
}),
|
||||
want: "NVIDIA GPU (12.9)",
|
||||
},
|
||||
{
|
||||
name: "OpenCL version",
|
||||
got: formatOpenCLDevice(&clientpb.OpenCLBackendInfo{
|
||||
Name: "Portable GPU", OpenCLVersion: "OpenCL 3.0", Version: "legacy",
|
||||
}),
|
||||
want: "Portable GPU (OpenCL 3.0)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.got != test.want {
|
||||
t.Fatalf("backend display = %q, want %q", test.got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerBackendInfoLevelTwo(t *testing.T) {
|
||||
zero := uint32(0)
|
||||
falseValue := false
|
||||
station := &clientpb.Crackstation{
|
||||
Name: "station",
|
||||
OperatorName: "operator",
|
||||
HostUUID: "host-uuid",
|
||||
Version: "station-v1",
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
HashcatVersion: "v7.1.2",
|
||||
CUDA: []*clientpb.CUDABackendInfo{{
|
||||
Name: "CUDA GPU", CUDAVersion: "CUDA 13", Version: "device-v1", Type: "GPU",
|
||||
Vendor: "NVIDIA", VendorID: 4318, BackendDeviceID: 1, BackendDeviceAlias: &zero,
|
||||
PreferredThreadSize: &zero, MemoryUnified: &falseValue, LocalMemory: "64 KB",
|
||||
CacheSize: "8 MB", PCIAddress: "01:00.0", MemoryFree: "8 GB", MemoryTotal: "12 GB",
|
||||
}},
|
||||
HIP: []*clientpb.HIPBackendInfo{{Name: "HIP GPU", HIPVersion: "HIP 7", BackendDeviceID: 2}},
|
||||
Metal: []*clientpb.MetalBackendInfo{{
|
||||
Name: "Metal GPU", MetalVersion: "Metal 410", BackendDeviceID: 3,
|
||||
RegistryID: &zero, GPUHeadless: &falseValue, GPULowPower: &falseValue, GPURemovable: &falseValue,
|
||||
PhysicalLocation: "built-in", MaxTXRate: "100", MemoryAllocPerBlock: "4 GB",
|
||||
}},
|
||||
OpenCL: []*clientpb.OpenCLBackendInfo{{
|
||||
Name: "OpenCL GPU", OpenCLVersion: "OpenCL 3.0", OpenCLDriverVersion: "driver-v1",
|
||||
BackendDeviceID: 4, PlatformVendor: "Portable", PlatformName: "Platform",
|
||||
PlatformVersion: "platform-v1", MemoryAllocPerBlock: "2 GB",
|
||||
}},
|
||||
}
|
||||
|
||||
output := renderCracker(station, 0, settings.SliverDefault, 2)
|
||||
for _, expected := range []string{
|
||||
"station", "operator", "host-uuid", "station-v1", "linux/amd64", "v7.1.2",
|
||||
"CUDA GPU", "HIP GPU", "Metal GPU", "OpenCL GPU", "OpenCL 3.0", "driver-v1",
|
||||
"Backend Device ID", "Backend Device Alias", "Preferred Thread Size", "Unified Memory",
|
||||
"NVIDIA", "4318", "device-v1", "64 KB", "8 MB", "01:00.0",
|
||||
"Registry ID", "Headless", "Low Power", "Removable", "built-in", "100",
|
||||
"OpenCL Platform ID", "Portable", "Platform", "platform-v1", "2 GB",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("level-2 backend info does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerBackendInfoLevelOneOmitsDetails(t *testing.T) {
|
||||
station := &clientpb.Crackstation{
|
||||
Name: "station",
|
||||
CUDA: []*clientpb.CUDABackendInfo{{
|
||||
Name: "GPU", BackendDeviceID: 7, PCIAddress: "01:00.0",
|
||||
}},
|
||||
}
|
||||
output := renderCracker(station, 0, settings.SliverDefault, 1)
|
||||
for _, unexpected := range []string{"Host UUID", "Backend Device ID", "PCI Address"} {
|
||||
if strings.Contains(output, unexpected) {
|
||||
t.Errorf("level-1 backend info unexpectedly contains %q:\n%s", unexpected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerPreservesOptionalPresence(t *testing.T) {
|
||||
zero := uint32(0)
|
||||
falseValue := false
|
||||
withPresence := renderCracker(&clientpb.Crackstation{
|
||||
CUDA: []*clientpb.CUDABackendInfo{{
|
||||
BackendDeviceAlias: &zero,
|
||||
MemoryUnified: &falseValue,
|
||||
}},
|
||||
}, 0, settings.SliverDefault, 2)
|
||||
if !strings.Contains(withPresence, "Backend Device Alias") || !strings.Contains(withPresence, "Unified Memory") {
|
||||
t.Fatalf("present zero/false optional fields were omitted:\n%s", withPresence)
|
||||
}
|
||||
|
||||
withoutPresence := renderCracker(&clientpb.Crackstation{
|
||||
CUDA: []*clientpb.CUDABackendInfo{{}},
|
||||
}, 0, settings.SliverDefault, 2)
|
||||
if strings.Contains(withoutPresence, "Backend Device Alias") || strings.Contains(withoutPresence, "Unified Memory") {
|
||||
t.Fatalf("absent optional fields were rendered:\n%s", withoutPresence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerReportsNoBackendDevices(t *testing.T) {
|
||||
output := renderCracker(&clientpb.Crackstation{
|
||||
Name: "station",
|
||||
CUDA: []*clientpb.CUDABackendInfo{nil},
|
||||
}, 0, settings.SliverDefault, 1)
|
||||
if !strings.Contains(output, "No backend devices reported") {
|
||||
t.Fatalf("missing no-device message:\n%s", output)
|
||||
}
|
||||
}
|
||||
@@ -1,419 +1,11 @@
|
||||
package crack
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Hashcat mode names sourced from client/credentials/EXAMPLES.md.
|
||||
var hashcatHashTypes = map[int32]string{
|
||||
0: "MD5",
|
||||
10: "md5($pass.$salt)",
|
||||
11: "Joomla < 2.5.18",
|
||||
12: "PostgreSQL",
|
||||
20: "md5($salt.$pass)",
|
||||
21: "osCommerce, xt:Commerce",
|
||||
22: "Juniper NetScreen/SSG (ScreenOS)",
|
||||
23: "Skype",
|
||||
24: "SolarWinds Serv-U",
|
||||
30: "md5(utf16le($pass).$salt)",
|
||||
40: "md5($salt.utf16le($pass))",
|
||||
50: "HMAC-MD5 (key = $pass)",
|
||||
60: "HMAC-MD5 (key = $salt)",
|
||||
70: "md5(utf16le($pass))",
|
||||
100: "SHA1",
|
||||
101: "nsldap, SHA-1(Base64), Netscape LDAP SHA",
|
||||
110: "sha1($pass.$salt)",
|
||||
111: "nsldaps, SSHA-1(Base64), Netscape LDAP SSHA",
|
||||
112: "Oracle S: Type (Oracle 11+)",
|
||||
120: "sha1($salt.$pass)",
|
||||
121: "SMF (Simple Machines Forum) > v1.1",
|
||||
122: "macOS v10.4, macOS v10.5, macOS v10.6",
|
||||
123: "EPi",
|
||||
124: "Django (SHA-1)",
|
||||
125: "ArubaOS",
|
||||
130: "sha1(utf16le($pass).$salt)",
|
||||
131: "MSSQL (2000)",
|
||||
132: "MSSQL (2005)",
|
||||
133: "PeopleSoft",
|
||||
140: "sha1($salt.utf16le($pass))",
|
||||
141: "Episerver 6.x < .NET 4",
|
||||
150: "HMAC-SHA1 (key = $pass)",
|
||||
160: "HMAC-SHA1 (key = $salt)",
|
||||
170: "sha1(utf16le($pass))",
|
||||
190: "sha1(LinkedIn)",
|
||||
200: "MySQL323",
|
||||
300: "MySQL4.1/MySQL5",
|
||||
400: "phpass, phpBB3 (MD5)",
|
||||
500: "md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)",
|
||||
501: "Juniper IVE",
|
||||
600: "BLAKE2b-512",
|
||||
900: "MD4",
|
||||
1000: "NTLM",
|
||||
1100: "Domain Cached Credentials (DCC), MS Cache",
|
||||
1300: "SHA2-224",
|
||||
1400: "SHA2-256",
|
||||
1410: "sha256($pass.$salt)",
|
||||
1411: "SSHA-256(Base64), LDAP {SSHA256}",
|
||||
1420: "sha256($salt.$pass)",
|
||||
1421: "hMailServer",
|
||||
1430: "sha256(utf16le($pass).$salt)",
|
||||
1431: "base64(sha256(unicode($pass)))",
|
||||
1440: "sha256($salt.utf16le($pass))",
|
||||
1441: "Episerver 6.x >= .NET 4",
|
||||
1450: "HMAC-SHA256 (key = $pass)",
|
||||
1460: "HMAC-SHA256 (key = $salt)",
|
||||
1470: "sha256(utf16le($pass))",
|
||||
1500: "descrypt, DES (Unix), Traditional DES",
|
||||
1600: "Apache $apr1$ MD5, md5apr1, MD5 (APR)",
|
||||
1700: "SHA2-512",
|
||||
1710: "sha512($pass.$salt)",
|
||||
1711: "SSHA-512(Base64), LDAP {SSHA512}",
|
||||
1720: "sha512($salt.$pass)",
|
||||
1722: "macOS v10.7",
|
||||
1730: "sha512(utf16le($pass).$salt)",
|
||||
1731: "MSSQL (2012, 2014)",
|
||||
1740: "sha512($salt.utf16le($pass))",
|
||||
1750: "HMAC-SHA512 (key = $pass)",
|
||||
1760: "HMAC-SHA512 (key = $salt)",
|
||||
1770: "sha512(utf16le($pass))",
|
||||
1800: "sha512crypt $6$, SHA512 (Unix)",
|
||||
2000: "STDOUT",
|
||||
2100: "Domain Cached Credentials 2 (DCC2), MS Cache 2",
|
||||
2400: "Cisco-PIX MD5",
|
||||
2410: "Cisco-ASA MD5",
|
||||
2500: "WPA-EAPOL-PBKDF2",
|
||||
2501: "WPA-EAPOL-PMK",
|
||||
2600: "md5(md5($pass))",
|
||||
2611: "vBulletin < v3.8.5",
|
||||
2612: "PHPS",
|
||||
2711: "vBulletin >= v3.8.5",
|
||||
2811: "MyBB 1.2+, IPB2+ (Invision Power Board)",
|
||||
3000: "LM",
|
||||
3100: "Oracle H: Type (Oracle 7+)",
|
||||
3200: "bcrypt $2\\*$, Blowfish (Unix)",
|
||||
3300: "MD5(Sun)",
|
||||
3500: "md5(md5(md5($pass)))",
|
||||
3610: "md5(md5($salt).$pass )",
|
||||
3710: "md5($salt.md5($pass))",
|
||||
3711: "MediaWiki B type",
|
||||
3720: "md5($pass.md5($salt))",
|
||||
3721: "WebEdition CMS",
|
||||
3800: "md5($salt.$pass.$salt)",
|
||||
3910: "md5(md5($pass).md5($salt))",
|
||||
4010: "md5($salt.md5($salt.$pass))",
|
||||
4110: "md5($salt.md5($pass.$salt))",
|
||||
4210: "md5($username.0.$pass)",
|
||||
4300: "md5(strtoupper(md5($pass)))",
|
||||
4400: "md5(sha1($pass))",
|
||||
4500: "sha1(sha1($pass))",
|
||||
4510: "sha1(sha1($pass).$salt)",
|
||||
4520: "sha1($salt.sha1($pass))",
|
||||
4521: "Redmine",
|
||||
4522: "PunBB",
|
||||
4600: "sha1(sha1(sha1($pass)))",
|
||||
4700: "sha1(md5($pass))",
|
||||
4710: "sha1(md5($pass).$salt)",
|
||||
4711: "Huawei sha1(md5($pass).$salt)",
|
||||
4800: "iSCSI CHAP authentication, MD5(CHAP)",
|
||||
4900: "sha1($salt.$pass.$salt)",
|
||||
5000: "sha1(sha1($salt.$pass.$salt))",
|
||||
5100: "Half MD5",
|
||||
5200: "Password Safe v3",
|
||||
5300: "IKE-PSK MD5",
|
||||
5400: "IKE-PSK SHA1",
|
||||
5500: "NetNTLMv1 / NetNTLMv1+ESS",
|
||||
5600: "NetNTLMv2",
|
||||
5700: "Cisco-IOS type 4 (SHA256)",
|
||||
5800: "Samsung Android Password/PIN",
|
||||
6000: "RIPEMD-160",
|
||||
6100: "Whirlpool",
|
||||
6211: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish",
|
||||
6212: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-AES / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish-Serpent",
|
||||
6213: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish-Serpent / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-Twofish-AES",
|
||||
6221: "TrueCrypt 5.0+ SHA512 + AES / TrueCrypt 5.0+ SHA512 + Serpent / TrueCrypt 5.0+ SHA512 + Twofish",
|
||||
6222: "TrueCrypt 5.0+ SHA512 + AES-Twofish / TrueCrypt 5.0+ SHA512 + Serpent-AES / TrueCrypt 5.0+ SHA512 + Twofish-Serpent",
|
||||
6223: "TrueCrypt 5.0+ SHA512 + AES-Twofish-Serpent / TrueCrypt 5.0+ SHA512 + Serpent-Twofish-AES",
|
||||
6231: "TrueCrypt 5.0+ Whirlpool + AES / TrueCrypt 5.0+ Whirlpool + Serpent / TrueCrypt 5.0+ Whirlpool + Twofish",
|
||||
6232: "TrueCrypt 5.0+ Whirlpool + AES-Twofish / TrueCrypt 5.0+ Whirlpool + Serpent-AES / TrueCrypt 5.0+ Whirlpool + Twofish-Serpent",
|
||||
6233: "TrueCrypt 5.0+ Whirlpool + AES-Twofish-Serpent / TrueCrypt 5.0+ Whirlpool + Serpent-Twofish-AES",
|
||||
6241: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish + boot",
|
||||
6242: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-AES + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish-Serpent + boot",
|
||||
6243: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish-Serpent + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-Twofish-AES + boot",
|
||||
6300: "AIX {smd5}",
|
||||
6400: "AIX {ssha256}",
|
||||
6500: "AIX {ssha512}",
|
||||
6600: "1Password, agilekeychain",
|
||||
6700: "AIX {ssha1}",
|
||||
6800: "LastPass + LastPass sniffed",
|
||||
6900: "GOST R 34.11-94",
|
||||
7000: "FortiGate (FortiOS)",
|
||||
7100: "macOS v10.8+ (PBKDF2-SHA512)",
|
||||
7200: "GRUB 2",
|
||||
7300: "IPMI2 RAKP HMAC-SHA1",
|
||||
7400: "sha256crypt $5$, SHA256 (Unix)",
|
||||
7401: "MySQL $A$ (sha256crypt)",
|
||||
7500: "Kerberos 5, etype 23, AS-REQ Pre-Auth",
|
||||
7700: "SAP CODVN B (BCODE)",
|
||||
7701: "SAP CODVN B (BCODE) from RFC_READ_TABLE",
|
||||
7800: "SAP CODVN F/G (PASSCODE)",
|
||||
7801: "SAP CODVN F/G (PASSCODE) from RFC_READ_TABLE",
|
||||
7900: "Drupal7",
|
||||
8000: "Sybase ASE",
|
||||
8100: "Citrix NetScaler (SHA1)",
|
||||
8200: "1Password, cloudkeychain",
|
||||
8300: "DNSSEC (NSEC3)",
|
||||
8400: "WBB3 (Woltlab Burning Board)",
|
||||
8500: "RACF",
|
||||
8600: "Lotus Notes/Domino 5",
|
||||
8700: "Lotus Notes/Domino 6",
|
||||
8800: "Android FDE <= 4.3",
|
||||
8900: "scrypt",
|
||||
9000: "Password Safe v2",
|
||||
9100: "Lotus Notes/Domino 8",
|
||||
9200: "Cisco-IOS $8$ (PBKDF2-SHA256)",
|
||||
9300: "Cisco-IOS $9$ (scrypt)",
|
||||
9400: "MS Office 2007",
|
||||
9500: "MS Office 2010",
|
||||
9600: "MS Office 2013",
|
||||
9700: "MS Office <= 2003 MD5 + RC4, oldoffice$0, oldoffice$1",
|
||||
9710: "MS Office <= 2003 $0/$1, MD5 + RC4, collider #1",
|
||||
9720: "MS Office <= 2003 $0/$1, MD5 + RC4, collider #2",
|
||||
9800: "MS Office <= 2003 SHA1 + RC4, oldoffice$3, oldoffice$4",
|
||||
9810: "MS Office <= 2003 $3, SHA1 + RC4, collider #1",
|
||||
9820: "MS Office <= 2003 $3, SHA1 + RC4, collider #2",
|
||||
9900: "Radmin2",
|
||||
10000: "Django (PBKDF2-SHA256)",
|
||||
10100: "SipHash",
|
||||
10200: "CRAM-MD5",
|
||||
10300: "SAP CODVN H (PWDSALTEDHASH) iSSHA-1",
|
||||
10400: "PDF 1.1 - 1.3 (Acrobat 2 - 4)",
|
||||
10410: "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #1",
|
||||
10420: "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #2",
|
||||
10500: "PDF 1.4 - 1.6 (Acrobat 5 - 8)",
|
||||
10600: "PDF 1.7 Level 3 (Acrobat 9)",
|
||||
10700: "PDF 1.7 Level 8 (Acrobat 10 - 11)",
|
||||
10800: "SHA2-384",
|
||||
10810: "sha384($pass.$salt)",
|
||||
10820: "sha384($salt.$pass)",
|
||||
10830: "sha384(utf16le($pass).$salt)",
|
||||
10840: "sha384($salt.utf16le($pass))",
|
||||
10870: "sha384(utf16le($pass))",
|
||||
10900: "PBKDF2-HMAC-SHA256",
|
||||
10901: "RedHat 389-DS LDAP (PBKDF2-HMAC-SHA256)",
|
||||
11000: "PrestaShop",
|
||||
11100: "PostgreSQL CRAM (MD5)",
|
||||
11200: "MySQL CRAM (SHA1)",
|
||||
11300: "Bitcoin/Litecoin wallet.dat",
|
||||
11400: "SIP digest authentication (MD5)",
|
||||
11500: "CRC32",
|
||||
11600: "7-Zip",
|
||||
11700: "GOST R 34.11-2012 (Streebog) 256-bit, big-endian",
|
||||
11750: "HMAC-Streebog-256 (key = $pass), big-endian",
|
||||
11760: "HMAC-Streebog-256 (key = $salt), big-endian",
|
||||
11800: "GOST R 34.11-2012 (Streebog) 512-bit, big-endian",
|
||||
11850: "HMAC-Streebog-512 (key = $pass), big-endian",
|
||||
11860: "HMAC-Streebog-512 (key = $salt), big-endian",
|
||||
11900: "PBKDF2-HMAC-MD5",
|
||||
12000: "PBKDF2-HMAC-SHA1",
|
||||
12001: "Atlassian (PBKDF2-HMAC-SHA1)",
|
||||
12100: "PBKDF2-HMAC-SHA512",
|
||||
12200: "eCryptfs",
|
||||
12300: "Oracle T: Type (Oracle 12+)",
|
||||
12400: "BSDi Crypt, Extended DES",
|
||||
12500: "RAR3-hp",
|
||||
12600: "ColdFusion 10+",
|
||||
12700: "Blockchain, My Wallet",
|
||||
12800: "MS-AzureSync PBKDF2-HMAC-SHA256",
|
||||
12900: "Android FDE (Samsung DEK)",
|
||||
13000: "RAR5",
|
||||
13100: "Kerberos 5, etype 23, TGS-REP",
|
||||
13200: "AxCrypt 1",
|
||||
13300: "AxCrypt 1 in-memory SHA1",
|
||||
13400: "KeePass 1 AES / without keyfile / KeePass 2 AES / without keyfile / KeePass 1 Twofish / with keyfile / Keepass 2 AES / with keyfile",
|
||||
13500: "PeopleSoft PS_TOKEN",
|
||||
13600: "WinZip",
|
||||
13711: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + AES / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Serpent / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Twofish",
|
||||
13712: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + AES-Twofish / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Serpent-AES / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Twofish-Serpent",
|
||||
13713: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + Serpent-Twofish-AES",
|
||||
13721: "VeraCrypt PBKDF2-HMAC-SHA512 + AES / VeraCrypt PBKDF2-HMAC-SHA512 + Serpent / VeraCrypt PBKDF2-HMAC-SHA512 + Twofish",
|
||||
13722: "VeraCrypt PBKDF2-HMAC-SHA512 + AES-Twofish / VeraCrypt PBKDF2-HMAC-SHA512 + Serpent-AES / VeraCrypt PBKDF2-HMAC-SHA512 + Twofish-Serpent",
|
||||
13723: "VeraCrypt PBKDF2-HMAC-SHA512 + Serpent-Twofish-AES",
|
||||
13731: "VeraCrypt PBKDF2-HMAC-Whirlpool + AES / VeraCrypt PBKDF2-HMAC-Whirlpool + Serpent / VeraCrypt PBKDF2-HMAC-Whirlpool + Twofish",
|
||||
13732: "VeraCrypt PBKDF2-HMAC-Whirlpool + AES-Twofish / VeraCrypt PBKDF2-HMAC-Whirlpool + Serpent-AES / VeraCrypt PBKDF2-HMAC-Whirlpool + Twofish-Serpent",
|
||||
13733: "VeraCrypt PBKDF2-HMAC-Whirlpool + Serpent-Twofish-AES",
|
||||
13741: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + boot-mode + AES",
|
||||
13742: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + boot-mode + AES-Twofish",
|
||||
13743: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + boot-mode + AES-Twofish-Serpent",
|
||||
13751: "VeraCrypt PBKDF2-HMAC-SHA256 + AES / VeraCrypt PBKDF2-HMAC-SHA256 + Serpent / VeraCrypt PBKDF2-HMAC-SHA256 + Twofish",
|
||||
13752: "VeraCrypt PBKDF2-HMAC-SHA256 + AES-Twofish / VeraCrypt PBKDF2-HMAC-SHA256 + Serpent-AES / VeraCrypt PBKDF2-HMAC-SHA256 + Twofish-Serpent",
|
||||
13753: "VeraCrypt PBKDF2-HMAC-SHA256 + Serpent-Twofish-AES",
|
||||
13761: "VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + Twofish / VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + PIM + AES",
|
||||
13762: "VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + Serpent-AES",
|
||||
13763: "VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + Serpent-Twofish-AES",
|
||||
13771: "VeraCrypt Streebog-512 + XTS 512 bit",
|
||||
13772: "VeraCrypt Streebog-512 + XTS 1024 bit",
|
||||
13773: "VeraCrypt Streebog-512 + XTS 1536 bit",
|
||||
13800: "Windows Phone 8+ PIN/password",
|
||||
13900: "OpenCart",
|
||||
14000: "DES (PT = $salt, key = $pass)",
|
||||
14100: "3DES (PT = $salt, key = $pass)",
|
||||
14400: "sha1(CX)",
|
||||
14500: "Linux Kernel Crypto API (2.4)",
|
||||
14600: "LUKS",
|
||||
14700: "iTunes backup < 10.0",
|
||||
14800: "iTunes backup >= 10.0",
|
||||
14900: "Skip32 (PT = $salt, key = $pass)",
|
||||
15000: "FileZilla Server >= 0.9.55",
|
||||
15100: "Juniper/NetBSD sha1crypt",
|
||||
15200: "Blockchain, My Wallet, V2",
|
||||
15300: "DPAPI masterkey file v1 + local context",
|
||||
15400: "ChaCha20",
|
||||
15500: "JKS Java Key Store Private Keys (SHA1)",
|
||||
15600: "Ethereum Wallet, PBKDF2-HMAC-SHA256",
|
||||
15700: "Ethereum Wallet, SCRYPT",
|
||||
15900: "DPAPI masterkey file v2 + Active Directory domain context",
|
||||
16000: "Tripcode",
|
||||
16100: "TACACS+",
|
||||
16200: "Apple Secure Notes",
|
||||
16300: "Ethereum Pre-Sale Wallet, PBKDF2-HMAC-SHA256",
|
||||
16400: "CRAM-MD5 Dovecot",
|
||||
16500: "JWT (JSON Web Token)",
|
||||
16600: "Electrum Wallet (Salt-Type 1-3)",
|
||||
16700: "FileVault 2",
|
||||
16800: "WPA-PMKID-PBKDF2",
|
||||
16801: "WPA-PMKID-PMK",
|
||||
16900: "Ansible Vault",
|
||||
17200: "PKZIP (Compressed)",
|
||||
17210: "PKZIP (Uncompressed)",
|
||||
17220: "PKZIP (Compressed Multi-File)",
|
||||
17225: "PKZIP (Mixed Multi-File)",
|
||||
17230: "PKZIP (Mixed Multi-File Checksum-Only)",
|
||||
17300: "SHA3-224",
|
||||
17400: "SHA3-256",
|
||||
17500: "SHA3-384",
|
||||
17600: "SHA3-512",
|
||||
17700: "Keccak-224",
|
||||
17800: "Keccak-256",
|
||||
17900: "Keccak-384",
|
||||
18000: "Keccak-512",
|
||||
18100: "TOTP (HMAC-SHA1)",
|
||||
18200: "Kerberos 5, etype 23, AS-REP",
|
||||
18300: "Apple File System (APFS)",
|
||||
18400: "Open Document Format (ODF) 1.2 (SHA-256, AES)",
|
||||
18500: "sha1(md5(md5($pass)))",
|
||||
18600: "Open Document Format (ODF) 1.1 (SHA-1, Blowfish)",
|
||||
18700: "Java Object hashCode()",
|
||||
18800: "Blockchain, My Wallet, Second Password (SHA256)",
|
||||
18900: "Android Backup",
|
||||
19000: "QNX /etc/shadow (MD5)",
|
||||
19100: "QNX /etc/shadow (SHA256)",
|
||||
19200: "QNX /etc/shadow (SHA512)",
|
||||
19300: "sha1($salt1.$pass.$salt2)",
|
||||
19500: "Ruby on Rails Restful-Authentication",
|
||||
19600: "Kerberos 5, etype 17, TGS-REP (AES128-CTS-HMAC-SHA1-96)",
|
||||
19700: "Kerberos 5, etype 18, TGS-REP (AES256-CTS-HMAC-SHA1-96)",
|
||||
19800: "Kerberos 5, etype 17, Pre-Auth",
|
||||
19900: "Kerberos 5, etype 18, Pre-Auth",
|
||||
20011: "DiskCryptor SHA512 + XTS 512 bit (AES) / DiskCryptor SHA512 + XTS 512 bit (Twofish) / DiskCryptor SHA512 + XTS 512 bit (Serpent)",
|
||||
20012: "DiskCryptor SHA512 + XTS 1024 bit (AES-Twofish) / DiskCryptor SHA512 + XTS 1024 bit (Twofish-Serpent) / DiskCryptor SHA512 + XTS 1024 bit (Serpent-AES)",
|
||||
20013: "DiskCryptor SHA512 + XTS 1536 bit (AES-Twofish-Serpent)",
|
||||
20200: "Python passlib pbkdf2-sha512",
|
||||
20300: "Python passlib pbkdf2-sha256",
|
||||
20400: "Python passlib pbkdf2-sha1",
|
||||
20500: "PKZIP Master Key",
|
||||
20510: "PKZIP Master Key (6 byte optimization)",
|
||||
20600: "Oracle Transportation Management (SHA256)",
|
||||
20710: "sha256(sha256($pass).$salt)",
|
||||
20711: "AuthMe sha256",
|
||||
20720: "sha256($salt.sha256($pass))",
|
||||
20800: "sha256(md5($pass))",
|
||||
20900: "md5(sha1($pass).md5($pass).sha1($pass))",
|
||||
21000: "BitShares v0.x - sha512(sha512_bin(pass))",
|
||||
21100: "sha1(md5($pass.$salt))",
|
||||
21200: "md5(sha1($salt).md5($pass))",
|
||||
21300: "md5($salt.sha1($salt.$pass))",
|
||||
21400: "sha256(sha256_bin($pass))",
|
||||
21500: "SolarWinds Orion",
|
||||
21501: "SolarWinds Orion v2",
|
||||
21600: "Web2py pbkdf2-sha512",
|
||||
21700: "Electrum Wallet (Salt-Type 4)",
|
||||
21800: "Electrum Wallet (Salt-Type 5)",
|
||||
22000: "WPA-PBKDF2-PMKID+EAPOL",
|
||||
22001: "WPA-PMK-PMKID+EAPOL",
|
||||
22100: "BitLocker",
|
||||
22200: "Citrix NetScaler (SHA512)",
|
||||
22300: "sha256($salt.$pass.$salt)",
|
||||
22301: "Telegram Mobile App Passcode (SHA256)",
|
||||
22400: "AES Crypt (SHA256)",
|
||||
22500: "MultiBit Classic .key (MD5)",
|
||||
22600: "Telegram Desktop < v2.1.14 (PBKDF2-HMAC-SHA1)",
|
||||
22700: "MultiBit HD (scrypt)",
|
||||
22911: "RSA/DSA/EC/OpenSSH Private Keys ($0$)",
|
||||
22921: "RSA/DSA/EC/OpenSSH Private Keys ($6$)",
|
||||
22931: "RSA/DSA/EC/OpenSSH Private Keys ($1, $3$)",
|
||||
22941: "RSA/DSA/EC/OpenSSH Private Keys ($4$)",
|
||||
22951: "RSA/DSA/EC/OpenSSH Private Keys ($5$)",
|
||||
23001: "SecureZIP AES-128",
|
||||
23002: "SecureZIP AES-192",
|
||||
23003: "SecureZIP AES-256",
|
||||
23100: "Apple Keychain",
|
||||
23200: "XMPP SCRAM PBKDF2-SHA1",
|
||||
23300: "Apple iWork",
|
||||
23400: "Bitwarden",
|
||||
23500: "AxCrypt 2 AES-128",
|
||||
23600: "AxCrypt 2 AES-256",
|
||||
23700: "RAR3-p (Uncompressed)",
|
||||
23800: "RAR3-p (Compressed)",
|
||||
23900: "BestCrypt v3 Volume Encryption",
|
||||
24100: "MongoDB ServerKey SCRAM-SHA-1",
|
||||
24200: "MongoDB ServerKey SCRAM-SHA-256",
|
||||
24300: "sha1($salt.sha1($pass.$salt))",
|
||||
24410: "PKCS#8 Private Keys (PBKDF2-HMAC-SHA1 + 3DES/AES)",
|
||||
24420: "PKCS#8 Private Keys (PBKDF2-HMAC-SHA256 + 3DES/AES)",
|
||||
24500: "Telegram Desktop >= v2.1.14 (PBKDF2-HMAC-SHA512)",
|
||||
24600: "SQLCipher",
|
||||
24700: "Stuffit5",
|
||||
24800: "Umbraco HMAC-SHA1",
|
||||
24900: "Dahua Authentication MD5",
|
||||
25300: "MS Office 2016 - SheetProtection",
|
||||
25400: "PDF 1.4 - 1.6 (Acrobat 5 - 8) - edit password",
|
||||
25500: "Stargazer Stellar Wallet XLM",
|
||||
25600: "bcrypt(md5($pass)) / bcryptmd5",
|
||||
25700: "MurmurHash",
|
||||
25800: "bcrypt(sha1($pass)) / bcryptsha1",
|
||||
25900: "KNX IP Secure - Device Authentication Code",
|
||||
26000: "Mozilla key3.db",
|
||||
26100: "Mozilla key4.db",
|
||||
26200: "OpenEdge Progress Encode",
|
||||
26300: "FortiGate256 (FortiOS256)",
|
||||
26401: "AES-128-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
26402: "AES-192-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
26403: "AES-256-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
26500: "iPhone passcode (UID key + System Keybag)",
|
||||
26600: "MetaMask Wallet",
|
||||
99999: "Plaintext",
|
||||
}
|
||||
import "github.com/bishopfox/sliver/client/command/crack/internal/hashcatdisplay"
|
||||
|
||||
func hashcatHashTypeName(mode int32) (string, bool) {
|
||||
name, ok := hashcatHashTypes[mode]
|
||||
return name, ok
|
||||
return hashcatdisplay.TypeName(mode)
|
||||
}
|
||||
|
||||
func humanizeHashRate(rate uint64) string {
|
||||
const unit = 1000
|
||||
if rate < unit {
|
||||
return fmt.Sprintf("%d H/s", rate)
|
||||
}
|
||||
value := float64(rate)
|
||||
exp := 0
|
||||
for value >= unit && exp < 5 {
|
||||
value /= unit
|
||||
exp++
|
||||
}
|
||||
suffixes := []string{"kH/s", "MH/s", "GH/s", "TH/s", "PH/s"}
|
||||
if exp == 0 {
|
||||
return fmt.Sprintf("%d H/s", rate)
|
||||
}
|
||||
if exp > len(suffixes) {
|
||||
exp = len(suffixes)
|
||||
}
|
||||
return fmt.Sprintf("%.2f %s", value, suffixes[exp-1])
|
||||
return hashcatdisplay.HumanizeRate(rate)
|
||||
}
|
||||
@@ -3,13 +3,127 @@ package crack
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type crackJobCompletionEntry struct {
|
||||
id string
|
||||
description string
|
||||
}
|
||||
|
||||
// CrackJobIDCompleter completes durable crack job IDs.
|
||||
//
|
||||
//nolint:revive // Keep the established exported completer name for compatibility.
|
||||
func CrackJobIDCompleter(con *console.SliverClient) carapace.Action {
|
||||
return crackJobIDCompleter(con, nil)
|
||||
}
|
||||
|
||||
func crackTerminalJobIDCompleter(con *console.SliverClient) carapace.Action {
|
||||
return crackJobIDCompleter(con, crackJobTerminal)
|
||||
}
|
||||
|
||||
func crackJobPausable(job *clientpb.CrackJob) bool {
|
||||
return job != nil && job.GetStatus() == clientpb.CrackJobStatus_IN_PROGRESS
|
||||
}
|
||||
|
||||
func crackJobResumable(job *clientpb.CrackJob) bool {
|
||||
return job != nil && job.GetStatus() == clientpb.CrackJobStatus_PAUSED
|
||||
}
|
||||
|
||||
func crackJobCancellable(job *clientpb.CrackJob) bool {
|
||||
return crackJobPausable(job) || crackJobResumable(job)
|
||||
}
|
||||
|
||||
func crackJobIDCompleter(con *console.SliverClient, include func(*clientpb.CrackJob) bool) carapace.Action {
|
||||
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
|
||||
entries, err := crackJobCompletionEntriesFiltered(con, include)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch crack jobs: %s", err.Error())
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(entries)*2)
|
||||
for _, entry := range entries {
|
||||
values = append(values, entry.id, entry.description)
|
||||
}
|
||||
return carapace.ActionValuesDescribed(values...).Tag("crack jobs")
|
||||
})
|
||||
}
|
||||
|
||||
func registerCrackJobIDCompletion(cmd *cobra.Command, con *console.SliverClient) {
|
||||
registerCrackJobIDCompletionFiltered(cmd, con, nil)
|
||||
}
|
||||
|
||||
func registerCrackTerminalJobIDCompletion(cmd *cobra.Command, con *console.SliverClient) {
|
||||
registerCrackJobIDCompletionFiltered(cmd, con, crackJobTerminal)
|
||||
}
|
||||
|
||||
func registerCrackJobIDCompletionFiltered(cmd *cobra.Command, con *console.SliverClient, include func(*clientpb.CrackJob) bool) {
|
||||
if cmd == nil || cmd.ValidArgsFunction != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) > 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
entries, err := crackJobCompletionEntriesFiltered(con, include)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
values := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.id, toComplete) {
|
||||
values = append(values, fmt.Sprintf("%s\t%s", entry.id, entry.description))
|
||||
}
|
||||
}
|
||||
return values, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
func crackJobCompletionEntries(con *console.SliverClient) ([]crackJobCompletionEntry, error) {
|
||||
return crackJobCompletionEntriesFiltered(con, nil)
|
||||
}
|
||||
|
||||
func crackJobCompletionEntriesFiltered(con *console.SliverClient, include func(*clientpb.CrackJob) bool) ([]crackJobCompletionEntry, error) {
|
||||
if con == nil || con.Rpc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
jobs, err := con.Rpc.CrackJobs(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if jobs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
entries := make([]crackJobCompletionEntry, 0, len(jobs.GetJobs()))
|
||||
for _, job := range jobs.GetJobs() {
|
||||
if job == nil || strings.TrimSpace(job.GetID()) == "" || (include != nil && !include(job)) {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, crackJobCompletionEntry{
|
||||
id: job.GetID(),
|
||||
description: fmt.Sprintf(
|
||||
"%s, created %s, keyspace %s, %d task(s), %d result(s)",
|
||||
safeCrackCell(job.GetStatus().String()),
|
||||
formatCrackTime(job.GetCreatedAt()),
|
||||
valueOrDash(job.GetKeyspace()),
|
||||
len(job.GetTasks()),
|
||||
job.GetResultCount(),
|
||||
),
|
||||
})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
hcstat2, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_MARKOV_HCSTAT2})
|
||||
@@ -35,14 +149,14 @@ func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
|
||||
|
||||
func CrackWordlistCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
hcstat2, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_MARKOV_HCSTAT2})
|
||||
wordlists, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_WORDLIST})
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch crack files: %s", err.Error())
|
||||
}
|
||||
|
||||
results := make([]string, 0)
|
||||
|
||||
for _, file := range hcstat2.Files {
|
||||
for _, file := range wordlists.Files {
|
||||
if file.Type != clientpb.CrackFileType_WORDLIST {
|
||||
continue
|
||||
}
|
||||
@@ -59,14 +173,14 @@ func CrackWordlistCompleter(con *console.SliverClient) carapace.Action {
|
||||
|
||||
func CrackRulesCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
hcstat2, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_MARKOV_HCSTAT2})
|
||||
rules, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_RULES})
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch crack files: %s", err.Error())
|
||||
}
|
||||
|
||||
results := make([]string, 0)
|
||||
|
||||
for _, file := range hcstat2.Files {
|
||||
for _, file := range rules.Files {
|
||||
if file.Type != clientpb.CrackFileType_RULES {
|
||||
continue
|
||||
}
|
||||
@@ -77,7 +191,7 @@ func CrackRulesCompleter(con *console.SliverClient) carapace.Action {
|
||||
results = append(results, desc)
|
||||
}
|
||||
|
||||
return carapace.ActionValuesDescribed(results...).Tag("wordlists")
|
||||
return carapace.ActionValuesDescribed(results...).Tag("rules")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type crackFilesListCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
requests []clientpb.CrackFileType
|
||||
}
|
||||
|
||||
type crackJobsListCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
jobs *clientpb.CrackJobs
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (capture *crackJobsListCapture) CrackJobs(_ context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.CrackJobs, error) {
|
||||
capture.calls++
|
||||
return capture.jobs, capture.err
|
||||
}
|
||||
|
||||
func (capture *crackFilesListCapture) CrackFilesList(_ context.Context, request *clientpb.CrackFile, _ ...grpc.CallOption) (*clientpb.CrackFiles, error) {
|
||||
capture.requests = append(capture.requests, request.GetType())
|
||||
return &clientpb.CrackFiles{}, nil
|
||||
}
|
||||
|
||||
func TestCrackFileCompletersRequestTheirOwnFileType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want clientpb.CrackFileType
|
||||
action func(*console.SliverClient) carapace.Action
|
||||
}{
|
||||
{name: "wordlists", want: clientpb.CrackFileType_WORDLIST, action: CrackWordlistCompleter},
|
||||
{name: "rules", want: clientpb.CrackFileType_RULES, action: CrackRulesCompleter},
|
||||
{name: "hcstat2", want: clientpb.CrackFileType_MARKOV_HCSTAT2, action: CrackHcstat2Completer},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
capture := &crackFilesListCapture{}
|
||||
client := &console.SliverClient{Rpc: capture}
|
||||
_ = test.action(client).Invoke(carapace.NewContext(""))
|
||||
if len(capture.requests) != 1 || capture.requests[0] != test.want {
|
||||
t.Fatalf("completion requests = %v, want [%s]", capture.requests, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobCompleterRequestsDurableJobs(t *testing.T) {
|
||||
capture := &crackJobsListCapture{jobs: &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{{
|
||||
ID: "12345678-1234-1234-1234-123456789abc",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
CreatedAt: "2026-09-11T12:00:00Z",
|
||||
Keyspace: "1000",
|
||||
Tasks: []*clientpb.CrackTask{{ID: "task-id"}},
|
||||
ResultCount: 2,
|
||||
}}}}
|
||||
client := &console.SliverClient{Rpc: capture}
|
||||
_ = CrackJobIDCompleter(client).Invoke(carapace.NewContext(""))
|
||||
if capture.calls != 1 {
|
||||
t.Fatalf("CrackJobs calls = %d, want 1", capture.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobCobraCompletionUsesFullIDsAndFiltersPrefix(t *testing.T) {
|
||||
firstID := "12345678-1234-1234-1234-123456789abc"
|
||||
secondID := "abcdefab-cdef-cdef-cdef-abcdefabcdef"
|
||||
capture := &crackJobsListCapture{jobs: &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{
|
||||
{
|
||||
ID: firstID,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
CreatedAt: "2026-09-11T12:00:00Z",
|
||||
Keyspace: "1000",
|
||||
Tasks: []*clientpb.CrackTask{{ID: "task-id"}},
|
||||
ResultCount: 2,
|
||||
},
|
||||
{ID: secondID, Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
nil,
|
||||
{},
|
||||
}}}
|
||||
client := &console.SliverClient{Rpc: capture}
|
||||
root := Commands(client)[0]
|
||||
job, _, err := root.Find([]string{"job"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job: %v", err)
|
||||
}
|
||||
|
||||
values, directive := job.ValidArgsFunction(job, nil, "1234")
|
||||
if directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("completion directive = %v, want no-file-completion", directive)
|
||||
}
|
||||
if len(values) != 1 {
|
||||
t.Fatalf("completion values = %#v, want one filtered result", values)
|
||||
}
|
||||
if !strings.HasPrefix(values[0], firstID+"\t") {
|
||||
t.Fatalf("completion = %q, want full ID %q", values[0], firstID)
|
||||
}
|
||||
for _, want := range []string{"IN_PROGRESS", "2026-09-11T12:00:00Z", "keyspace 1000", "1 task(s)", "2 result(s)"} {
|
||||
if !strings.Contains(values[0], want) {
|
||||
t.Errorf("completion %q does not contain %q", values[0], want)
|
||||
}
|
||||
}
|
||||
|
||||
values, directive = job.ValidArgsFunction(job, []string{firstID}, "")
|
||||
if len(values) != 0 || directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("second positional completion = %#v, %v", values, directive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobCompletionHandlesRPCFailuresAndNilData(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
capture *crackJobsListCapture
|
||||
}{
|
||||
{name: "rpc error", capture: &crackJobsListCapture{err: errors.New("rpc failed")}},
|
||||
{name: "nil response", capture: &crackJobsListCapture{}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
client := &console.SliverClient{Rpc: test.capture}
|
||||
root := Commands(client)[0]
|
||||
job, _, err := root.Find([]string{"job"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job: %v", err)
|
||||
}
|
||||
values, directive := job.ValidArgsFunction(job, nil, "")
|
||||
if len(values) != 0 || directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("completion = %#v, %v", values, directive)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// Package hashcatdisplay provides shared Hashcat presentation helpers for crack commands.
|
||||
package hashcatdisplay
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Hashcat mode names sourced from client/credentials/EXAMPLES.md.
|
||||
var hashcatHashTypes = map[int32]string{
|
||||
0: "MD5",
|
||||
10: "md5($pass.$salt)",
|
||||
11: "Joomla < 2.5.18",
|
||||
12: "PostgreSQL",
|
||||
20: "md5($salt.$pass)",
|
||||
21: "osCommerce, xt:Commerce",
|
||||
22: "Juniper NetScreen/SSG (ScreenOS)",
|
||||
23: "Skype",
|
||||
24: "SolarWinds Serv-U",
|
||||
30: "md5(utf16le($pass).$salt)",
|
||||
40: "md5($salt.utf16le($pass))",
|
||||
50: "HMAC-MD5 (key = $pass)",
|
||||
60: "HMAC-MD5 (key = $salt)",
|
||||
70: "md5(utf16le($pass))",
|
||||
100: "SHA1",
|
||||
101: "nsldap, SHA-1(Base64), Netscape LDAP SHA",
|
||||
110: "sha1($pass.$salt)",
|
||||
111: "nsldaps, SSHA-1(Base64), Netscape LDAP SSHA",
|
||||
112: "Oracle S: Type (Oracle 11+)",
|
||||
120: "sha1($salt.$pass)",
|
||||
121: "SMF (Simple Machines Forum) > v1.1",
|
||||
122: "macOS v10.4, macOS v10.5, macOS v10.6",
|
||||
123: "EPi",
|
||||
124: "Django (SHA-1)",
|
||||
125: "ArubaOS",
|
||||
130: "sha1(utf16le($pass).$salt)",
|
||||
131: "MSSQL (2000)",
|
||||
132: "MSSQL (2005)",
|
||||
133: "PeopleSoft",
|
||||
140: "sha1($salt.utf16le($pass))",
|
||||
141: "Episerver 6.x < .NET 4",
|
||||
150: "HMAC-SHA1 (key = $pass)",
|
||||
160: "HMAC-SHA1 (key = $salt)",
|
||||
170: "sha1(utf16le($pass))",
|
||||
190: "sha1(LinkedIn)",
|
||||
200: "MySQL323",
|
||||
300: "MySQL4.1/MySQL5",
|
||||
400: "phpass, phpBB3 (MD5)",
|
||||
500: "md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)",
|
||||
501: "Juniper IVE",
|
||||
600: "BLAKE2b-512",
|
||||
900: "MD4",
|
||||
1000: "NTLM",
|
||||
1100: "Domain Cached Credentials (DCC), MS Cache",
|
||||
1300: "SHA2-224",
|
||||
1400: "SHA2-256",
|
||||
1410: "sha256($pass.$salt)",
|
||||
1411: "SSHA-256(Base64), LDAP {SSHA256}",
|
||||
1420: "sha256($salt.$pass)",
|
||||
1421: "hMailServer",
|
||||
1430: "sha256(utf16le($pass).$salt)",
|
||||
1431: "base64(sha256(unicode($pass)))",
|
||||
1440: "sha256($salt.utf16le($pass))",
|
||||
1441: "Episerver 6.x >= .NET 4",
|
||||
1450: "HMAC-SHA256 (key = $pass)",
|
||||
1460: "HMAC-SHA256 (key = $salt)",
|
||||
1470: "sha256(utf16le($pass))",
|
||||
1500: "descrypt, DES (Unix), Traditional DES",
|
||||
1600: "Apache $apr1$ MD5, md5apr1, MD5 (APR)",
|
||||
1700: "SHA2-512",
|
||||
1710: "sha512($pass.$salt)",
|
||||
1711: "SSHA-512(Base64), LDAP {SSHA512}",
|
||||
1720: "sha512($salt.$pass)",
|
||||
1722: "macOS v10.7",
|
||||
1730: "sha512(utf16le($pass).$salt)",
|
||||
1731: "MSSQL (2012, 2014)",
|
||||
1740: "sha512($salt.utf16le($pass))",
|
||||
1750: "HMAC-SHA512 (key = $pass)",
|
||||
1760: "HMAC-SHA512 (key = $salt)",
|
||||
1770: "sha512(utf16le($pass))",
|
||||
1800: "sha512crypt $6$, SHA512 (Unix)",
|
||||
2000: "STDOUT",
|
||||
2100: "Domain Cached Credentials 2 (DCC2), MS Cache 2",
|
||||
2400: "Cisco-PIX MD5",
|
||||
2410: "Cisco-ASA MD5",
|
||||
2500: "WPA-EAPOL-PBKDF2",
|
||||
2501: "WPA-EAPOL-PMK",
|
||||
2600: "md5(md5($pass))",
|
||||
2611: "vBulletin < v3.8.5",
|
||||
2612: "PHPS",
|
||||
2711: "vBulletin >= v3.8.5",
|
||||
2811: "MyBB 1.2+, IPB2+ (Invision Power Board)",
|
||||
3000: "LM",
|
||||
3100: "Oracle H: Type (Oracle 7+)",
|
||||
3200: "bcrypt $2\\*$, Blowfish (Unix)",
|
||||
3300: "MD5(Sun)",
|
||||
3500: "md5(md5(md5($pass)))",
|
||||
3610: "md5(md5($salt).$pass )",
|
||||
3710: "md5($salt.md5($pass))",
|
||||
3711: "MediaWiki B type",
|
||||
3720: "md5($pass.md5($salt))",
|
||||
3721: "WebEdition CMS",
|
||||
3800: "md5($salt.$pass.$salt)",
|
||||
3910: "md5(md5($pass).md5($salt))",
|
||||
4010: "md5($salt.md5($salt.$pass))",
|
||||
4110: "md5($salt.md5($pass.$salt))",
|
||||
4210: "md5($username.0.$pass)",
|
||||
4300: "md5(strtoupper(md5($pass)))",
|
||||
4400: "md5(sha1($pass))",
|
||||
4500: "sha1(sha1($pass))",
|
||||
4510: "sha1(sha1($pass).$salt)",
|
||||
4520: "sha1($salt.sha1($pass))",
|
||||
4521: "Redmine",
|
||||
4522: "PunBB",
|
||||
4600: "sha1(sha1(sha1($pass)))",
|
||||
4700: "sha1(md5($pass))",
|
||||
4710: "sha1(md5($pass).$salt)",
|
||||
4711: "Huawei sha1(md5($pass).$salt)",
|
||||
4800: "iSCSI CHAP authentication, MD5(CHAP)",
|
||||
4900: "sha1($salt.$pass.$salt)",
|
||||
5000: "sha1(sha1($salt.$pass.$salt))",
|
||||
5100: "Half MD5",
|
||||
5200: "Password Safe v3",
|
||||
5300: "IKE-PSK MD5",
|
||||
5400: "IKE-PSK SHA1",
|
||||
5500: "NetNTLMv1 / NetNTLMv1+ESS",
|
||||
5600: "NetNTLMv2",
|
||||
5700: "Cisco-IOS type 4 (SHA256)",
|
||||
5800: "Samsung Android Password/PIN",
|
||||
6000: "RIPEMD-160",
|
||||
6100: "Whirlpool",
|
||||
6211: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish",
|
||||
6212: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-AES / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish-Serpent",
|
||||
6213: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish-Serpent / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-Twofish-AES",
|
||||
6221: "TrueCrypt 5.0+ SHA512 + AES / TrueCrypt 5.0+ SHA512 + Serpent / TrueCrypt 5.0+ SHA512 + Twofish",
|
||||
6222: "TrueCrypt 5.0+ SHA512 + AES-Twofish / TrueCrypt 5.0+ SHA512 + Serpent-AES / TrueCrypt 5.0+ SHA512 + Twofish-Serpent",
|
||||
6223: "TrueCrypt 5.0+ SHA512 + AES-Twofish-Serpent / TrueCrypt 5.0+ SHA512 + Serpent-Twofish-AES",
|
||||
6231: "TrueCrypt 5.0+ Whirlpool + AES / TrueCrypt 5.0+ Whirlpool + Serpent / TrueCrypt 5.0+ Whirlpool + Twofish",
|
||||
6232: "TrueCrypt 5.0+ Whirlpool + AES-Twofish / TrueCrypt 5.0+ Whirlpool + Serpent-AES / TrueCrypt 5.0+ Whirlpool + Twofish-Serpent",
|
||||
6233: "TrueCrypt 5.0+ Whirlpool + AES-Twofish-Serpent / TrueCrypt 5.0+ Whirlpool + Serpent-Twofish-AES",
|
||||
6241: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish + boot",
|
||||
6242: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-AES + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish-Serpent + boot",
|
||||
6243: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish-Serpent + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-Twofish-AES + boot",
|
||||
6300: "AIX {smd5}",
|
||||
6400: "AIX {ssha256}",
|
||||
6500: "AIX {ssha512}",
|
||||
6600: "1Password, agilekeychain",
|
||||
6700: "AIX {ssha1}",
|
||||
6800: "LastPass + LastPass sniffed",
|
||||
6900: "GOST R 34.11-94",
|
||||
7000: "FortiGate (FortiOS)",
|
||||
7100: "macOS v10.8+ (PBKDF2-SHA512)",
|
||||
7200: "GRUB 2",
|
||||
7300: "IPMI2 RAKP HMAC-SHA1",
|
||||
7400: "sha256crypt $5$, SHA256 (Unix)",
|
||||
7401: "MySQL $A$ (sha256crypt)",
|
||||
7500: "Kerberos 5, etype 23, AS-REQ Pre-Auth",
|
||||
7700: "SAP CODVN B (BCODE)",
|
||||
7701: "SAP CODVN B (BCODE) from RFC_READ_TABLE",
|
||||
7800: "SAP CODVN F/G (PASSCODE)",
|
||||
7801: "SAP CODVN F/G (PASSCODE) from RFC_READ_TABLE",
|
||||
7900: "Drupal7",
|
||||
8000: "Sybase ASE",
|
||||
8100: "Citrix NetScaler (SHA1)",
|
||||
8200: "1Password, cloudkeychain",
|
||||
8300: "DNSSEC (NSEC3)",
|
||||
8400: "WBB3 (Woltlab Burning Board)",
|
||||
8500: "RACF",
|
||||
8600: "Lotus Notes/Domino 5",
|
||||
8700: "Lotus Notes/Domino 6",
|
||||
8800: "Android FDE <= 4.3",
|
||||
8900: "scrypt",
|
||||
9000: "Password Safe v2",
|
||||
9100: "Lotus Notes/Domino 8",
|
||||
9200: "Cisco-IOS $8$ (PBKDF2-SHA256)",
|
||||
9300: "Cisco-IOS $9$ (scrypt)",
|
||||
9400: "MS Office 2007",
|
||||
9500: "MS Office 2010",
|
||||
9600: "MS Office 2013",
|
||||
9700: "MS Office <= 2003 MD5 + RC4, oldoffice$0, oldoffice$1",
|
||||
9710: "MS Office <= 2003 $0/$1, MD5 + RC4, collider #1",
|
||||
9720: "MS Office <= 2003 $0/$1, MD5 + RC4, collider #2",
|
||||
9800: "MS Office <= 2003 SHA1 + RC4, oldoffice$3, oldoffice$4",
|
||||
9810: "MS Office <= 2003 $3, SHA1 + RC4, collider #1",
|
||||
9820: "MS Office <= 2003 $3, SHA1 + RC4, collider #2",
|
||||
9900: "Radmin2",
|
||||
10000: "Django (PBKDF2-SHA256)",
|
||||
10100: "SipHash",
|
||||
10200: "CRAM-MD5",
|
||||
10300: "SAP CODVN H (PWDSALTEDHASH) iSSHA-1",
|
||||
10400: "PDF 1.1 - 1.3 (Acrobat 2 - 4)",
|
||||
10410: "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #1",
|
||||
10420: "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #2",
|
||||
10500: "PDF 1.4 - 1.6 (Acrobat 5 - 8)",
|
||||
10600: "PDF 1.7 Level 3 (Acrobat 9)",
|
||||
10700: "PDF 1.7 Level 8 (Acrobat 10 - 11)",
|
||||
10800: "SHA2-384",
|
||||
10810: "sha384($pass.$salt)",
|
||||
10820: "sha384($salt.$pass)",
|
||||
10830: "sha384(utf16le($pass).$salt)",
|
||||
10840: "sha384($salt.utf16le($pass))",
|
||||
10870: "sha384(utf16le($pass))",
|
||||
10900: "PBKDF2-HMAC-SHA256",
|
||||
10901: "RedHat 389-DS LDAP (PBKDF2-HMAC-SHA256)",
|
||||
11000: "PrestaShop",
|
||||
11100: "PostgreSQL CRAM (MD5)",
|
||||
11200: "MySQL CRAM (SHA1)",
|
||||
11300: "Bitcoin/Litecoin wallet.dat",
|
||||
11400: "SIP digest authentication (MD5)",
|
||||
11500: "CRC32",
|
||||
11600: "7-Zip",
|
||||
11700: "GOST R 34.11-2012 (Streebog) 256-bit, big-endian",
|
||||
11750: "HMAC-Streebog-256 (key = $pass), big-endian",
|
||||
11760: "HMAC-Streebog-256 (key = $salt), big-endian",
|
||||
11800: "GOST R 34.11-2012 (Streebog) 512-bit, big-endian",
|
||||
11850: "HMAC-Streebog-512 (key = $pass), big-endian",
|
||||
11860: "HMAC-Streebog-512 (key = $salt), big-endian",
|
||||
11900: "PBKDF2-HMAC-MD5",
|
||||
12000: "PBKDF2-HMAC-SHA1",
|
||||
12001: "Atlassian (PBKDF2-HMAC-SHA1)",
|
||||
12100: "PBKDF2-HMAC-SHA512",
|
||||
12200: "eCryptfs",
|
||||
12300: "Oracle T: Type (Oracle 12+)",
|
||||
12400: "BSDi Crypt, Extended DES",
|
||||
12500: "RAR3-hp",
|
||||
12600: "ColdFusion 10+",
|
||||
12700: "Blockchain, My Wallet",
|
||||
12800: "MS-AzureSync PBKDF2-HMAC-SHA256",
|
||||
12900: "Android FDE (Samsung DEK)",
|
||||
13000: "RAR5",
|
||||
13100: "Kerberos 5, etype 23, TGS-REP",
|
||||
13200: "AxCrypt 1",
|
||||
13300: "AxCrypt 1 in-memory SHA1",
|
||||
13400: "KeePass 1 AES / without keyfile / KeePass 2 AES / without keyfile / KeePass 1 Twofish / with keyfile / Keepass 2 AES / with keyfile",
|
||||
13500: "PeopleSoft PS_TOKEN",
|
||||
13600: "WinZip",
|
||||
13711: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + AES / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Serpent / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Twofish",
|
||||
13712: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + AES-Twofish / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Serpent-AES / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Twofish-Serpent",
|
||||
13713: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + Serpent-Twofish-AES",
|
||||
13721: "VeraCrypt PBKDF2-HMAC-SHA512 + AES / VeraCrypt PBKDF2-HMAC-SHA512 + Serpent / VeraCrypt PBKDF2-HMAC-SHA512 + Twofish",
|
||||
13722: "VeraCrypt PBKDF2-HMAC-SHA512 + AES-Twofish / VeraCrypt PBKDF2-HMAC-SHA512 + Serpent-AES / VeraCrypt PBKDF2-HMAC-SHA512 + Twofish-Serpent",
|
||||
13723: "VeraCrypt PBKDF2-HMAC-SHA512 + Serpent-Twofish-AES",
|
||||
13731: "VeraCrypt PBKDF2-HMAC-Whirlpool + AES / VeraCrypt PBKDF2-HMAC-Whirlpool + Serpent / VeraCrypt PBKDF2-HMAC-Whirlpool + Twofish",
|
||||
13732: "VeraCrypt PBKDF2-HMAC-Whirlpool + AES-Twofish / VeraCrypt PBKDF2-HMAC-Whirlpool + Serpent-AES / VeraCrypt PBKDF2-HMAC-Whirlpool + Twofish-Serpent",
|
||||
13733: "VeraCrypt PBKDF2-HMAC-Whirlpool + Serpent-Twofish-AES",
|
||||
13741: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + boot-mode + AES",
|
||||
13742: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + boot-mode + AES-Twofish",
|
||||
13743: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + boot-mode + AES-Twofish-Serpent",
|
||||
13751: "VeraCrypt PBKDF2-HMAC-SHA256 + AES / VeraCrypt PBKDF2-HMAC-SHA256 + Serpent / VeraCrypt PBKDF2-HMAC-SHA256 + Twofish",
|
||||
13752: "VeraCrypt PBKDF2-HMAC-SHA256 + AES-Twofish / VeraCrypt PBKDF2-HMAC-SHA256 + Serpent-AES / VeraCrypt PBKDF2-HMAC-SHA256 + Twofish-Serpent",
|
||||
13753: "VeraCrypt PBKDF2-HMAC-SHA256 + Serpent-Twofish-AES",
|
||||
13761: "VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + Twofish / VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + PIM + AES",
|
||||
13762: "VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + Serpent-AES",
|
||||
13763: "VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + Serpent-Twofish-AES",
|
||||
13771: "VeraCrypt Streebog-512 + XTS 512 bit",
|
||||
13772: "VeraCrypt Streebog-512 + XTS 1024 bit",
|
||||
13773: "VeraCrypt Streebog-512 + XTS 1536 bit",
|
||||
13800: "Windows Phone 8+ PIN/password",
|
||||
13900: "OpenCart",
|
||||
14000: "DES (PT = $salt, key = $pass)",
|
||||
14100: "3DES (PT = $salt, key = $pass)",
|
||||
14400: "sha1(CX)",
|
||||
14500: "Linux Kernel Crypto API (2.4)",
|
||||
14600: "LUKS",
|
||||
14700: "iTunes backup < 10.0",
|
||||
14800: "iTunes backup >= 10.0",
|
||||
14900: "Skip32 (PT = $salt, key = $pass)",
|
||||
15000: "FileZilla Server >= 0.9.55",
|
||||
15100: "Juniper/NetBSD sha1crypt",
|
||||
15200: "Blockchain, My Wallet, V2",
|
||||
15300: "DPAPI masterkey file v1 + local context",
|
||||
15400: "ChaCha20",
|
||||
15500: "JKS Java Key Store Private Keys (SHA1)",
|
||||
15600: "Ethereum Wallet, PBKDF2-HMAC-SHA256",
|
||||
15700: "Ethereum Wallet, SCRYPT",
|
||||
15900: "DPAPI masterkey file v2 + Active Directory domain context",
|
||||
16000: "Tripcode",
|
||||
16100: "TACACS+",
|
||||
16200: "Apple Secure Notes",
|
||||
16300: "Ethereum Pre-Sale Wallet, PBKDF2-HMAC-SHA256",
|
||||
16400: "CRAM-MD5 Dovecot",
|
||||
16500: "JWT (JSON Web Token)",
|
||||
16600: "Electrum Wallet (Salt-Type 1-3)",
|
||||
16700: "FileVault 2",
|
||||
16800: "WPA-PMKID-PBKDF2",
|
||||
16801: "WPA-PMKID-PMK",
|
||||
16900: "Ansible Vault",
|
||||
17200: "PKZIP (Compressed)",
|
||||
17210: "PKZIP (Uncompressed)",
|
||||
17220: "PKZIP (Compressed Multi-File)",
|
||||
17225: "PKZIP (Mixed Multi-File)",
|
||||
17230: "PKZIP (Mixed Multi-File Checksum-Only)",
|
||||
17300: "SHA3-224",
|
||||
17400: "SHA3-256",
|
||||
17500: "SHA3-384",
|
||||
17600: "SHA3-512",
|
||||
17700: "Keccak-224",
|
||||
17800: "Keccak-256",
|
||||
17900: "Keccak-384",
|
||||
18000: "Keccak-512",
|
||||
18100: "TOTP (HMAC-SHA1)",
|
||||
18200: "Kerberos 5, etype 23, AS-REP",
|
||||
18300: "Apple File System (APFS)",
|
||||
18400: "Open Document Format (ODF) 1.2 (SHA-256, AES)",
|
||||
18500: "sha1(md5(md5($pass)))",
|
||||
18600: "Open Document Format (ODF) 1.1 (SHA-1, Blowfish)",
|
||||
18700: "Java Object hashCode()",
|
||||
18800: "Blockchain, My Wallet, Second Password (SHA256)",
|
||||
18900: "Android Backup",
|
||||
19000: "QNX /etc/shadow (MD5)",
|
||||
19100: "QNX /etc/shadow (SHA256)",
|
||||
19200: "QNX /etc/shadow (SHA512)",
|
||||
19300: "sha1($salt1.$pass.$salt2)",
|
||||
19500: "Ruby on Rails Restful-Authentication",
|
||||
19600: "Kerberos 5, etype 17, TGS-REP (AES128-CTS-HMAC-SHA1-96)",
|
||||
19700: "Kerberos 5, etype 18, TGS-REP (AES256-CTS-HMAC-SHA1-96)",
|
||||
19800: "Kerberos 5, etype 17, Pre-Auth",
|
||||
19900: "Kerberos 5, etype 18, Pre-Auth",
|
||||
20011: "DiskCryptor SHA512 + XTS 512 bit (AES) / DiskCryptor SHA512 + XTS 512 bit (Twofish) / DiskCryptor SHA512 + XTS 512 bit (Serpent)",
|
||||
20012: "DiskCryptor SHA512 + XTS 1024 bit (AES-Twofish) / DiskCryptor SHA512 + XTS 1024 bit (Twofish-Serpent) / DiskCryptor SHA512 + XTS 1024 bit (Serpent-AES)",
|
||||
20013: "DiskCryptor SHA512 + XTS 1536 bit (AES-Twofish-Serpent)",
|
||||
20200: "Python passlib pbkdf2-sha512",
|
||||
20300: "Python passlib pbkdf2-sha256",
|
||||
20400: "Python passlib pbkdf2-sha1",
|
||||
20500: "PKZIP Master Key",
|
||||
20510: "PKZIP Master Key (6 byte optimization)",
|
||||
20600: "Oracle Transportation Management (SHA256)",
|
||||
20710: "sha256(sha256($pass).$salt)",
|
||||
20711: "AuthMe sha256",
|
||||
20720: "sha256($salt.sha256($pass))",
|
||||
20800: "sha256(md5($pass))",
|
||||
20900: "md5(sha1($pass).md5($pass).sha1($pass))",
|
||||
21000: "BitShares v0.x - sha512(sha512_bin(pass))",
|
||||
21100: "sha1(md5($pass.$salt))",
|
||||
21200: "md5(sha1($salt).md5($pass))",
|
||||
21300: "md5($salt.sha1($salt.$pass))",
|
||||
21400: "sha256(sha256_bin($pass))",
|
||||
21500: "SolarWinds Orion",
|
||||
21501: "SolarWinds Orion v2",
|
||||
21600: "Web2py pbkdf2-sha512",
|
||||
21700: "Electrum Wallet (Salt-Type 4)",
|
||||
21800: "Electrum Wallet (Salt-Type 5)",
|
||||
22000: "WPA-PBKDF2-PMKID+EAPOL",
|
||||
22001: "WPA-PMK-PMKID+EAPOL",
|
||||
22100: "BitLocker",
|
||||
22200: "Citrix NetScaler (SHA512)",
|
||||
22300: "sha256($salt.$pass.$salt)",
|
||||
22301: "Telegram Mobile App Passcode (SHA256)",
|
||||
22400: "AES Crypt (SHA256)",
|
||||
22500: "MultiBit Classic .key (MD5)",
|
||||
22600: "Telegram Desktop < v2.1.14 (PBKDF2-HMAC-SHA1)",
|
||||
22700: "MultiBit HD (scrypt)",
|
||||
22911: "RSA/DSA/EC/OpenSSH Private Keys ($0$)",
|
||||
22921: "RSA/DSA/EC/OpenSSH Private Keys ($6$)",
|
||||
22931: "RSA/DSA/EC/OpenSSH Private Keys ($1, $3$)",
|
||||
22941: "RSA/DSA/EC/OpenSSH Private Keys ($4$)",
|
||||
22951: "RSA/DSA/EC/OpenSSH Private Keys ($5$)",
|
||||
23001: "SecureZIP AES-128",
|
||||
23002: "SecureZIP AES-192",
|
||||
23003: "SecureZIP AES-256",
|
||||
23100: "Apple Keychain",
|
||||
23200: "XMPP SCRAM PBKDF2-SHA1",
|
||||
23300: "Apple iWork",
|
||||
23400: "Bitwarden",
|
||||
23500: "AxCrypt 2 AES-128",
|
||||
23600: "AxCrypt 2 AES-256",
|
||||
23700: "RAR3-p (Uncompressed)",
|
||||
23800: "RAR3-p (Compressed)",
|
||||
23900: "BestCrypt v3 Volume Encryption",
|
||||
24100: "MongoDB ServerKey SCRAM-SHA-1",
|
||||
24200: "MongoDB ServerKey SCRAM-SHA-256",
|
||||
24300: "sha1($salt.sha1($pass.$salt))",
|
||||
24410: "PKCS#8 Private Keys (PBKDF2-HMAC-SHA1 + 3DES/AES)",
|
||||
24420: "PKCS#8 Private Keys (PBKDF2-HMAC-SHA256 + 3DES/AES)",
|
||||
24500: "Telegram Desktop >= v2.1.14 (PBKDF2-HMAC-SHA512)",
|
||||
24600: "SQLCipher",
|
||||
24700: "Stuffit5",
|
||||
24800: "Umbraco HMAC-SHA1",
|
||||
24900: "Dahua Authentication MD5",
|
||||
25300: "MS Office 2016 - SheetProtection",
|
||||
25400: "PDF 1.4 - 1.6 (Acrobat 5 - 8) - edit password",
|
||||
25500: "Stargazer Stellar Wallet XLM",
|
||||
25600: "bcrypt(md5($pass)) / bcryptmd5",
|
||||
25700: "MurmurHash",
|
||||
25800: "bcrypt(sha1($pass)) / bcryptsha1",
|
||||
25900: "KNX IP Secure - Device Authentication Code",
|
||||
26000: "Mozilla key3.db",
|
||||
26100: "Mozilla key4.db",
|
||||
26200: "OpenEdge Progress Encode",
|
||||
26300: "FortiGate256 (FortiOS256)",
|
||||
26401: "AES-128-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
26402: "AES-192-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
26403: "AES-256-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
26500: "iPhone passcode (UID key + System Keybag)",
|
||||
26600: "MetaMask Wallet",
|
||||
99999: "Plaintext",
|
||||
}
|
||||
|
||||
// TypeName returns the display name for a Hashcat mode.
|
||||
func TypeName(mode int32) (string, bool) {
|
||||
name, ok := hashcatHashTypes[mode]
|
||||
return name, ok
|
||||
}
|
||||
|
||||
// HumanizeRate formats a hashes-per-second rate for terminal display.
|
||||
func HumanizeRate(rate uint64) string {
|
||||
const unit = 1000
|
||||
if rate < unit {
|
||||
return fmt.Sprintf("%d H/s", rate)
|
||||
}
|
||||
value := float64(rate)
|
||||
exp := 0
|
||||
for value >= unit && exp < 5 {
|
||||
value /= unit
|
||||
exp++
|
||||
}
|
||||
suffixes := []string{"kH/s", "MH/s", "GH/s", "TH/s", "PH/s"}
|
||||
if exp == 0 {
|
||||
return fmt.Sprintf("%d H/s", rate)
|
||||
}
|
||||
if exp > len(suffixes) {
|
||||
exp = len(suffixes)
|
||||
}
|
||||
return fmt.Sprintf("%.2f %s", value, suffixes[exp-1])
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
cracktop "github.com/bishopfox/sliver/client/command/crack/top"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CrackTopCmd launches the full-screen real-time crack cluster monitor.
|
||||
// The implementation lives in the top subpackage; this facade preserves the
|
||||
// existing command-handler API for callers of client/command/crack.
|
||||
//
|
||||
//nolint:revive // Preserve the established exported command-handler API.
|
||||
func CrackTopCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
cracktop.CrackTopCmd(cmd, con, args)
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
const crackTopActionTestJobID = "dddddddd-4444-4444-8444-444444444444"
|
||||
|
||||
func newCrackTopJobActionTestModel(
|
||||
status clientpb.CrackJobStatus,
|
||||
actions map[crackTopJobAction]crackTopJobActionFunc,
|
||||
) *crackTopModel {
|
||||
snapshot := &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{
|
||||
ID: crackTopActionTestJobID,
|
||||
Status: status,
|
||||
UpdatedAt: 100,
|
||||
Tasks: []*clientpb.CrackTask{{
|
||||
ID: "action-task",
|
||||
HostUUID: "action-station",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
LastHeartbeatAt: 1_700_000_000,
|
||||
}},
|
||||
}},
|
||||
Stations: []*clientpb.Crackstation{{
|
||||
ID: "action-station",
|
||||
HostUUID: "action-station",
|
||||
Status: &clientpb.CrackstationStatus{
|
||||
HostUUID: "action-station",
|
||||
State: clientpb.States_CRACKING,
|
||||
CurrentCrackJobID: crackTopActionTestJobID,
|
||||
},
|
||||
}},
|
||||
RefreshedAt: time.Unix(1_700_000_000, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{
|
||||
"action-task": {status: &crackStatusView{Devices: []crackDeviceView{{Speed: "100"}}}},
|
||||
},
|
||||
}
|
||||
model := newCrackTopModel(context.Background(), nil, nil, time.Second)
|
||||
model.jobActions = actions
|
||||
model.snapshot = snapshot
|
||||
model.dashboard = buildCrackTopDashboard(snapshot)
|
||||
model.refreshing = false
|
||||
model.width = 100
|
||||
model.height = 28
|
||||
model.normalizeJobSelection()
|
||||
return model
|
||||
}
|
||||
|
||||
func singleCrackTopAction(action crackTopJobAction, actionJob crackTopJobActionFunc) map[crackTopJobAction]crackTopJobActionFunc {
|
||||
return map[crackTopJobAction]crackTopJobActionFunc{action: actionJob}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteConfirmsAndRemovesTerminalJob(t *testing.T) {
|
||||
deletedID := ""
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
deletedID = jobID
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, openCommand := model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
if openCommand != nil || model.pendingJobAction != (crackTopPendingJobAction{action: crackTopJobActionDelete, jobID: crackTopActionTestJobID}) {
|
||||
t.Fatalf("delete prompt = command:%v prompt:%#v", openCommand != nil, model.pendingJobAction)
|
||||
}
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"Delete crack job?", crackTopActionTestJobID, "This cannot be undone", "y delete"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Fatalf("delete confirmation is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
if got := lipgloss.Width(model.View().Content); got > model.width {
|
||||
t.Fatalf("delete modal width = %d, want <= %d", got, model.width)
|
||||
}
|
||||
|
||||
_, deleteCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if deleteCommand == nil || model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != crackTopActionTestJobID {
|
||||
t.Fatalf("confirmed delete = command:%v pending:%#v running:%#v", deleteCommand != nil, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
rawMessage := deleteCommand()
|
||||
message, ok := rawMessage.(crackTopJobActionCompletedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("delete command returned %T, want crackTopJobActionCompletedMsg", rawMessage)
|
||||
}
|
||||
if deletedID != crackTopActionTestJobID || message.action != crackTopJobActionDelete || message.jobID != crackTopActionTestJobID || message.err != nil {
|
||||
t.Fatalf("delete call/message = called:%q message:%#v", deletedID, message)
|
||||
}
|
||||
|
||||
_, refreshCommand := model.Update(message)
|
||||
if refreshCommand == nil || !model.refreshing {
|
||||
t.Fatalf("successful delete refresh = command:%v refreshing:%v", refreshCommand != nil, model.refreshing)
|
||||
}
|
||||
if len(model.snapshot.Jobs) != 0 || len(model.dashboard.Jobs) != 0 || model.selectedJobID != "" {
|
||||
t.Fatalf("deleted job remains in dashboard: snapshot=%#v dashboard=%#v selected=%q", model.snapshot.Jobs, model.dashboard.Jobs, model.selectedJobID)
|
||||
}
|
||||
if _, ok := model.snapshot.taskTelemetry["action-task"]; ok {
|
||||
t.Fatal("deleted job telemetry remains cached")
|
||||
}
|
||||
if model.toastLevel != "success" || !strings.Contains(model.toast, "Deleted crack job dddddddd") {
|
||||
t.Fatalf("success toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteRejectsInProgressJob(t *testing.T) {
|
||||
deleteCalls := 0
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
deleteCalls++
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, command := model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
if command == nil {
|
||||
t.Fatal("in-progress delete did not schedule a warning")
|
||||
}
|
||||
if model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != "" || deleteCalls != 0 {
|
||||
t.Fatalf("in-progress delete changed state: pending=%#v running=%#v calls=%d", model.pendingJobAction, model.runningJobAction, deleteCalls)
|
||||
}
|
||||
if model.toastLevel != "warning" || !strings.Contains(model.toast, "Only completed, failed, or cancelled crack jobs can be deleted") {
|
||||
t.Fatalf("in-progress warning = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteFailureRetainsJobAndEscapeCancels(t *testing.T) {
|
||||
wantErr := errors.New("database is busy\ntry later")
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_FAILED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
return nil, wantErr
|
||||
}),
|
||||
)
|
||||
|
||||
_, _ = model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
_, cancelCommand := model.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
|
||||
if cancelCommand != nil || model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != "" {
|
||||
t.Fatalf("escaped delete prompt = command:%v pending=%#v running=%#v", cancelCommand != nil, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
|
||||
_, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyDelete})
|
||||
_, enterCommand := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
|
||||
if enterCommand != nil || model.pendingJobAction.jobID != crackTopActionTestJobID || model.runningJobAction.jobID != "" {
|
||||
t.Fatalf("bare enter confirmed destructive action: command=%v pending=%#v running=%#v", enterCommand != nil, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
_, deleteCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if deleteCommand == nil {
|
||||
t.Fatal("y did not confirm deletion")
|
||||
}
|
||||
message := deleteCommand().(crackTopJobActionCompletedMsg)
|
||||
_, toastCommand := model.Update(message)
|
||||
if toastCommand == nil || model.refreshing {
|
||||
t.Fatalf("failed delete = toast command:%v refreshing:%v", toastCommand != nil, model.refreshing)
|
||||
}
|
||||
if len(model.snapshot.Jobs) != 1 || model.snapshot.Jobs[0].GetID() != crackTopActionTestJobID {
|
||||
t.Fatalf("failed delete removed job: %#v", model.snapshot.Jobs)
|
||||
}
|
||||
if model.toastLevel != "error" || !strings.Contains(model.toast, "database is busy try later") || strings.Contains(model.toast, "\n") {
|
||||
t.Fatalf("failure toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopResizeBelowMinimumCancelsHiddenConfirmation(t *testing.T) {
|
||||
deleteCalls := 0
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
deleteCalls++
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, _ = model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
if model.pendingJobAction.jobID == "" {
|
||||
t.Fatal("delete confirmation did not open")
|
||||
}
|
||||
_, resizeCommand := model.Update(tea.WindowSizeMsg{Width: crackTopMinWidth - 1, Height: crackTopMinHeight})
|
||||
if resizeCommand != nil || model.pendingJobAction.jobID != "" {
|
||||
t.Fatalf("small resize left hidden confirmation actionable: command=%v pending=%#v", resizeCommand != nil, model.pendingJobAction)
|
||||
}
|
||||
_, confirmCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if confirmCommand != nil || model.runningJobAction.jobID != "" || deleteCalls != 0 {
|
||||
t.Fatalf("hidden confirmation executed: command=%v running=%#v calls=%d", confirmCommand != nil, model.runningJobAction, deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopBelowMinimumCannotOpenHiddenConfirmation(t *testing.T) {
|
||||
deleteCalls := 0
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
deleteCalls++
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
_, _ = model.Update(tea.WindowSizeMsg{Width: crackTopMinWidth, Height: crackTopMinHeight - 1})
|
||||
|
||||
_, openCommand := model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
_, confirmCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if openCommand != nil || confirmCommand != nil || model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != "" || deleteCalls != 0 {
|
||||
t.Fatalf("undersized dashboard accepted hidden delete: open=%v confirm=%v pending=%#v running=%#v calls=%d",
|
||||
openCommand != nil, confirmCommand != nil, model.pendingJobAction, model.runningJobAction, deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteAllowsEveryTerminalStatus(t *testing.T) {
|
||||
for _, status := range []clientpb.CrackJobStatus{
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
clientpb.CrackJobStatus_FAILED,
|
||||
clientpb.CrackJobStatus_CANCELLED,
|
||||
} {
|
||||
t.Run(status.String(), func(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
status,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) { return nil, nil }),
|
||||
)
|
||||
_, command := model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
if command != nil || model.pendingJobAction.jobID != crackTopActionTestJobID {
|
||||
t.Fatalf("terminal status %s did not open delete confirmation", status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteSuppressesStaleRefreshUntilServerOmitsJob(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) { return nil, nil }),
|
||||
)
|
||||
model.removeJob(crackTopActionTestJobID)
|
||||
|
||||
stale := &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{ID: crackTopActionTestJobID, Status: clientpb.CrackJobStatus_COMPLETED, Tasks: []*clientpb.CrackTask{{ID: "stale-task"}}}},
|
||||
RefreshedAt: time.Unix(1_700_000_001, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{"stale-task": {}},
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: stale})
|
||||
if len(model.snapshot.Jobs) != 0 || len(model.dashboard.Jobs) != 0 {
|
||||
t.Fatalf("stale refresh restored deleted job: snapshot=%#v dashboard=%#v", model.snapshot.Jobs, model.dashboard.Jobs)
|
||||
}
|
||||
if _, ok := model.snapshot.taskTelemetry["stale-task"]; ok {
|
||||
t.Fatal("stale refresh retained deleted job telemetry")
|
||||
}
|
||||
if _, ok := model.deletedJobIDs[crackTopActionTestJobID]; !ok {
|
||||
t.Fatal("stale refresh cleared the deletion tombstone")
|
||||
}
|
||||
|
||||
fresh := &crackTopSnapshot{RefreshedAt: time.Unix(1_700_000_002, 0)}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: fresh})
|
||||
if _, ok := model.deletedJobIDs[crackTopActionTestJobID]; ok {
|
||||
t.Fatal("server-confirmed refresh did not clear the deletion tombstone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopPauseConfirmsAppliesAuthoritativeStatusAndRefreshes(t *testing.T) {
|
||||
pausedID := ""
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
singleCrackTopAction(crackTopJobActionPause, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
pausedID = jobID
|
||||
return &clientpb.CrackJob{ID: jobID, Status: clientpb.CrackJobStatus_PAUSED, UpdatedAt: 200}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, openCommand := model.Update(tea.KeyPressMsg{Text: "p", Code: 'p'})
|
||||
if openCommand != nil || model.pendingJobAction != (crackTopPendingJobAction{action: crackTopJobActionPause, jobID: crackTopActionTestJobID}) {
|
||||
t.Fatalf("pause prompt = command:%v prompt:%#v", openCommand != nil, model.pendingJobAction)
|
||||
}
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"Pause crack job?", "release its current crackstation work", "may restart", "y pause"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Fatalf("pause confirmation is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
_, enterCommand := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
|
||||
if enterCommand != nil || model.runningJobAction.jobID != "" {
|
||||
t.Fatal("enter confirmed pause without an explicit y")
|
||||
}
|
||||
|
||||
_, pauseCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if pauseCommand == nil || model.runningJobAction.action != crackTopJobActionPause {
|
||||
t.Fatalf("pause confirmation = command:%v running:%#v", pauseCommand != nil, model.runningJobAction)
|
||||
}
|
||||
message := pauseCommand().(crackTopJobActionCompletedMsg)
|
||||
if pausedID != crackTopActionTestJobID || message.status != clientpb.CrackJobStatus_PAUSED || !message.hasStatus {
|
||||
t.Fatalf("pause call/message = called:%q message:%#v", pausedID, message)
|
||||
}
|
||||
_, refreshCommand := model.Update(message)
|
||||
if refreshCommand == nil || !model.refreshing {
|
||||
t.Fatalf("successful pause refresh = command:%v refreshing:%v", refreshCommand != nil, model.refreshing)
|
||||
}
|
||||
if got := model.snapshot.Jobs[0].GetStatus(); got != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("local pause status = %s, want PAUSED", got)
|
||||
}
|
||||
if model.dashboard.Jobs[0].RateKnown || model.dashboard.ActiveJobs != 0 {
|
||||
t.Fatalf("paused job retained active throughput: row=%#v active=%d", model.dashboard.Jobs[0], model.dashboard.ActiveJobs)
|
||||
}
|
||||
if model.toastLevel != "success" || !strings.Contains(model.toast, "Paused crack job dddddddd") {
|
||||
t.Fatalf("pause toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopCancelConfirmsActiveAndPausedJobs(t *testing.T) {
|
||||
for _, status := range []clientpb.CrackJobStatus{clientpb.CrackJobStatus_IN_PROGRESS, clientpb.CrackJobStatus_PAUSED} {
|
||||
t.Run(status.String(), func(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
status,
|
||||
singleCrackTopAction(crackTopJobActionCancel, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
return &clientpb.CrackJob{ID: jobID, Status: clientpb.CrackJobStatus_CANCELLED, UpdatedAt: 200}, nil
|
||||
}),
|
||||
)
|
||||
_, openCommand := model.Update(tea.KeyPressMsg{Text: "c", Code: 'c'})
|
||||
if openCommand != nil || model.pendingJobAction.action != crackTopJobActionCancel {
|
||||
t.Fatalf("cancel prompt = command:%v prompt:%#v", openCommand != nil, model.pendingJobAction)
|
||||
}
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"Cancel crack job?", "Cancelled jobs cannot be resumed", "y cancel"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Fatalf("cancel confirmation is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
_, cancelCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
message := cancelCommand().(crackTopJobActionCompletedMsg)
|
||||
_, refreshCommand := model.Update(message)
|
||||
if refreshCommand == nil || model.snapshot.Jobs[0].GetStatus() != clientpb.CrackJobStatus_CANCELLED {
|
||||
t.Fatalf("cancel result = command:%v job:%#v", refreshCommand != nil, model.snapshot.Jobs[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopResumePausedJobWithoutDestructiveConfirmation(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_PAUSED,
|
||||
singleCrackTopAction(crackTopJobActionResume, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
return &clientpb.CrackJob{ID: jobID, Status: clientpb.CrackJobStatus_IN_PROGRESS, UpdatedAt: 300}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, resumeCommand := model.Update(tea.KeyPressMsg{Text: "u", Code: 'u'})
|
||||
if resumeCommand == nil || model.pendingJobAction.jobID != "" || model.runningJobAction.action != crackTopJobActionResume {
|
||||
t.Fatalf("resume start = command:%v pending:%#v running:%#v", resumeCommand != nil, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
plain := strings.ToLower(ansi.Strip(model.View().Content))
|
||||
if !strings.Contains(plain, "resuming crack job dddddddd") {
|
||||
t.Fatalf("resume progress footer missing:\n%s", plain)
|
||||
}
|
||||
message := resumeCommand().(crackTopJobActionCompletedMsg)
|
||||
_, refreshCommand := model.Update(message)
|
||||
if refreshCommand == nil || model.snapshot.Jobs[0].GetStatus() != clientpb.CrackJobStatus_IN_PROGRESS {
|
||||
t.Fatalf("resume result = command:%v job:%#v", refreshCommand != nil, model.snapshot.Jobs[0])
|
||||
}
|
||||
if model.toastLevel != "success" || !strings.Contains(model.toast, "Resumed crack job dddddddd") {
|
||||
t.Fatalf("resume toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleActionsRejectIneligibleStatuses(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key rune
|
||||
action crackTopJobAction
|
||||
status clientpb.CrackJobStatus
|
||||
want string
|
||||
}{
|
||||
{name: "pause paused", key: 'p', action: crackTopJobActionPause, status: clientpb.CrackJobStatus_PAUSED, want: "Only in-progress crack jobs can be paused"},
|
||||
{name: "resume active", key: 'u', action: crackTopJobActionResume, status: clientpb.CrackJobStatus_IN_PROGRESS, want: "Only paused crack jobs can be resumed"},
|
||||
{name: "cancel complete", key: 'c', action: crackTopJobActionCancel, status: clientpb.CrackJobStatus_COMPLETED, want: "Only in-progress or paused crack jobs can be cancelled"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
calls := 0
|
||||
model := newCrackTopJobActionTestModel(
|
||||
test.status,
|
||||
singleCrackTopAction(test.action, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
calls++
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
_, command := model.Update(tea.KeyPressMsg{Text: string(test.key), Code: test.key})
|
||||
if command == nil || calls != 0 || model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != "" {
|
||||
t.Fatalf("ineligible action = command:%v calls:%d pending:%#v running:%#v", command != nil, calls, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
if model.toastLevel != "warning" || !strings.Contains(model.toast, test.want) {
|
||||
t.Fatalf("ineligible toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleRejectsNonAuthoritativeSuccessResponse(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_PAUSED,
|
||||
singleCrackTopAction(crackTopJobActionResume, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, resumeCommand := model.Update(tea.KeyPressMsg{Text: "u", Code: 'u'})
|
||||
message := resumeCommand().(crackTopJobActionCompletedMsg)
|
||||
if message.err == nil || !strings.Contains(message.err.Error(), "no job state") {
|
||||
t.Fatalf("empty resume response error = %v", message.err)
|
||||
}
|
||||
_, toastCommand := model.Update(message)
|
||||
if toastCommand == nil || model.refreshing || model.snapshot.Jobs[0].GetStatus() != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("empty resume response mutated state: command=%v refreshing=%v job=%#v", toastCommand != nil, model.refreshing, model.snapshot.Jobs[0])
|
||||
}
|
||||
if model.toastLevel != "error" || !strings.Contains(model.toast, "server returned no job state") {
|
||||
t.Fatalf("empty resume toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleRejectsIDOnlyResumeResponse(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_PAUSED,
|
||||
singleCrackTopAction(crackTopJobActionResume, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
return &clientpb.CrackJob{ID: jobID}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, resumeCommand := model.Update(tea.KeyPressMsg{Text: "u", Code: 'u'})
|
||||
message := resumeCommand().(crackTopJobActionCompletedMsg)
|
||||
if message.err == nil || !strings.Contains(message.err.Error(), "without an update timestamp") {
|
||||
t.Fatalf("ID-only resume response error = %v", message.err)
|
||||
}
|
||||
_, toastCommand := model.Update(message)
|
||||
if toastCommand == nil || model.refreshing || model.snapshot.Jobs[0].GetStatus() != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("ID-only resume response mutated state: command=%v refreshing=%v job=%#v", toastCommand != nil, model.refreshing, model.snapshot.Jobs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleStatusOverrideSuppressesStaleRefresh(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(clientpb.CrackJobStatus_IN_PROGRESS, nil)
|
||||
model.overrideJobStatus(crackTopActionTestJobID, clientpb.CrackJobStatus_PAUSED, 200)
|
||||
|
||||
stale := &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{ID: crackTopActionTestJobID, Status: clientpb.CrackJobStatus_IN_PROGRESS, UpdatedAt: 100}},
|
||||
RefreshedAt: time.Unix(1_700_000_001, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{},
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: stale})
|
||||
if got := model.snapshot.Jobs[0].GetStatus(); got != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("stale refresh status = %s, want PAUSED", got)
|
||||
}
|
||||
if _, ok := model.jobStatusOverrides[crackTopActionTestJobID]; !ok {
|
||||
t.Fatal("stale refresh cleared authoritative status override")
|
||||
}
|
||||
|
||||
observed := &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{ID: crackTopActionTestJobID, Status: clientpb.CrackJobStatus_PAUSED, UpdatedAt: 200}},
|
||||
RefreshedAt: time.Unix(1_700_000_002, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{},
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: observed})
|
||||
if _, ok := model.jobStatusOverrides[crackTopActionTestJobID]; ok {
|
||||
t.Fatal("server-confirmed refresh did not clear status override")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleStatusOverrideYieldsToRepeatedSameSecondConflict(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(clientpb.CrackJobStatus_IN_PROGRESS, nil)
|
||||
model.overrideJobStatus(crackTopActionTestJobID, clientpb.CrackJobStatus_PAUSED, 200)
|
||||
|
||||
conflictingSnapshot := func(refreshedAt int64) *crackTopSnapshot {
|
||||
return &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{
|
||||
ID: crackTopActionTestJobID,
|
||||
Status: clientpb.CrackJobStatus_CANCELLED,
|
||||
UpdatedAt: 200,
|
||||
}},
|
||||
RefreshedAt: time.Unix(refreshedAt, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{},
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: conflictingSnapshot(1_700_000_001)})
|
||||
if got := model.snapshot.Jobs[0].GetStatus(); got != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("first same-second conflict status = %s, want PAUSED stale-snapshot mask", got)
|
||||
}
|
||||
override, ok := model.jobStatusOverrides[crackTopActionTestJobID]
|
||||
if !ok || !override.equalVersionDisagreementSeen {
|
||||
t.Fatalf("first same-second conflict override = %#v, present=%v", override, ok)
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: conflictingSnapshot(1_700_000_002)})
|
||||
if got := model.snapshot.Jobs[0].GetStatus(); got != clientpb.CrackJobStatus_CANCELLED {
|
||||
t.Fatalf("repeated same-second conflict status = %s, want CANCELLED", got)
|
||||
}
|
||||
if _, ok := model.jobStatusOverrides[crackTopActionTestJobID]; ok {
|
||||
t.Fatal("repeated same-second conflict did not release status override")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopActiveFilterRetainsPausedJob(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(clientpb.CrackJobStatus_PAUSED, nil)
|
||||
model.filter = crackTopFilterActive
|
||||
model.normalizeJobSelection()
|
||||
|
||||
jobs := model.filteredJobs()
|
||||
if len(jobs) != 1 || jobs[0].ID != crackTopActionTestJobID || model.selectedJobID != crackTopActionTestJobID {
|
||||
t.Fatalf("active filter lost paused job: jobs=%#v selected=%q", jobs, model.selectedJobID)
|
||||
}
|
||||
if model.dashboard.ActiveJobs != 0 {
|
||||
t.Fatalf("paused job counted as active throughput work: %d", model.dashboard.ActiveJobs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package top
|
||||
|
||||
import "time"
|
||||
|
||||
const crackTopHistoryLimit = 120
|
||||
|
||||
// crackTopHistorySample captures the real telemetry available at one refresh
|
||||
// observation. Known fields distinguish an observed zero from telemetry that
|
||||
// was unavailable, including when the refresh itself failed.
|
||||
type crackTopHistorySample struct {
|
||||
At time.Time
|
||||
|
||||
ClusterRate uint64
|
||||
ClusterRateKnown bool
|
||||
ClusterComplete bool
|
||||
Progress float64
|
||||
ProgressKnown bool
|
||||
Utilization float64
|
||||
UtilizationKnown bool
|
||||
Temperature float64
|
||||
TemperatureKnown bool
|
||||
CountsKnown bool
|
||||
|
||||
ActiveJobs int
|
||||
OnlineWorkers int
|
||||
TotalWorkers int
|
||||
Recovered uint64
|
||||
}
|
||||
|
||||
func crackTopHistorySampleFromDashboard(dashboard crackTopDashboard, at time.Time) crackTopHistorySample {
|
||||
sample := crackTopHistorySample{
|
||||
At: at,
|
||||
ActiveJobs: dashboard.ActiveJobs,
|
||||
OnlineWorkers: dashboard.OnlineWorkers,
|
||||
TotalWorkers: dashboard.OnlineWorkers + dashboard.Unavailable,
|
||||
Recovered: dashboard.Recovered,
|
||||
ClusterRateKnown: dashboard.ClusterRateKnown,
|
||||
ClusterComplete: dashboard.ClusterComplete,
|
||||
ProgressKnown: dashboard.ProgressKnown,
|
||||
CountsKnown: true,
|
||||
}
|
||||
if sample.ClusterRateKnown {
|
||||
sample.ClusterRate = dashboard.ClusterRate
|
||||
}
|
||||
if sample.ProgressKnown {
|
||||
sample.Progress = dashboard.Progress
|
||||
}
|
||||
|
||||
var utilizationTotal float64
|
||||
utilizationSamples := 0
|
||||
for _, worker := range dashboard.Workers {
|
||||
if !worker.TelemetryLive {
|
||||
continue
|
||||
}
|
||||
if worker.HasUtil {
|
||||
utilizationTotal += worker.Utilization
|
||||
utilizationSamples++
|
||||
}
|
||||
if worker.HasTemp && (!sample.TemperatureKnown || worker.Temperature > sample.Temperature) {
|
||||
sample.Temperature = worker.Temperature
|
||||
sample.TemperatureKnown = true
|
||||
}
|
||||
}
|
||||
if utilizationSamples > 0 {
|
||||
sample.Utilization = utilizationTotal / float64(utilizationSamples)
|
||||
sample.UtilizationKnown = true
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func appendCrackTopHistory(history []crackTopHistorySample, sample crackTopHistorySample) []crackTopHistorySample {
|
||||
if len(history) >= crackTopHistoryLimit {
|
||||
start := len(history) - (crackTopHistoryLimit - 1)
|
||||
retained := copy(history, history[start:])
|
||||
history = history[:retained]
|
||||
}
|
||||
return append(history, sample)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
//nolint:gocyclo // This test validates every derived history field in one complete sample.
|
||||
func TestCrackTopHistorySampleFromDashboardDerivesLiveTelemetry(t *testing.T) {
|
||||
at := time.Unix(1_700_000_000, 123)
|
||||
dashboard := crackTopDashboard{
|
||||
ActiveJobs: 2,
|
||||
OnlineWorkers: 3,
|
||||
Unavailable: 2,
|
||||
Recovered: 9,
|
||||
ClusterRate: 12_345,
|
||||
ClusterRateKnown: true,
|
||||
ClusterComplete: true,
|
||||
Progress: 0.625,
|
||||
ProgressKnown: true,
|
||||
Workers: []crackTopWorkerRow{
|
||||
{TelemetryLive: true, Utilization: 20, HasUtil: true, Temperature: 60, HasTemp: true},
|
||||
{TelemetryLive: true, Utilization: 80, HasUtil: true, Temperature: 75, HasTemp: true},
|
||||
{TelemetryLive: true},
|
||||
{TelemetryLive: false, Utilization: 100, HasUtil: true, Temperature: 99, HasTemp: true},
|
||||
},
|
||||
}
|
||||
|
||||
sample := crackTopHistorySampleFromDashboard(dashboard, at)
|
||||
if !sample.At.Equal(at) {
|
||||
t.Fatalf("sample time = %s, want %s", sample.At, at)
|
||||
}
|
||||
if !sample.ClusterRateKnown || sample.ClusterRate != 12_345 {
|
||||
t.Fatalf("cluster rate = %d known %v, want 12345 known", sample.ClusterRate, sample.ClusterRateKnown)
|
||||
}
|
||||
if !sample.ClusterComplete {
|
||||
t.Fatal("complete cluster snapshot was recorded as incomplete")
|
||||
}
|
||||
if !sample.ProgressKnown || sample.Progress != 0.625 {
|
||||
t.Fatalf("progress = %v known %v, want 0.625 known", sample.Progress, sample.ProgressKnown)
|
||||
}
|
||||
if !sample.UtilizationKnown || sample.Utilization != 50 {
|
||||
t.Fatalf("utilization = %v known %v, want live-worker mean 50", sample.Utilization, sample.UtilizationKnown)
|
||||
}
|
||||
if !sample.TemperatureKnown || sample.Temperature != 75 {
|
||||
t.Fatalf("temperature = %v known %v, want live-worker maximum 75", sample.Temperature, sample.TemperatureKnown)
|
||||
}
|
||||
if sample.ActiveJobs != 2 || sample.OnlineWorkers != 3 || sample.TotalWorkers != 5 || sample.Recovered != 9 {
|
||||
t.Fatalf("sample counts = active:%d online:%d total:%d recovered:%d", sample.ActiveJobs, sample.OnlineWorkers, sample.TotalWorkers, sample.Recovered)
|
||||
}
|
||||
if !sample.CountsKnown {
|
||||
t.Fatal("successful dashboard sample marked counts unknown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopHistorySampleFromDashboardPreservesUnknowns(t *testing.T) {
|
||||
dashboard := crackTopDashboard{
|
||||
ClusterRate: 999,
|
||||
Progress: 0.9,
|
||||
Workers: []crackTopWorkerRow{
|
||||
{TelemetryLive: false, Utilization: 88, HasUtil: true, Temperature: 70, HasTemp: true},
|
||||
{TelemetryLive: true},
|
||||
},
|
||||
}
|
||||
|
||||
sample := crackTopHistorySampleFromDashboard(dashboard, time.Time{})
|
||||
if sample.ClusterRateKnown || sample.ClusterRate != 0 {
|
||||
t.Fatalf("unknown cluster rate = %d known %v, want zero unknown", sample.ClusterRate, sample.ClusterRateKnown)
|
||||
}
|
||||
if sample.ClusterComplete {
|
||||
t.Fatal("incomplete cluster snapshot was recorded as complete")
|
||||
}
|
||||
if sample.ProgressKnown || sample.Progress != 0 {
|
||||
t.Fatalf("unknown progress = %v known %v, want zero unknown", sample.Progress, sample.ProgressKnown)
|
||||
}
|
||||
if sample.UtilizationKnown || sample.Utilization != 0 {
|
||||
t.Fatalf("unknown utilization = %v known %v, want zero unknown", sample.Utilization, sample.UtilizationKnown)
|
||||
}
|
||||
if sample.TemperatureKnown || sample.Temperature != 0 {
|
||||
t.Fatalf("unknown temperature = %v known %v, want zero unknown", sample.Temperature, sample.TemperatureKnown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendCrackTopHistoryCapsAndRetainsNewestSamples(t *testing.T) {
|
||||
history := make([]crackTopHistorySample, 0, crackTopHistoryLimit)
|
||||
for index := 0; index < crackTopHistoryLimit+5; index++ {
|
||||
history = appendCrackTopHistory(history, crackTopHistorySample{ActiveJobs: index})
|
||||
}
|
||||
|
||||
if len(history) != crackTopHistoryLimit {
|
||||
t.Fatalf("history length = %d, want %d", len(history), crackTopHistoryLimit)
|
||||
}
|
||||
if history[0].ActiveJobs != 5 {
|
||||
t.Fatalf("oldest retained sample = %d, want 5", history[0].ActiveJobs)
|
||||
}
|
||||
if history[len(history)-1].ActiveJobs != crackTopHistoryLimit+4 {
|
||||
t.Fatalf("newest retained sample = %d, want %d", history[len(history)-1].ActiveJobs, crackTopHistoryLimit+4)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const crackTopWarningRunes = 256
|
||||
|
||||
// crackTopRPC is the complete wire-level dependency of the crack top client.
|
||||
// Keeping it narrow also makes it difficult for the dashboard to accidentally
|
||||
// fall back to RPCs that expose crack targets or recovered material.
|
||||
type crackTopRPC interface {
|
||||
CrackTop(context.Context, *commonpb.Empty, ...grpc.CallOption) (*clientpb.CrackTopSnapshot, error)
|
||||
}
|
||||
|
||||
type crackTopTaskTelemetry struct {
|
||||
status *crackStatusView
|
||||
parseErr bool
|
||||
progressExact bool
|
||||
}
|
||||
|
||||
// crackTopSnapshot is the client-side representation consumed by the existing
|
||||
// dashboard telemetry code. Jobs and stations contain only fields supplied by
|
||||
// the narrow CrackTop RPC; parsed status is kept separately so no sensitive raw
|
||||
// status JSON has to be reconstructed.
|
||||
type crackTopSnapshot struct {
|
||||
Jobs []*clientpb.CrackJob
|
||||
Stations []*clientpb.Crackstation
|
||||
RefreshedAt time.Time
|
||||
taskTelemetry map[string]crackTopTaskTelemetry
|
||||
}
|
||||
|
||||
// CrackTop snapshots are atomic. A successful refresh replaces all previously
|
||||
// displayed server state, while a failed refresh returns no snapshot and leaves
|
||||
// the model's existing value untouched.
|
||||
func mergeCrackTopSnapshot(_ *crackTopSnapshot, incoming *crackTopSnapshot) *crackTopSnapshot {
|
||||
return incoming
|
||||
}
|
||||
|
||||
func loadCrackTopSnapshot(ctx context.Context, rpc crackTopRPC, perCallTimeout time.Duration, completionTime time.Time) (*crackTopSnapshot, error) {
|
||||
if rpc == nil {
|
||||
return nil, crackTopContextError("load crack top snapshot", errors.New("rpc client is unavailable"))
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
var cancel context.CancelFunc
|
||||
if perCallTimeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(ctx, perCallTimeout)
|
||||
} else {
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
wire, err := rpc.CrackTop(ctx, &commonpb.Empty{})
|
||||
if completionTime.IsZero() {
|
||||
completionTime = time.Now()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, crackTopContextError("load crack top snapshot", err)
|
||||
}
|
||||
if wire == nil {
|
||||
return nil, crackTopContextError("load crack top snapshot", errors.New("server returned an empty response"))
|
||||
}
|
||||
|
||||
refreshedAt := completionTime
|
||||
if wire.GetObservedAt() != 0 {
|
||||
refreshedAt = time.Unix(wire.GetObservedAt(), 0)
|
||||
}
|
||||
snapshot := &crackTopSnapshot{
|
||||
Jobs: make([]*clientpb.CrackJob, len(wire.GetJobs())),
|
||||
Stations: make([]*clientpb.Crackstation, len(wire.GetCrackstations())),
|
||||
RefreshedAt: refreshedAt,
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{},
|
||||
}
|
||||
for index, job := range wire.GetJobs() {
|
||||
snapshot.Jobs[index] = crackTopJobFromWire(job, snapshot.taskTelemetry)
|
||||
}
|
||||
for index, station := range wire.GetCrackstations() {
|
||||
snapshot.Stations[index] = crackTopStationFromWire(station)
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func crackTopJobFromWire(wire *clientpb.CrackTopJob, telemetry map[string]crackTopTaskTelemetry) *clientpb.CrackJob {
|
||||
if wire == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := &clientpb.CrackCommand{
|
||||
AttackMode: wire.GetAttackMode(),
|
||||
HashType: wire.GetHashType(),
|
||||
}
|
||||
if wire.HashMode != nil {
|
||||
hashMode := wire.GetHashMode()
|
||||
command.HashMode = &hashMode
|
||||
}
|
||||
job := &clientpb.CrackJob{
|
||||
ID: wire.GetID(),
|
||||
CreatedAt: wire.GetCreatedAt(),
|
||||
CompletedAt: wire.GetCompletedAt(),
|
||||
Status: wire.GetStatus(),
|
||||
Command: command,
|
||||
UpdatedAt: wire.GetUpdatedAt(),
|
||||
ResultCount: wire.GetResultCount(),
|
||||
Tasks: make([]*clientpb.CrackTask, len(wire.GetTasks())),
|
||||
}
|
||||
for index, task := range wire.GetTasks() {
|
||||
job.Tasks[index] = crackTopTaskFromWire(task, telemetry)
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
func crackTopTaskFromWire(wire *clientpb.CrackTopTask, telemetry map[string]crackTopTaskTelemetry) *clientpb.CrackTask {
|
||||
if wire == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
task := &clientpb.CrackTask{
|
||||
ID: wire.GetID(),
|
||||
HostUUID: wire.GetHostUUID(),
|
||||
CreatedAt: wire.GetCreatedAt(),
|
||||
StartedAt: wire.GetStartedAt(),
|
||||
CompletedAt: wire.GetCompletedAt(),
|
||||
Kind: wire.GetKind(),
|
||||
State: wire.GetState(),
|
||||
Attempt: wire.GetAttempt(),
|
||||
LeaseExpiresAt: wire.GetLeaseExpiresAt(),
|
||||
UpdatedAt: wire.GetUpdatedAt(),
|
||||
LastHeartbeatAt: wire.GetLastHeartbeatAt(),
|
||||
ShardSkip: wire.GetShardSkip(),
|
||||
ShardLimit: wire.GetShardLimit(),
|
||||
}
|
||||
|
||||
taskID := strings.TrimSpace(task.GetID())
|
||||
if telemetry == nil || taskID == "" {
|
||||
return task
|
||||
}
|
||||
metadata := crackTopTaskTelemetry{
|
||||
parseErr: wire.GetStatusParseError(),
|
||||
progressExact: wire.GetProgressExact(),
|
||||
}
|
||||
if wire.GetStatusAvailable() && !metadata.parseErr {
|
||||
metadata.status = &crackStatusView{Devices: crackTopDevicesFromWire(wire.GetDevices())}
|
||||
if metadata.progressExact || wire.GetShardSkip() == 0 {
|
||||
metadata.status.ProgressCurrent = wire.GetProgressCurrent()
|
||||
metadata.status.ProgressTotal = wire.GetProgressTotal()
|
||||
}
|
||||
}
|
||||
if metadata.status != nil || metadata.parseErr {
|
||||
telemetry[taskID] = metadata
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
func crackTopDevicesFromWire(devices []*clientpb.CrackTopDevice) []crackDeviceView {
|
||||
converted := make([]crackDeviceView, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if device == nil {
|
||||
continue
|
||||
}
|
||||
view := crackDeviceView{ID: device.GetID()}
|
||||
if device.GetSpeedAvailable() {
|
||||
view.Speed = strconv.FormatUint(device.GetSpeed(), 10)
|
||||
}
|
||||
if device.GetTemperatureAvailable() {
|
||||
view.Temperature = strconv.FormatFloat(device.GetTemperature(), 'f', -1, 64)
|
||||
}
|
||||
if device.GetUtilizationAvailable() {
|
||||
view.Utilization = strconv.FormatFloat(device.GetUtilization(), 'f', -1, 64)
|
||||
}
|
||||
converted = append(converted, view)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func crackTopStationFromWire(wire *clientpb.CrackTopStation) *clientpb.Crackstation {
|
||||
if wire == nil {
|
||||
return nil
|
||||
}
|
||||
station := &clientpb.Crackstation{
|
||||
ID: wire.GetID(),
|
||||
Name: wire.GetName(),
|
||||
HostUUID: wire.GetHostUUID(),
|
||||
}
|
||||
if wire.GetStatusAvailable() {
|
||||
station.Status = &clientpb.CrackstationStatus{
|
||||
Name: wire.GetName(),
|
||||
HostUUID: wire.GetHostUUID(),
|
||||
State: wire.GetState(),
|
||||
CurrentCrackJobID: wire.GetCurrentCrackJobID(),
|
||||
IsSyncing: wire.GetIsSyncing(),
|
||||
}
|
||||
}
|
||||
return station
|
||||
}
|
||||
|
||||
func crackTopContextError(scope string, err error) error {
|
||||
message := crackTopSanitizeError(err)
|
||||
if strings.TrimSpace(scope) == "" {
|
||||
return errors.New(message)
|
||||
}
|
||||
return fmt.Errorf("%s: %s", strings.TrimSpace(scope), message)
|
||||
}
|
||||
|
||||
func crackTopWarningText(message string) string {
|
||||
return crackTopSanitizeError(errors.New(message))
|
||||
}
|
||||
|
||||
func crackTopSanitizeError(err error) string {
|
||||
if err == nil {
|
||||
return "unknown error"
|
||||
}
|
||||
message := ansi.Strip(err.Error())
|
||||
message = strings.Map(func(character rune) rune {
|
||||
if unicode.IsSpace(character) {
|
||||
return ' '
|
||||
}
|
||||
if !unicode.IsPrint(character) {
|
||||
return -1
|
||||
}
|
||||
return character
|
||||
}, message)
|
||||
message = strings.Join(strings.Fields(message), " ")
|
||||
if message == "" {
|
||||
return "unknown error"
|
||||
}
|
||||
runes := []rune(message)
|
||||
if len(runes) > crackTopWarningRunes {
|
||||
message = string(runes[:crackTopWarningRunes-1]) + "…"
|
||||
}
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type crackTopRPCTestClient struct {
|
||||
calls atomic.Int32
|
||||
call func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error)
|
||||
}
|
||||
|
||||
func (client *crackTopRPCTestClient) CrackTop(ctx context.Context, request *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.CrackTopSnapshot, error) {
|
||||
client.calls.Add(1)
|
||||
if client.call == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return client.call(ctx, request)
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test intentionally validates every field in one complete lean-snapshot conversion.
|
||||
func TestLoadCrackTopSnapshotUsesOneRPCAndConvertsLeanSnapshot(t *testing.T) {
|
||||
hashMode := uint32(5600)
|
||||
wire := &clientpb.CrackTopSnapshot{
|
||||
ObservedAt: 1_700_000_123,
|
||||
Jobs: []*clientpb.CrackTopJob{
|
||||
{
|
||||
ID: "job-one",
|
||||
CreatedAt: "2026-09-12T10:00:00Z",
|
||||
CompletedAt: "",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
UpdatedAt: 1_700_000_120,
|
||||
ResultCount: 7,
|
||||
AttackMode: clientpb.CrackAttackMode_BRUTEFORCE,
|
||||
HashType: clientpb.HashType_NET_NTLM_V2,
|
||||
HashMode: &hashMode,
|
||||
Tasks: []*clientpb.CrackTopTask{
|
||||
{
|
||||
ID: "task-live",
|
||||
HostUUID: "worker-one",
|
||||
CreatedAt: 11,
|
||||
StartedAt: 12,
|
||||
CompletedAt: 13,
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
Attempt: 2,
|
||||
LeaseExpiresAt: 1_700_000_500,
|
||||
UpdatedAt: 14,
|
||||
LastHeartbeatAt: 15,
|
||||
ShardSkip: 100,
|
||||
ShardLimit: 200,
|
||||
ProgressCurrent: "125",
|
||||
ProgressTotal: "300",
|
||||
StatusAvailable: true,
|
||||
StatusParseError: false,
|
||||
ProgressExact: true,
|
||||
Devices: []*clientpb.CrackTopDevice{
|
||||
{
|
||||
ID: "gpu-0",
|
||||
Speed: 9_876_543,
|
||||
SpeedAvailable: true,
|
||||
Temperature: 71.5,
|
||||
TemperatureAvailable: true,
|
||||
Utilization: 99.25,
|
||||
UtilizationAvailable: true,
|
||||
},
|
||||
nil,
|
||||
{ID: "gpu-1", Speed: 123, Temperature: 45, Utilization: 67},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
{
|
||||
ID: "task-malformed",
|
||||
HostUUID: "worker-two",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
ProgressCurrent: "sensitive-raw-data-must-not-be-rebuilt",
|
||||
StatusAvailable: true,
|
||||
StatusParseError: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
{ID: "job-two", Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
},
|
||||
Crackstations: []*clientpb.CrackTopStation{
|
||||
{
|
||||
ID: "station-one",
|
||||
Name: "rig-one",
|
||||
HostUUID: "worker-one",
|
||||
State: clientpb.States_CRACKING,
|
||||
CurrentCrackJobID: "job-one",
|
||||
IsSyncing: false,
|
||||
StatusAvailable: true,
|
||||
},
|
||||
nil,
|
||||
{ID: "station-two", Name: "rig-two", HostUUID: "worker-two"},
|
||||
},
|
||||
}
|
||||
wireBefore := proto.Clone(wire).(*clientpb.CrackTopSnapshot)
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, request *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
if ctx == nil {
|
||||
t.Fatal("CrackTop received a nil context")
|
||||
}
|
||||
if request == nil {
|
||||
t.Fatal("CrackTop received a nil request")
|
||||
}
|
||||
return wire, nil
|
||||
},
|
||||
}
|
||||
|
||||
fallback := time.Unix(1_600_000_000, 987_654_321)
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, fallback)
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
if want := time.Unix(wire.GetObservedAt(), 0); !snapshot.RefreshedAt.Equal(want) {
|
||||
t.Fatalf("RefreshedAt = %s, want server observation time %s", snapshot.RefreshedAt, want)
|
||||
}
|
||||
if !proto.Equal(wire, wireBefore) {
|
||||
t.Fatal("loader mutated the wire snapshot")
|
||||
}
|
||||
|
||||
if len(snapshot.Jobs) != 3 || snapshot.Jobs[1] != nil || snapshot.Jobs[2].GetID() != "job-two" {
|
||||
t.Fatalf("jobs did not preserve wire order and nil entries: %#v", snapshot.Jobs)
|
||||
}
|
||||
job := snapshot.Jobs[0]
|
||||
if job.GetID() != "job-one" || job.GetCreatedAt() != wire.Jobs[0].GetCreatedAt() || job.GetCompletedAt() != wire.Jobs[0].GetCompletedAt() ||
|
||||
job.GetStatus() != clientpb.CrackJobStatus_IN_PROGRESS || job.GetUpdatedAt() != 1_700_000_120 || job.GetResultCount() != 7 {
|
||||
t.Fatalf("converted job metadata = %#v", job)
|
||||
}
|
||||
if job.GetCommand() == nil || job.GetCommand().GetAttackMode() != clientpb.CrackAttackMode_BRUTEFORCE ||
|
||||
job.GetCommand().GetHashType() != clientpb.HashType_NET_NTLM_V2 || job.GetCommand().HashMode == nil || job.GetCommand().GetHashMode() != hashMode {
|
||||
t.Fatalf("converted job command = %#v", job.GetCommand())
|
||||
}
|
||||
if job.GetErr() != "" || job.GetResultFileID() != "" || job.GetKeyspace() != "" || len(job.GetResults()) != 0 ||
|
||||
len(job.GetCommand().GetHashes()) != 0 || len(job.GetCommand().GetPositionalArguments()) != 0 || len(job.GetCommand().GetCredentialIDs()) != 0 {
|
||||
t.Fatalf("lean job conversion populated target or result fields: %#v", job)
|
||||
}
|
||||
if len(job.GetTasks()) != 3 || job.GetTasks()[1] != nil {
|
||||
t.Fatalf("tasks did not preserve wire order and nil entries: %#v", job.GetTasks())
|
||||
}
|
||||
task := job.GetTasks()[0]
|
||||
if task.GetID() != "task-live" || task.GetHostUUID() != "worker-one" || task.GetCreatedAt() != 11 || task.GetStartedAt() != 12 ||
|
||||
task.GetCompletedAt() != 13 || task.GetKind() != clientpb.CrackTaskKind_CRACK_TASK_CRACK ||
|
||||
task.GetState() != clientpb.CrackTaskState_CRACK_TASK_RUNNING || task.GetAttempt() != 2 ||
|
||||
task.GetLeaseExpiresAt() != 1_700_000_500 || task.GetUpdatedAt() != 14 || task.GetLastHeartbeatAt() != 15 ||
|
||||
task.GetShardSkip() != 100 || task.GetShardLimit() != 200 {
|
||||
t.Fatalf("converted task metadata = %#v", task)
|
||||
}
|
||||
if task.GetErr() != "" || len(task.GetStdout()) != 0 || len(task.GetStderr()) != 0 || task.GetCommand() != nil ||
|
||||
task.GetLeaseToken() != "" || task.GetKeyspace() != "" || len(task.GetLatestStatusJSON()) != 0 || len(task.GetRecoveredJSON()) != 0 {
|
||||
t.Fatalf("lean task conversion populated execution or raw telemetry fields: %#v", task)
|
||||
}
|
||||
|
||||
metadata, ok := snapshot.taskTelemetry["task-live"]
|
||||
if !ok || metadata.parseErr || metadata.status == nil {
|
||||
t.Fatalf("live task telemetry = %#v, present=%v", metadata, ok)
|
||||
}
|
||||
if !metadata.progressExact {
|
||||
t.Fatal("server-declared exact progress was discarded")
|
||||
}
|
||||
if metadata.status.ProgressCurrent != "125" || metadata.status.ProgressTotal != "300" || len(metadata.status.Devices) != 2 {
|
||||
t.Fatalf("converted status = %#v", metadata.status)
|
||||
}
|
||||
firstDevice := metadata.status.Devices[0]
|
||||
if firstDevice.ID != "gpu-0" || firstDevice.Speed != "9876543" || firstDevice.Temperature != "71.5" || firstDevice.Utilization != "99.25" {
|
||||
t.Fatalf("converted available device telemetry = %#v", firstDevice)
|
||||
}
|
||||
secondDevice := metadata.status.Devices[1]
|
||||
if secondDevice.ID != "gpu-1" || secondDevice.Speed != "" || secondDevice.Temperature != "" || secondDevice.Utilization != "" {
|
||||
t.Fatalf("unavailable device telemetry was treated as available: %#v", secondDevice)
|
||||
}
|
||||
malformed, ok := snapshot.taskTelemetry["task-malformed"]
|
||||
if !ok || !malformed.parseErr || malformed.status != nil {
|
||||
t.Fatalf("malformed task telemetry = %#v, present=%v", malformed, ok)
|
||||
}
|
||||
|
||||
if len(snapshot.Stations) != 3 || snapshot.Stations[1] != nil {
|
||||
t.Fatalf("stations did not preserve wire order and nil entries: %#v", snapshot.Stations)
|
||||
}
|
||||
station := snapshot.Stations[0]
|
||||
if station.GetID() != "station-one" || station.GetName() != "rig-one" || station.GetHostUUID() != "worker-one" || station.GetStatus() == nil ||
|
||||
station.GetStatus().GetName() != "rig-one" || station.GetStatus().GetHostUUID() != "worker-one" ||
|
||||
station.GetStatus().GetState() != clientpb.States_CRACKING || station.GetStatus().GetCurrentCrackJobID() != "job-one" || station.GetStatus().GetIsSyncing() {
|
||||
t.Fatalf("converted station = %#v", station)
|
||||
}
|
||||
if station.GetOperatorName() != "" || station.GetGOOS() != "" || station.GetGOARCH() != "" || station.GetVersion() != "" ||
|
||||
len(station.GetBenchmarks()) != 0 || len(station.GetCUDA()) != 0 || len(station.GetMetal()) != 0 || len(station.GetOpenCL()) != 0 ||
|
||||
len(station.GetHIP()) != 0 || len(station.GetCapabilities()) != 0 {
|
||||
t.Fatalf("lean station conversion populated private inventory fields: %#v", station)
|
||||
}
|
||||
if snapshot.Stations[2].GetStatus() != nil {
|
||||
t.Fatalf("station without available status got a fabricated status: %#v", snapshot.Stations[2])
|
||||
}
|
||||
|
||||
*snapshot.Jobs[0].Command.HashMode = 1000
|
||||
snapshot.Jobs[0].Tasks[0].ID = "changed-task"
|
||||
snapshot.Stations[0].Status.Name = "changed-station"
|
||||
metadata.status.Devices[0].ID = "changed-device"
|
||||
if wire.Jobs[0].GetHashMode() != 5600 || wire.Jobs[0].Tasks[0].GetID() != "task-live" ||
|
||||
wire.Crackstations[0].GetName() != "rig-one" || wire.Jobs[0].Tasks[0].Devices[0].GetID() != "gpu-0" {
|
||||
t.Fatal("converted snapshot aliases mutable wire data")
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test intentionally covers the full loader-to-dashboard telemetry path.
|
||||
func TestLoadCrackTopSnapshotFeedsDashboardTelemetry(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
return &clientpb.CrackTopSnapshot{
|
||||
ObservedAt: now.Unix(),
|
||||
Jobs: []*clientpb.CrackTopJob{{
|
||||
ID: crackTopTestJobID,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: []*clientpb.CrackTopTask{{
|
||||
ID: "task-wire",
|
||||
HostUUID: "worker-wire",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
LeaseExpiresAt: now.Add(time.Minute).Unix(),
|
||||
LastHeartbeatAt: now.Unix(),
|
||||
ProgressCurrent: "25",
|
||||
ProgressTotal: "100",
|
||||
StatusAvailable: true,
|
||||
ProgressExact: true,
|
||||
Devices: []*clientpb.CrackTopDevice{{
|
||||
ID: "gpu-0",
|
||||
Speed: 125_000,
|
||||
SpeedAvailable: true,
|
||||
Temperature: 67,
|
||||
TemperatureAvailable: true,
|
||||
Utilization: 98,
|
||||
UtilizationAvailable: true,
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
Crackstations: []*clientpb.CrackTopStation{{
|
||||
ID: "station-wire",
|
||||
Name: "Wire Worker",
|
||||
HostUUID: "worker-wire",
|
||||
State: clientpb.States_CRACKING,
|
||||
CurrentCrackJobID: crackTopTestJobID,
|
||||
StatusAvailable: true,
|
||||
}},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
dashboard := buildCrackTopDashboard(snapshot)
|
||||
if len(dashboard.Jobs) != 1 || !dashboard.Jobs[0].ProgressKnown || dashboard.Jobs[0].Progress != 0.25 {
|
||||
t.Fatalf("job progress from narrow telemetry = %#v", dashboard.Jobs)
|
||||
}
|
||||
if !dashboard.ClusterRateKnown || !dashboard.ClusterComplete || dashboard.ClusterRate != 125_000 {
|
||||
t.Fatalf("cluster telemetry = rate:%d known:%v complete:%v", dashboard.ClusterRate, dashboard.ClusterRateKnown, dashboard.ClusterComplete)
|
||||
}
|
||||
if len(dashboard.Workers) != 1 {
|
||||
t.Fatalf("workers = %#v, want one", dashboard.Workers)
|
||||
}
|
||||
worker := dashboard.Workers[0]
|
||||
if !worker.TelemetryLive || !worker.RateKnown || worker.Rate != 125_000 || worker.Devices != 1 ||
|
||||
!worker.HasTemp || worker.Temperature != 67 || !worker.HasUtil || worker.Utilization != 98 {
|
||||
t.Fatalf("worker telemetry from narrow snapshot = %#v", worker)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotUsesCompletionTimeOnlyWithoutObservedAt(t *testing.T) {
|
||||
fallback := time.Unix(1_700_000_000, 123_456_789)
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
return &clientpb.CrackTopSnapshot{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, 0, fallback)
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
if !snapshot.RefreshedAt.Equal(fallback) {
|
||||
t.Fatalf("RefreshedAt = %s, want deterministic completion time %s", snapshot.RefreshedAt, fallback)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotPropagatesDeadlineAndCancelsCallContext(t *testing.T) {
|
||||
var callContext context.Context
|
||||
var deadline time.Time
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, _ *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
callContext = ctx
|
||||
var ok bool
|
||||
deadline, ok = ctx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("CrackTop context has no deadline")
|
||||
}
|
||||
return &clientpb.CrackTopSnapshot{ObservedAt: 1}, nil
|
||||
},
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
timeout := time.Second
|
||||
_, err := loadCrackTopSnapshot(t.Context(), client, timeout, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
if deadline.Before(started) || deadline.After(started.Add(timeout+100*time.Millisecond)) {
|
||||
t.Fatalf("RPC deadline = %s, want about %s after %s", deadline, timeout, started)
|
||||
}
|
||||
select {
|
||||
case <-callContext.Done():
|
||||
if !errors.Is(callContext.Err(), context.Canceled) {
|
||||
t.Fatalf("completed call context error = %v, want context canceled", callContext.Err())
|
||||
}
|
||||
default:
|
||||
t.Fatal("completed call context was not canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotReturnsNilOnTimeout(t *testing.T) {
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, _ *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, 10*time.Millisecond, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("timed-out load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: context deadline exceeded" {
|
||||
t.Fatalf("timed-out load error = %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotPropagatesParentCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, _ *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(ctx, client, time.Second, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("canceled load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: context canceled" {
|
||||
t.Fatalf("canceled load error = %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotSanitizesRPCErrorAndRejectsEmptyResponse(t *testing.T) {
|
||||
t.Run("rpc error", func(t *testing.T) {
|
||||
raw := errors.New("bad\r\n\x1b[31mserver\x1b[0m\tfailure")
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
return nil, raw
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("failed load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: bad server failure" {
|
||||
t.Fatalf("sanitized error = %v", err)
|
||||
}
|
||||
if errors.Is(err, raw) {
|
||||
t.Fatal("sanitized error retained the raw RPC error in its unwrap chain")
|
||||
}
|
||||
if strings.ContainsAny(err.Error(), "\r\n\t\x1b") {
|
||||
t.Fatalf("error still contains terminal controls: %q", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil response", func(t *testing.T) {
|
||||
client := &crackTopRPCTestClient{}
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("empty load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: server returned an empty response" {
|
||||
t.Fatalf("empty response error = %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,598 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
//nolint:gocyclo // This test validates all history fields across success, gap, failure, and recovery snapshots.
|
||||
func TestCrackTopAcceptedSnapshotsAppendRealOverviewHistory(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
first := crackTopLiveTestSnapshot()
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: first})
|
||||
if len(model.history) != 1 {
|
||||
t.Fatalf("history length after first accepted snapshot = %d, want 1", len(model.history))
|
||||
}
|
||||
if sample := model.history[0]; !sample.At.Equal(first.RefreshedAt) ||
|
||||
!sample.ClusterRateKnown || sample.ClusterRate != 2_502_000 ||
|
||||
!sample.ProgressKnown || sample.Progress != 0.3125 ||
|
||||
!sample.UtilizationKnown || sample.Utilization != 98.5 ||
|
||||
!sample.TemperatureKnown || sample.Temperature != 70 {
|
||||
t.Fatalf("first history sample does not reflect live fixture telemetry: %#v", sample)
|
||||
}
|
||||
|
||||
gapTime := first.RefreshedAt.Add(time.Second)
|
||||
unknownGap := &crackTopSnapshot{
|
||||
Stations: []*clientpb.Crackstation{{
|
||||
HostUUID: "worker-without-status",
|
||||
Name: "Awaiting Status",
|
||||
}},
|
||||
RefreshedAt: gapTime,
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: unknownGap})
|
||||
if len(model.history) != 2 {
|
||||
t.Fatalf("history length after second accepted snapshot = %d, want 2", len(model.history))
|
||||
}
|
||||
gap := model.history[1]
|
||||
if !gap.At.Equal(gapTime) {
|
||||
t.Fatalf("unknown-gap time = %s, want %s", gap.At, gapTime)
|
||||
}
|
||||
if gap.ClusterRateKnown || gap.ProgressKnown || gap.UtilizationKnown || gap.TemperatureKnown {
|
||||
t.Fatalf("unreported telemetry was recorded as known instead of an unknown gap: %#v", gap)
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{err: errors.New("refresh failed")})
|
||||
if len(model.history) != 3 {
|
||||
t.Fatalf("failed refresh history length = %d, want an explicit third gap sample", len(model.history))
|
||||
}
|
||||
failure := model.history[2]
|
||||
if failure.ClusterRateKnown || failure.ProgressKnown || failure.UtilizationKnown || failure.TemperatureKnown || failure.CountsKnown {
|
||||
t.Fatalf("failed refresh did not produce an unknown chart gap: %#v", failure)
|
||||
}
|
||||
|
||||
recovered := crackTopLiveTestSnapshot()
|
||||
recovered.RefreshedAt = gapTime.Add(time.Second)
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: recovered})
|
||||
if len(model.history) != 4 || !model.history[3].ClusterRateKnown {
|
||||
t.Fatalf("recovered refresh did not resume live history after the gap: %#v", model.history)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopWideOverviewRendersRealDataCardsChartsAndJournal(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
model.refreshing = false
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: crackTopLiveTestSnapshot()})
|
||||
|
||||
view := model.View()
|
||||
if !view.AltScreen {
|
||||
t.Fatal("wide crack top overview did not request the alternate screen")
|
||||
}
|
||||
plain := ansi.Strip(view.Content)
|
||||
for _, expected := range []string{
|
||||
"CLUSTER / STATE",
|
||||
"THROUGHPUT",
|
||||
"SAMPLE AVG",
|
||||
"DIAGNOSTICS",
|
||||
"KEYSPACE / PROGRESS",
|
||||
"OBSERVED WORKERS",
|
||||
"DEVICE LOAD",
|
||||
"rate",
|
||||
"progress",
|
||||
"worker max util",
|
||||
"RECENT JOURNAL",
|
||||
"ACTIVE 1",
|
||||
"JOBS 1 loaded",
|
||||
"2/2 online",
|
||||
"2.50 MH/s",
|
||||
"RECOVERED 7 loaded",
|
||||
"31.2%",
|
||||
"98% avg worker max",
|
||||
"70°C max",
|
||||
"QUEUE 2 run 0 leased 0 waiting",
|
||||
"Alpha Rig",
|
||||
"Beta Rig",
|
||||
"snapshot ready",
|
||||
} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Errorf("wide overview does not contain %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
if got := ansi.StringWidth(strings.Split(view.Content, "\n")[0]); got > model.width {
|
||||
t.Fatalf("wide overview first-line width = %d, want <= %d", got, model.width)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewDoesNotPresentDefaultsAsLiveBeforeSnapshot(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = crackTopOverviewWidth
|
||||
model.height = crackTopOverviewHeight
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"WAITING", "first live snapshot", "ACTIVE —", "RECOVERED —", "QUEUE —"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Errorf("pre-snapshot overview is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
for _, unsupported := range []string{"HEALTHY", "OK LIVE", "ACTIVE 0", "RECOVERED 0", "QUEUE 0"} {
|
||||
if strings.Contains(plain, unsupported) {
|
||||
t.Errorf("pre-snapshot overview presented %q as observed:\n%s", unsupported, plain)
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{err: errors.New("rpc unavailable")})
|
||||
failed := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"NO SNAPSHOT", "refresh failed", "ERROR", "no live snapshot"} {
|
||||
if !strings.Contains(failed, expected) {
|
||||
t.Errorf("initial-failure overview is missing %q:\n%s", expected, failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopCompactViewDoesNotPresentDefaultsAsLiveBeforeSnapshot(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 90
|
||||
model.height = 28
|
||||
model.refreshing = false
|
||||
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"waiting for data", "ACTIVE —", "JOBS —", "WORKERS —", "RECOVERED —", "awaiting first snapshot"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Errorf("compact pre-snapshot view is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
for _, unsupported := range []string{"ACTIVE 0", "JOBS 0 loaded", "WORKERS 0/0", "RECOVERED 0", "no active jobs"} {
|
||||
if strings.Contains(plain, unsupported) {
|
||||
t.Errorf("compact pre-snapshot view presented %q as observed:\n%s", unsupported, plain)
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{err: errors.New("rpc unavailable")})
|
||||
failed := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"no snapshot", "ACTIVE —", "current data unavailable"} {
|
||||
if !strings.Contains(failed, expected) {
|
||||
t.Errorf("compact initial-failure view is missing %q:\n%s", expected, failed)
|
||||
}
|
||||
}
|
||||
for _, unsupported := range []string{"ACTIVE 0", "JOBS 0 loaded", "WORKERS 0/0", "RECOVERED 0", "no active jobs"} {
|
||||
if strings.Contains(failed, unsupported) {
|
||||
t.Errorf("compact initial-failure view presented %q as observed:\n%s", unsupported, failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewMarksRetainedSnapshotMetricsUnavailableAfterRefreshFailure(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: crackTopLiveTestSnapshot()})
|
||||
_, _ = model.Update(crackTopSnapshotMsg{err: errors.New("temporary outage")})
|
||||
|
||||
meters := ansi.Strip(model.renderOverviewMeters(150, crackTopOverviewMetersHeight))
|
||||
if count := strings.Count(meters, "current data unavailable"); count != 3 {
|
||||
t.Fatalf("failed refresh meters did not expose progress/worker gaps:\n%s", meters)
|
||||
}
|
||||
if strings.Contains(meters, "no active jobs") || strings.Contains(meters, "no observed workers") {
|
||||
t.Fatalf("failed refresh meters converted unknown counts to zero:\n%s", meters)
|
||||
}
|
||||
throughput := ansi.Strip(model.renderOverviewThroughputCard(55, crackTopOverviewSummaryHeight))
|
||||
if !strings.Contains(throughput, "CLUSTER last 2.50 MH/s") || !strings.Contains(throughput, "RECOVERED 7 last snapshot") || !strings.Contains(throughput, "VIEW —") {
|
||||
t.Fatalf("retained throughput was not labeled as stale:\n%s", throughput)
|
||||
}
|
||||
diagnostics := ansi.Strip(model.renderOverviewDiagnosticsCard(55, crackTopOverviewSummaryHeight))
|
||||
if !strings.Contains(diagnostics, "QUEUE last 2 run") {
|
||||
t.Fatalf("retained queue counts were presented as current:\n%s", diagnostics)
|
||||
}
|
||||
jobs := ansi.Strip(model.renderJobsPane(90, 8))
|
||||
workers := ansi.Strip(model.renderWorkersPane(59, 8))
|
||||
if !strings.Contains(jobs, "LAST SNAPSHOT") || !strings.Contains(workers, "LAST SNAPSHOT") {
|
||||
t.Fatalf("retained detail panes were not labeled stale:\njobs:\n%s\nworkers:\n%s", jobs, workers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewRefreshInFlightIsSilent(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: crackTopLiveTestSnapshot()})
|
||||
if command := model.startRefresh(); command == nil || !model.refreshing {
|
||||
t.Fatalf("refresh did not start: command=%v refreshing=%v", command != nil, model.refreshing)
|
||||
}
|
||||
|
||||
top := ansi.Strip(model.renderOverviewTopBar(model.width))
|
||||
diagnostics := ansi.Strip(model.renderOverviewDiagnosticsCard(50, crackTopOverviewSummaryHeight))
|
||||
footer := ansi.Strip(model.renderOverviewFooter(model.width))
|
||||
header := ansi.Strip(model.renderHeader(model.width))
|
||||
for name, rendered := range map[string]string{
|
||||
"top bar": top,
|
||||
"diagnostics": diagnostics,
|
||||
"footer": footer,
|
||||
"header": header,
|
||||
} {
|
||||
lower := strings.ToLower(rendered)
|
||||
if strings.Contains(lower, "refreshing") || strings.Contains(lower, "live snapshot in flight") {
|
||||
t.Errorf("%s exposed an in-flight refresh:\n%s", name, rendered)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(diagnostics, "OK LIVE DATA COMPLETE") {
|
||||
t.Errorf("silent refresh changed snapshot diagnostics:\n%s", diagnostics)
|
||||
}
|
||||
if !strings.Contains(footer, "JOBS focus") {
|
||||
t.Errorf("silent refresh displaced the normal footer:\n%s", footer)
|
||||
}
|
||||
if !strings.Contains(header, "updated ") {
|
||||
t.Errorf("silent refresh hid the current snapshot timestamp:\n%s", header)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewLabelsPartialRateHistoryAsLowerBounds(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
model.snapshot = &crackTopSnapshot{RefreshedAt: time.Now()}
|
||||
model.dashboard = crackTopDashboard{ClusterRate: 2_000, ClusterRateKnown: true, ClusterComplete: false}
|
||||
model.history = []crackTopHistorySample{
|
||||
{At: time.Now().Add(-time.Second), ClusterRate: 1_000, ClusterRateKnown: true, ClusterComplete: true},
|
||||
{At: time.Now(), ClusterRate: 2_000, ClusterRateKnown: true, ClusterComplete: false},
|
||||
}
|
||||
|
||||
average, peak, known, complete := crackTopRateStats(model.history)
|
||||
if !known || complete || average != 1_500 || peak != 2_000 {
|
||||
t.Fatalf("partial rate stats = avg:%d peak:%d known:%v complete:%v", average, peak, known, complete)
|
||||
}
|
||||
plain := ansi.Strip(model.renderOverviewThroughputCard(55, crackTopOverviewSummaryHeight))
|
||||
if !strings.Contains(plain, "CLUSTER ≥ 2.00 kH/s") || !strings.Contains(plain, "AVG ≥ 1.50 kH/s") || !strings.Contains(plain, "PEAK ≥ 2.00 kH/s") {
|
||||
t.Fatalf("partial rate card lost lower-bound semantics:\n%s", plain)
|
||||
}
|
||||
rateChart := ansi.Strip(model.renderOverviewCharts(150, 8))
|
||||
if !strings.Contains(rateChart, "rate ≥ 2.00 kH/s") {
|
||||
t.Fatalf("partial rate chart lost current lower-bound semantics:\n%s", rateChart)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewPausedOnlyWorkloadIsNotIdle(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.snapshot = &crackTopSnapshot{RefreshedAt: time.Now()}
|
||||
model.dashboard = crackTopDashboard{Jobs: []crackTopJobRow{{ID: "paused", Status: clientpb.CrackJobStatus_PAUSED}}}
|
||||
plain := ansi.Strip(model.renderOverviewStateCard(50, crackTopOverviewSummaryHeight))
|
||||
if !strings.Contains(plain, "PAUSED") || strings.Contains(plain, "IDLE") {
|
||||
t.Fatalf("paused-only workload state is misleading:\n%s", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopPlotPreservesUnknownGapAndBounds(t *testing.T) {
|
||||
const (
|
||||
width = 4
|
||||
height = 4
|
||||
)
|
||||
plot := crackTopRenderPlot([]crackTopChartPoint{
|
||||
{value: 100, known: true},
|
||||
{value: 100, known: true},
|
||||
{known: false},
|
||||
{value: 100, known: true},
|
||||
}, width, height, 100)
|
||||
|
||||
lines := strings.Split(plot, "\n")
|
||||
if len(lines) != height {
|
||||
t.Fatalf("plot height = %d, want %d:\n%s", len(lines), height, plot)
|
||||
}
|
||||
for index, line := range lines {
|
||||
if got := ansi.StringWidth(line); got > width {
|
||||
t.Fatalf("plot line %d width = %d, want <= %d: %q", index+1, got, width, line)
|
||||
}
|
||||
}
|
||||
top := []rune(lines[0])
|
||||
if len(top) != width {
|
||||
t.Fatalf("plot top row width = %d runes, want %d: %q", len(top), width, lines[0])
|
||||
}
|
||||
if top[0] != '╶' || top[1] != '╴' || top[2] != ' ' || top[3] != '●' {
|
||||
t.Fatalf("plot did not preserve the unknown sample as a visible gap: %q", lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopPlotConnectsVerticalTransitions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
points []crackTopChartPoint
|
||||
height int
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "rising",
|
||||
points: []crackTopChartPoint{
|
||||
{value: 0, known: true},
|
||||
{value: 50, known: true},
|
||||
{value: 100, known: true},
|
||||
},
|
||||
height: 3,
|
||||
want: " ╷\n·┌┘\n╶┘·",
|
||||
},
|
||||
{
|
||||
name: "falling",
|
||||
points: []crackTopChartPoint{
|
||||
{value: 100, known: true},
|
||||
{value: 50, known: true},
|
||||
{value: 0, known: true},
|
||||
},
|
||||
height: 3,
|
||||
want: "╶┐ \n·└┐\n··╵",
|
||||
},
|
||||
{
|
||||
name: "flat",
|
||||
points: []crackTopChartPoint{
|
||||
{value: 50, known: true},
|
||||
{value: 50, known: true},
|
||||
{value: 50, known: true},
|
||||
},
|
||||
height: 3,
|
||||
want: " \n╶─╴\n···",
|
||||
},
|
||||
{
|
||||
name: "full range rise",
|
||||
points: []crackTopChartPoint{
|
||||
{value: 0, known: true},
|
||||
{value: 100, known: true},
|
||||
},
|
||||
height: 4,
|
||||
want: " ╷\n │\n·│\n╶┘",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
got := crackTopRenderPlot(testCase.points, len(testCase.points), testCase.height, 100)
|
||||
if got != testCase.want {
|
||||
t.Fatalf("plot =\n%q\nwant\n%q", got, testCase.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopChartStatsDoesNotPresentStaleLatestAsCurrent(t *testing.T) {
|
||||
latest, average, peak, latestKnown, historyKnown, latestLowerBound, historyLowerBound := crackTopChartStats([]crackTopChartPoint{
|
||||
{value: 25, known: true},
|
||||
{value: 75, known: true},
|
||||
{known: false},
|
||||
})
|
||||
if latestKnown || !historyKnown {
|
||||
t.Fatalf("latest/history known = %v/%v, want false/true", latestKnown, historyKnown)
|
||||
}
|
||||
if latestLowerBound || historyLowerBound {
|
||||
t.Fatalf("unexpected lower-bound flags = latest:%v history:%v", latestLowerBound, historyLowerBound)
|
||||
}
|
||||
if latest != 0 || average != 50 || peak != 75 {
|
||||
t.Fatalf("latest/average/peak = %v/%v/%v, want 0/50/75", latest, average, peak)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopChartStatsUseOnlyVisiblePlotWindow(t *testing.T) {
|
||||
points := []crackTopChartPoint{
|
||||
{value: 1_000, known: true},
|
||||
{value: 10, known: true},
|
||||
{value: 20, known: true},
|
||||
{value: 30, known: true},
|
||||
}
|
||||
visible := crackTopVisibleChartPoints(points, 3)
|
||||
latest, average, peak, latestKnown, historyKnown, _, _ := crackTopChartStats(visible)
|
||||
if len(visible) != 3 || !latestKnown || !historyKnown || latest != 30 || average != 20 || peak != 30 {
|
||||
t.Fatalf("visible chart stats = len:%d latest:%v avg:%v peak:%v latest-known:%v history-known:%v", len(visible), latest, average, peak, latestKnown, historyKnown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopRecoveredDeltaUsesOnlyObservedCountEndpoints(t *testing.T) {
|
||||
history := []crackTopHistorySample{
|
||||
{Recovered: 0, CountsKnown: false},
|
||||
{Recovered: 10, CountsKnown: true},
|
||||
{Recovered: 0, CountsKnown: false},
|
||||
{Recovered: 13, CountsKnown: true},
|
||||
}
|
||||
if delta, known := crackTopRecoveredDelta(history); !known || delta != 3 {
|
||||
t.Fatalf("recovered delta across boundary gaps = %d known:%v, want 3 known", delta, known)
|
||||
}
|
||||
if delta, known := crackTopRecoveredDelta([]crackTopHistorySample{{Recovered: 13}, {Recovered: 14}}); known || delta != 0 {
|
||||
t.Fatalf("unobserved recovered endpoints = %d known:%v, want unknown", delta, known)
|
||||
}
|
||||
if delta, known := crackTopRecoveredDelta([]crackTopHistorySample{{Recovered: 13, CountsKnown: true}}); known || delta != 0 {
|
||||
t.Fatalf("single recovered endpoint = %d known:%v, want unknown", delta, known)
|
||||
}
|
||||
if delta, known := crackTopRecoveredDelta([]crackTopHistorySample{{Recovered: 13, CountsKnown: true}, {Recovered: 12, CountsKnown: true}}); known || delta != 0 {
|
||||
t.Fatalf("decreasing recovered counter = %d known:%v, want unknown", delta, known)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopChartSpanDisclosesSampleIndexedHistory(t *testing.T) {
|
||||
points := []crackTopChartPoint{
|
||||
{at: time.Unix(1_700_000_000, 0)},
|
||||
{at: time.Unix(1_700_000_001, 0)},
|
||||
{at: time.Unix(1_700_000_061, 0)},
|
||||
}
|
||||
if got := crackTopChartSpan(points); got != "1m1s/3 samples" {
|
||||
t.Fatalf("chart span = %q, want elapsed time and sample count", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopPlotDistinguishesUnknownFromObservedZero(t *testing.T) {
|
||||
plot := crackTopRenderPlot([]crackTopChartPoint{
|
||||
{value: 0, known: true},
|
||||
{known: false},
|
||||
{value: 0, known: true},
|
||||
}, 3, 3, 100)
|
||||
bottom := strings.Split(plot, "\n")[2]
|
||||
if bottom != "●·●" {
|
||||
t.Fatalf("zero/unknown/zero baseline = %q, want observed points around a grid gap", bottom)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopAuthoritativeJobActionIsJournaled(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.runningJobAction = crackTopPendingJobAction{action: crackTopJobActionPause, jobID: crackTopTestJobID}
|
||||
_, _ = model.Update(crackTopJobActionCompletedMsg{
|
||||
action: crackTopJobActionPause,
|
||||
jobID: crackTopTestJobID,
|
||||
status: clientpb.CrackJobStatus_PAUSED,
|
||||
updatedAt: 1_700_000_100,
|
||||
hasStatus: true,
|
||||
})
|
||||
if len(model.journal) != 1 || model.journal[0].label != "JOB" || model.journal[0].message != "aaaaaaaa PAUSED" {
|
||||
t.Fatalf("authoritative action journal = %#v", model.journal)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test keeps the complete action-to-authoritative-snapshot freshness transition together.
|
||||
func TestCrackTopActionMarksTaskTelemetryStaleUntilAuthoritativeSnapshot(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
initial := crackTopLiveTestSnapshot()
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: initial})
|
||||
actionUpdatedAt := initial.RefreshedAt.Add(time.Second).Unix()
|
||||
|
||||
model.runningJobAction = crackTopPendingJobAction{action: crackTopJobActionPause, jobID: crackTopTestJobID}
|
||||
_, _ = model.Update(crackTopJobActionCompletedMsg{
|
||||
action: crackTopJobActionPause,
|
||||
jobID: crackTopTestJobID,
|
||||
status: clientpb.CrackJobStatus_PAUSED,
|
||||
updatedAt: actionUpdatedAt,
|
||||
hasStatus: true,
|
||||
})
|
||||
|
||||
if !model.authoritativeSnapshotPending() {
|
||||
t.Fatal("successful lifecycle action did not mark task telemetry as awaiting an authoritative snapshot")
|
||||
}
|
||||
latest := model.history[len(model.history)-1]
|
||||
if latest.ClusterRateKnown || latest.ProgressKnown || latest.UtilizationKnown || latest.CountsKnown {
|
||||
t.Fatalf("post-action history sample presented stale task telemetry as current: %#v", latest)
|
||||
}
|
||||
plain := ansi.Strip(strings.Join([]string{
|
||||
model.renderOverviewStateCard(50, crackTopOverviewSummaryHeight),
|
||||
model.renderOverviewThroughputCard(50, crackTopOverviewSummaryHeight),
|
||||
model.renderOverviewDiagnosticsCard(50, crackTopOverviewSummaryHeight),
|
||||
model.renderOverviewMeters(150, crackTopOverviewMetersHeight),
|
||||
model.renderJobsPane(90, 8),
|
||||
model.renderWorkersPane(59, 8),
|
||||
}, "\n"))
|
||||
for _, expected := range []string{"SYNCING", "TASK DATA LAST", "RECOVERED 7 last snapshot", "task data pending", "QUEUE last 2 run", "last snapshot • syncing", "TASK DATA LAST/SYNCING", "LAST SNAPSHOT/SYNCING"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Errorf("post-action overview is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
if strings.Contains(strings.ToLower(plain), "refreshing") {
|
||||
t.Errorf("post-action overview exposed refresh progress:\n%s", plain)
|
||||
}
|
||||
|
||||
stale := crackTopLiveTestSnapshot()
|
||||
stale.RefreshedAt = stale.RefreshedAt.Add(500 * time.Millisecond)
|
||||
stale.Jobs[0].UpdatedAt = actionUpdatedAt
|
||||
stale.Stations = stale.Stations[:1]
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: stale})
|
||||
if !model.authoritativeSnapshotPending() {
|
||||
t.Fatal("equal-version pre-action snapshot prematurely cleared lifecycle synchronization state")
|
||||
}
|
||||
staleSample := model.history[len(model.history)-1]
|
||||
if staleSample.ClusterRateKnown || staleSample.ProgressKnown || staleSample.UtilizationKnown || staleSample.CountsKnown {
|
||||
t.Fatalf("client-overridden pre-action snapshot was recorded as authoritative telemetry: %#v", staleSample)
|
||||
}
|
||||
foundUnrelatedTransition := false
|
||||
for _, entry := range model.journal {
|
||||
if entry.label == "WORKER" && entry.message == "Beta Rig disconnected" {
|
||||
foundUnrelatedTransition = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundUnrelatedTransition {
|
||||
t.Fatalf("pending-action snapshot discarded an unrelated worker transition: %#v", model.journal)
|
||||
}
|
||||
|
||||
authoritative := crackTopLiveTestSnapshot()
|
||||
authoritative.RefreshedAt = authoritative.RefreshedAt.Add(2 * time.Second)
|
||||
authoritative.Jobs[0].Status = clientpb.CrackJobStatus_PAUSED
|
||||
authoritative.Jobs[0].UpdatedAt = actionUpdatedAt
|
||||
for _, task := range authoritative.Jobs[0].Tasks {
|
||||
task.State = clientpb.CrackTaskState_CRACK_TASK_QUEUED
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: authoritative})
|
||||
if model.authoritativeSnapshotPending() {
|
||||
t.Fatal("matching authoritative snapshot did not clear lifecycle synchronization state")
|
||||
}
|
||||
diagnostics := ansi.Strip(model.renderOverviewDiagnosticsCard(50, crackTopOverviewSummaryHeight))
|
||||
if strings.Contains(diagnostics, "QUEUE last") || strings.Contains(diagnostics, "SYNC action") {
|
||||
t.Fatalf("authoritative snapshot remained labeled as stale:\n%s", diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewSelectionMarkerFollowsFocusedPane(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.dashboard = buildCrackTopDashboard(crackTopLiveTestSnapshot())
|
||||
model.normalizeJobSelection()
|
||||
model.normalizeWorkerSelection()
|
||||
|
||||
jobs := ansi.Strip(model.renderJobsPane(75, 8))
|
||||
workers := ansi.Strip(model.renderWorkersPane(55, 8))
|
||||
if !strings.Contains(jobs, "› aaaaaaaa") || strings.Contains(workers, "› ") {
|
||||
t.Fatalf("job focus markers are ambiguous:\njobs:\n%s\nworkers:\n%s", jobs, workers)
|
||||
}
|
||||
|
||||
_, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyTab})
|
||||
jobs = ansi.Strip(model.renderJobsPane(75, 8))
|
||||
workers = ansi.Strip(model.renderWorkersPane(55, 8))
|
||||
if strings.Contains(jobs, "› ") || !strings.Contains(workers, "› ") {
|
||||
t.Fatalf("worker focus markers are ambiguous:\njobs:\n%s\nworkers:\n%s", jobs, workers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopJournalRecordsRecoveredJobAndWorkerTransitions(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
first := crackTopLiveTestSnapshot()
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: first})
|
||||
|
||||
second := crackTopLiveTestSnapshot()
|
||||
second.RefreshedAt = first.RefreshedAt.Add(time.Second)
|
||||
second.Jobs[0].Status = clientpb.CrackJobStatus_COMPLETED
|
||||
second.Jobs[0].ResultCount = 10
|
||||
second.Stations = second.Stations[:1]
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: second})
|
||||
|
||||
wants := map[string]string{
|
||||
"JOB": "aaaaaaaa COMPLETED",
|
||||
"WORKER": "Beta Rig disconnected",
|
||||
"RECOVERED": "+3 across loaded jobs",
|
||||
}
|
||||
for label, message := range wants {
|
||||
found := false
|
||||
for _, entry := range model.journal {
|
||||
if entry.label == label && entry.message == message {
|
||||
found = true
|
||||
if !entry.at.Equal(second.RefreshedAt) {
|
||||
t.Errorf("%s journal time = %s, want %s", label, entry.at, second.RefreshedAt)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("journal missing %s transition %q: %#v", label, message, model.journal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopJournalRecordsActiveJobRemovedByAnotherOperator(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
first := crackTopLiveTestSnapshot()
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: first})
|
||||
|
||||
second := &crackTopSnapshot{RefreshedAt: first.RefreshedAt.Add(time.Second)}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: second})
|
||||
|
||||
for _, entry := range model.journal {
|
||||
if entry.label == "JOB" && entry.message == "aaaaaaaa removed from live set" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("journal did not record externally removed active job: %#v", model.journal)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
type crackTopQueueCounts struct {
|
||||
queued int
|
||||
leased int
|
||||
running int
|
||||
}
|
||||
|
||||
func TestBuildCrackTopDashboardCountsActiveJobTaskQueueStates(t *testing.T) {
|
||||
crackTasks := func(states ...clientpb.CrackTaskState) []*clientpb.CrackTask {
|
||||
tasks := make([]*clientpb.CrackTask, 0, len(states))
|
||||
for _, state := range states {
|
||||
tasks = append(tasks, &clientpb.CrackTask{
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobs []*clientpb.CrackJob
|
||||
want crackTopQueueCounts
|
||||
}{
|
||||
{
|
||||
name: "in-progress job",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "in-progress",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: crackTasks(
|
||||
clientpb.CrackTaskState_CRACK_TASK_QUEUED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_LEASED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
),
|
||||
}},
|
||||
want: crackTopQueueCounts{queued: 1, leased: 1, running: 1},
|
||||
},
|
||||
{
|
||||
name: "paused job",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "paused",
|
||||
Status: clientpb.CrackJobStatus_PAUSED,
|
||||
Tasks: crackTasks(
|
||||
clientpb.CrackTaskState_CRACK_TASK_QUEUED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_LEASED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
),
|
||||
}},
|
||||
want: crackTopQueueCounts{queued: 1, leased: 1, running: 1},
|
||||
},
|
||||
{
|
||||
name: "legacy unspecified crack task with shard telemetry",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "legacy-crack",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: []*clientpb.CrackTask{{
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
ShardLimit: 100,
|
||||
}},
|
||||
}},
|
||||
want: crackTopQueueCounts{running: 1},
|
||||
},
|
||||
{
|
||||
name: "keyspace and auxiliary scheduler stages",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "queries",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: []*clientpb.CrackTask{
|
||||
{Kind: clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED, State: clientpb.CrackTaskState_CRACK_TASK_QUEUED},
|
||||
{Kind: clientpb.CrackTaskKind_CRACK_TASK_KEYSPACE, State: clientpb.CrackTaskState_CRACK_TASK_QUEUED},
|
||||
{Kind: clientpb.CrackTaskKind_CRACK_TASK_BENCHMARK, State: clientpb.CrackTaskState_CRACK_TASK_LEASED},
|
||||
{Kind: clientpb.CrackTaskKind_CRACK_TASK_QUERY, State: clientpb.CrackTaskState_CRACK_TASK_RUNNING},
|
||||
},
|
||||
}},
|
||||
want: crackTopQueueCounts{queued: 2, leased: 1, running: 1},
|
||||
},
|
||||
{
|
||||
name: "terminal jobs",
|
||||
jobs: []*clientpb.CrackJob{
|
||||
{ID: "completed", Status: clientpb.CrackJobStatus_COMPLETED, Tasks: crackTasks(clientpb.CrackTaskState_CRACK_TASK_QUEUED)},
|
||||
{ID: "failed", Status: clientpb.CrackJobStatus_FAILED, Tasks: crackTasks(clientpb.CrackTaskState_CRACK_TASK_LEASED)},
|
||||
{ID: "cancelled", Status: clientpb.CrackJobStatus_CANCELLED, Tasks: crackTasks(clientpb.CrackTaskState_CRACK_TASK_RUNNING)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "terminal task states",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "active-with-terminal-tasks",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: append(
|
||||
crackTasks(
|
||||
clientpb.CrackTaskState_CRACK_TASK_COMPLETED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_FAILED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_CANCELLED,
|
||||
),
|
||||
nil,
|
||||
),
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
dashboard := buildCrackTopDashboard(&crackTopSnapshot{Jobs: testCase.jobs})
|
||||
got := crackTopQueueCounts{
|
||||
queued: dashboard.QueuedTasks,
|
||||
leased: dashboard.LeasedTasks,
|
||||
running: dashboard.RunningTasks,
|
||||
}
|
||||
if got != testCase.want {
|
||||
t.Fatalf("task queue counts = %+v, want %+v", got, testCase.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/crack/internal/hashcatdisplay"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
// These views intentionally contain only the status fields used by Crack Top.
|
||||
// The production loader fills them from the lean CrackTop RPC; JSON decoding is
|
||||
// retained for compatibility with locally constructed snapshots and fixtures.
|
||||
type crackStatusView struct {
|
||||
ProgressCurrent string
|
||||
ProgressTotal string
|
||||
Devices []crackDeviceView
|
||||
}
|
||||
|
||||
type crackDeviceView struct {
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
Speed string
|
||||
Temperature string
|
||||
Utilization string
|
||||
}
|
||||
|
||||
func hashcatHashTypeName(mode int32) (string, bool) {
|
||||
return hashcatdisplay.TypeName(mode)
|
||||
}
|
||||
|
||||
func humanizeHashRate(rate uint64) string {
|
||||
return hashcatdisplay.HumanizeRate(rate)
|
||||
}
|
||||
|
||||
func crackJobSortTime(job *clientpb.CrackJob) time.Time {
|
||||
if job == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if updatedAt := job.GetUpdatedAt(); updatedAt > 0 {
|
||||
return time.Unix(updatedAt, 0)
|
||||
}
|
||||
createdAt := strings.TrimSpace(job.GetCreatedAt())
|
||||
if unix, err := strconv.ParseInt(createdAt, 10, 64); err == nil && unix > 0 {
|
||||
return time.Unix(unix, 0)
|
||||
}
|
||||
parsed, _ := time.Parse(time.RFC3339, createdAt)
|
||||
return parsed
|
||||
}
|
||||
|
||||
func crackJobTasks(job *clientpb.CrackJob) []*clientpb.CrackTask {
|
||||
if job == nil {
|
||||
return nil
|
||||
}
|
||||
return job.GetTasks()
|
||||
}
|
||||
|
||||
func crackTaskStatus(task *clientpb.CrackTask) (*crackStatusView, error) {
|
||||
if task == nil || len(bytes.TrimSpace(task.GetLatestStatusJSON())) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return parseCrackStatusJSON(task.GetLatestStatusJSON())
|
||||
}
|
||||
|
||||
func parseCrackStatusJSON(raw []byte) (*crackStatusView, error) {
|
||||
root, err := decodeLastCrackTopJSONObject(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := &crackStatusView{}
|
||||
status.ProgressCurrent, status.ProgressTotal = crackTopJSONPair(root["progress"])
|
||||
if devices, ok := root["devices"].([]any); ok {
|
||||
for _, item := range devices {
|
||||
values, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
status.Devices = append(status.Devices, crackDeviceView{
|
||||
ID: crackTopJSONText(firstCrackTopJSONValue(values, "device_id", "id")),
|
||||
Name: crackTopSafeCell(crackTopJSONText(firstCrackTopJSONValue(values, "device_name", "name"))),
|
||||
Type: crackTopSafeCell(crackTopJSONText(firstCrackTopJSONValue(values, "device_type", "type"))),
|
||||
Speed: crackTopJSONText(firstCrackTopJSONValue(values, "speed")),
|
||||
Temperature: crackTopJSONText(firstCrackTopJSONValue(values, "temp", "temperature")),
|
||||
Utilization: crackTopJSONText(firstCrackTopJSONValue(values, "util", "utilization")),
|
||||
})
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func decodeLastCrackTopJSONObject(raw []byte) (map[string]any, error) {
|
||||
decode := func(candidate []byte) (map[string]any, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(candidate))
|
||||
decoder.UseNumber()
|
||||
var last map[string]any
|
||||
for {
|
||||
value := map[string]any{}
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
break
|
||||
}
|
||||
last = value
|
||||
}
|
||||
if last == nil {
|
||||
return nil, errors.New("no JSON object")
|
||||
}
|
||||
return last, nil
|
||||
}
|
||||
|
||||
trimmed := bytes.TrimSpace(raw)
|
||||
if value, err := decode(trimmed); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
lines := bytes.Split(trimmed, []byte{'\n'})
|
||||
for index := len(lines) - 1; index >= 0; index-- {
|
||||
candidate := bytes.TrimSpace(lines[index])
|
||||
if len(candidate) == 0 {
|
||||
continue
|
||||
}
|
||||
if value, err := decode(candidate); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("hashcat status did not contain a JSON object")
|
||||
}
|
||||
|
||||
func firstCrackTopJSONValue(values map[string]any, names ...string) any {
|
||||
for _, name := range names {
|
||||
if value, ok := values[name]; ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func crackTopJSONPair(value any) (string, string) {
|
||||
items, ok := value.([]any)
|
||||
if !ok || len(items) < 2 {
|
||||
return "", ""
|
||||
}
|
||||
return crackTopJSONText(items[0]), crackTopJSONText(items[1])
|
||||
}
|
||||
|
||||
func crackTopJSONText(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return crackTopSafeCell(typed)
|
||||
case json.Number:
|
||||
return typed.String()
|
||||
case float64:
|
||||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||||
case bool:
|
||||
return strconv.FormatBool(typed)
|
||||
default:
|
||||
return crackTopSafeCell(fmt.Sprint(typed))
|
||||
}
|
||||
}
|
||||
|
||||
func valueOrDash(value string) string {
|
||||
value = crackTopSafeCell(value)
|
||||
if value == "" {
|
||||
return "-"
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
const crackTopMaxProgressTextSize = 148
|
||||
|
||||
type crackTopDashboard struct {
|
||||
Jobs []crackTopJobRow
|
||||
Workers []crackTopWorkerRow
|
||||
ActiveJobs int
|
||||
QueuedTasks int
|
||||
LeasedTasks int
|
||||
RunningTasks int
|
||||
OnlineWorkers int
|
||||
Unavailable int
|
||||
Recovered uint64
|
||||
ClusterRate uint64
|
||||
ClusterRateKnown bool
|
||||
ClusterComplete bool
|
||||
ExpectedWorkers int
|
||||
ReportingWorkers int
|
||||
Progress float64
|
||||
ProgressKnown bool
|
||||
ProgressJobs int
|
||||
}
|
||||
|
||||
type crackTopJobRow struct {
|
||||
ID string
|
||||
Status clientpb.CrackJobStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Progress float64
|
||||
ProgressKnown bool
|
||||
ProgressComplete bool
|
||||
Rate uint64
|
||||
RateKnown bool
|
||||
Workers int
|
||||
Reporting int
|
||||
Results uint64
|
||||
Mode string
|
||||
TelemetryErrors int
|
||||
}
|
||||
|
||||
type crackTopWorkerRow struct {
|
||||
ID string
|
||||
Name string
|
||||
State string
|
||||
Online bool
|
||||
Syncing bool
|
||||
JobID string
|
||||
Rate uint64
|
||||
RateKnown bool
|
||||
Devices int
|
||||
Temperature float64
|
||||
HasTemp bool
|
||||
Utilization float64
|
||||
HasUtil bool
|
||||
LastSeen time.Time
|
||||
TelemetryLive bool
|
||||
}
|
||||
|
||||
type crackTopTaskSample struct {
|
||||
jobID string
|
||||
task *clientpb.CrackTask
|
||||
status *crackStatusView
|
||||
statusErr error
|
||||
rate uint64
|
||||
rateKnown bool
|
||||
devices int
|
||||
temperature float64
|
||||
hasTemp bool
|
||||
utilization float64
|
||||
hasUtil bool
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
type crackTopProgressAccumulator struct {
|
||||
completed *big.Rat
|
||||
total *big.Rat
|
||||
}
|
||||
|
||||
func newCrackTopProgressAccumulator() *crackTopProgressAccumulator {
|
||||
return &crackTopProgressAccumulator{completed: new(big.Rat), total: new(big.Rat)}
|
||||
}
|
||||
|
||||
func (a *crackTopProgressAccumulator) add(fraction, weight *big.Rat) {
|
||||
if a == nil || fraction == nil || weight == nil || weight.Sign() <= 0 {
|
||||
return
|
||||
}
|
||||
clamped := new(big.Rat).Set(fraction)
|
||||
if clamped.Sign() < 0 {
|
||||
clamped.SetInt64(0)
|
||||
}
|
||||
if clamped.Cmp(big.NewRat(1, 1)) > 0 {
|
||||
clamped.SetInt64(1)
|
||||
}
|
||||
a.completed.Add(a.completed, new(big.Rat).Mul(clamped, weight))
|
||||
a.total.Add(a.total, weight)
|
||||
}
|
||||
|
||||
func (a *crackTopProgressAccumulator) value() (float64, bool) {
|
||||
if a == nil || a.total.Sign() <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
fraction, _ := new(big.Rat).Quo(a.completed, a.total).Float64()
|
||||
if math.IsNaN(fraction) || math.IsInf(fraction, 0) {
|
||||
return 0, false
|
||||
}
|
||||
return math.Max(0, math.Min(1, fraction)), true
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Jobs, task telemetry, and connected workers are normalized in one immutable snapshot pass.
|
||||
func buildCrackTopDashboard(snapshot *crackTopSnapshot) crackTopDashboard {
|
||||
dashboard := crackTopDashboard{}
|
||||
if snapshot == nil {
|
||||
return dashboard
|
||||
}
|
||||
|
||||
stations := make(map[string]*clientpb.Crackstation, len(snapshot.Stations))
|
||||
for _, station := range snapshot.Stations {
|
||||
if station == nil {
|
||||
continue
|
||||
}
|
||||
hostID := crackTopStationID(station)
|
||||
if hostID == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := stations[hostID]; !exists {
|
||||
stations[hostID] = station
|
||||
}
|
||||
}
|
||||
dashboard.OnlineWorkers = len(stations)
|
||||
|
||||
jobs := make([]*clientpb.CrackJob, 0, len(snapshot.Jobs))
|
||||
for _, job := range snapshot.Jobs {
|
||||
if job != nil && strings.TrimSpace(job.GetID()) != "" {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(jobs, func(i, j int) bool {
|
||||
left := crackJobSortTime(jobs[i])
|
||||
right := crackJobSortTime(jobs[j])
|
||||
if left.Equal(right) {
|
||||
return jobs[i].GetID() < jobs[j].GetID()
|
||||
}
|
||||
return left.After(right)
|
||||
})
|
||||
|
||||
globalProgress := newCrackTopProgressAccumulator()
|
||||
workerSamples := map[string]crackTopTaskSample{}
|
||||
for _, job := range jobs {
|
||||
row, samples, progress := crackTopJobTelemetry(job, stations, snapshot.RefreshedAt, snapshot.taskTelemetry)
|
||||
dashboard.Jobs = append(dashboard.Jobs, row)
|
||||
dashboard.Recovered = saturatingUint64Add(dashboard.Recovered, row.Results)
|
||||
jobStatus := job.GetStatus()
|
||||
if jobStatus == clientpb.CrackJobStatus_IN_PROGRESS || jobStatus == clientpb.CrackJobStatus_PAUSED {
|
||||
for _, task := range crackJobTasks(job) {
|
||||
if task == nil {
|
||||
continue
|
||||
}
|
||||
switch task.GetState() {
|
||||
case clientpb.CrackTaskState_CRACK_TASK_QUEUED:
|
||||
dashboard.QueuedTasks++
|
||||
case clientpb.CrackTaskState_CRACK_TASK_LEASED:
|
||||
dashboard.LeasedTasks++
|
||||
case clientpb.CrackTaskState_CRACK_TASK_RUNNING:
|
||||
dashboard.RunningTasks++
|
||||
}
|
||||
}
|
||||
}
|
||||
if jobStatus == clientpb.CrackJobStatus_IN_PROGRESS {
|
||||
dashboard.ActiveJobs++
|
||||
if row.ProgressComplete {
|
||||
dashboard.ProgressJobs++
|
||||
}
|
||||
if progress != nil {
|
||||
globalProgress.completed.Add(globalProgress.completed, progress.completed)
|
||||
globalProgress.total.Add(globalProgress.total, progress.total)
|
||||
}
|
||||
for hostID, sample := range samples {
|
||||
if hostID == "" {
|
||||
continue
|
||||
}
|
||||
if current, ok := workerSamples[hostID]; !ok || crackTopSamplePreferred(sample, current, stations[hostID], snapshot.RefreshedAt) {
|
||||
workerSamples[hostID] = sample
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dashboard.Progress, dashboard.ProgressKnown = globalProgress.value()
|
||||
dashboard.ProgressKnown = dashboard.ProgressKnown && dashboard.ProgressJobs == dashboard.ActiveJobs
|
||||
if dashboard.ActiveJobs == 0 {
|
||||
dashboard.Progress = 0
|
||||
dashboard.ProgressKnown = false
|
||||
}
|
||||
|
||||
workerIDs := make(map[string]struct{}, len(stations)+len(workerSamples))
|
||||
for hostID := range stations {
|
||||
workerIDs[hostID] = struct{}{}
|
||||
}
|
||||
for hostID := range workerSamples {
|
||||
workerIDs[hostID] = struct{}{}
|
||||
}
|
||||
for hostID := range workerIDs {
|
||||
station := stations[hostID]
|
||||
sample, hasSample := workerSamples[hostID]
|
||||
worker := crackTopWorkerRow{ID: hostID, Online: station != nil}
|
||||
if station != nil {
|
||||
worker.Name = crackTopSafeCell(station.GetName())
|
||||
if worker.Name == "" && station.GetStatus() != nil {
|
||||
worker.Name = crackTopSafeCell(station.GetStatus().GetName())
|
||||
}
|
||||
if status := station.GetStatus(); status != nil {
|
||||
worker.State = status.GetState().String()
|
||||
worker.Syncing = status.GetIsSyncing()
|
||||
worker.JobID = strings.TrimSpace(status.GetCurrentCrackJobID())
|
||||
}
|
||||
}
|
||||
if worker.Name == "" {
|
||||
worker.Name = crackTopShortID(hostID)
|
||||
}
|
||||
if worker.State == "" {
|
||||
if worker.Online {
|
||||
worker.State = "ONLINE"
|
||||
} else {
|
||||
worker.State = "OFFLINE"
|
||||
}
|
||||
}
|
||||
if crackTopStationExpectsTelemetry(station) {
|
||||
dashboard.ExpectedWorkers++
|
||||
}
|
||||
if hasSample {
|
||||
if worker.JobID == "" && !worker.Online {
|
||||
worker.JobID = sample.jobID
|
||||
}
|
||||
worker.LastSeen = sample.lastSeen
|
||||
if crackTopTaskSampleIsLive(sample, station, snapshot.RefreshedAt) {
|
||||
worker.TelemetryLive = true
|
||||
worker.Rate = sample.rate
|
||||
worker.RateKnown = sample.rateKnown
|
||||
worker.Devices = sample.devices
|
||||
worker.Temperature = sample.temperature
|
||||
worker.HasTemp = sample.hasTemp
|
||||
worker.Utilization = sample.utilization
|
||||
worker.HasUtil = sample.hasUtil
|
||||
}
|
||||
}
|
||||
if !worker.Online {
|
||||
dashboard.Unavailable++
|
||||
}
|
||||
if worker.RateKnown {
|
||||
dashboard.ClusterRate = saturatingUint64Add(dashboard.ClusterRate, worker.Rate)
|
||||
dashboard.ClusterRateKnown = true
|
||||
dashboard.ReportingWorkers++
|
||||
}
|
||||
dashboard.Workers = append(dashboard.Workers, worker)
|
||||
}
|
||||
dashboard.ClusterComplete = dashboard.ReportingWorkers == dashboard.ExpectedWorkers
|
||||
dashboard.ClusterRateKnown = dashboard.ReportingWorkers > 0 || dashboard.ClusterComplete
|
||||
|
||||
sort.SliceStable(dashboard.Workers, func(i, j int) bool {
|
||||
left, right := dashboard.Workers[i], dashboard.Workers[j]
|
||||
if left.Online != right.Online {
|
||||
return left.Online
|
||||
}
|
||||
if left.RateKnown != right.RateKnown {
|
||||
return left.RateKnown
|
||||
}
|
||||
if left.Rate != right.Rate {
|
||||
return left.Rate > right.Rate
|
||||
}
|
||||
leftName := strings.ToLower(left.Name)
|
||||
rightName := strings.ToLower(right.Name)
|
||||
if leftName != rightName {
|
||||
return leftName < rightName
|
||||
}
|
||||
return left.ID < right.ID
|
||||
})
|
||||
return dashboard
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Job telemetry combines task state, liveness, progress, and device metrics in one aggregation pass.
|
||||
func crackTopJobTelemetry(
|
||||
job *clientpb.CrackJob,
|
||||
stations map[string]*clientpb.Crackstation,
|
||||
now time.Time,
|
||||
taskTelemetry map[string]crackTopTaskTelemetry,
|
||||
) (crackTopJobRow, map[string]crackTopTaskSample, *crackTopProgressAccumulator) {
|
||||
row := crackTopJobRow{
|
||||
ID: strings.TrimSpace(job.GetID()),
|
||||
Status: job.GetStatus(),
|
||||
CreatedAt: crackTopParseTime(job.GetCreatedAt()),
|
||||
UpdatedAt: crackTopParseUnix(job.GetUpdatedAt()),
|
||||
Results: job.GetResultCount(),
|
||||
Mode: crackTopJobMode(job),
|
||||
}
|
||||
progress := newCrackTopProgressAccumulator()
|
||||
samples := map[string]crackTopTaskSample{}
|
||||
workers := map[string]struct{}{}
|
||||
workTasks := 0
|
||||
knownProgressTasks := 0
|
||||
allProgressWeightsKnown := true
|
||||
|
||||
for _, task := range crackJobTasks(job) {
|
||||
if !crackTopCrackTask(task, taskTelemetry) {
|
||||
continue
|
||||
}
|
||||
workTasks++
|
||||
hostID := strings.TrimSpace(task.GetHostUUID())
|
||||
if hostID != "" {
|
||||
workers[hostID] = struct{}{}
|
||||
}
|
||||
status, statusErr := crackTaskStatus(task)
|
||||
progressExact := task.GetShardSkip() == 0
|
||||
if metadata, ok := taskTelemetry[strings.TrimSpace(task.GetID())]; ok {
|
||||
status = metadata.status
|
||||
statusErr = nil
|
||||
progressExact = metadata.progressExact || task.GetShardSkip() == 0
|
||||
if metadata.parseErr {
|
||||
statusErr = errors.New("crack status telemetry could not be parsed")
|
||||
}
|
||||
}
|
||||
if statusErr != nil {
|
||||
row.TelemetryErrors++
|
||||
}
|
||||
fraction, weight, known := crackTopTaskProgress(task, status, progressExact)
|
||||
if known {
|
||||
progress.add(fraction, weight)
|
||||
knownProgressTasks++
|
||||
} else if weight := crackTopAssignedShardWeight(task); weight != nil {
|
||||
// Unknown work may already be complete, so zero is only a lower
|
||||
// bound. Including its weight prevents known small shards from
|
||||
// overstating the job's completion percentage.
|
||||
progress.add(new(big.Rat), weight)
|
||||
} else {
|
||||
allProgressWeightsKnown = false
|
||||
}
|
||||
sample := crackTopTaskSample{
|
||||
jobID: row.ID,
|
||||
task: task,
|
||||
status: status,
|
||||
statusErr: statusErr,
|
||||
lastSeen: crackTopTaskTime(task),
|
||||
}
|
||||
if status != nil {
|
||||
sample.rate, sample.rateKnown = crackTopStatusRate(status)
|
||||
sample.devices = len(status.Devices)
|
||||
sample.temperature, sample.hasTemp = crackTopStatusMaximum(status, func(device crackDeviceView) string { return device.Temperature })
|
||||
sample.utilization, sample.hasUtil = crackTopStatusMaximum(status, func(device crackDeviceView) string { return device.Utilization })
|
||||
}
|
||||
if hostID != "" {
|
||||
if current, ok := samples[hostID]; !ok || crackTopSamplePreferred(sample, current, stations[hostID], now) {
|
||||
samples[hostID] = sample
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
row.Workers = len(workers)
|
||||
row.Progress, row.ProgressKnown = progress.value()
|
||||
row.ProgressKnown = row.ProgressKnown && workTasks > 0 && allProgressWeightsKnown
|
||||
row.ProgressComplete = row.ProgressKnown && knownProgressTasks == workTasks
|
||||
if job.GetStatus() == clientpb.CrackJobStatus_COMPLETED {
|
||||
row.Progress = 1
|
||||
row.ProgressKnown = true
|
||||
row.ProgressComplete = true
|
||||
}
|
||||
if job.GetStatus() == clientpb.CrackJobStatus_IN_PROGRESS {
|
||||
for hostID, sample := range samples {
|
||||
if crackTopTaskSampleIsLive(sample, stations[hostID], now) && sample.rateKnown {
|
||||
row.Rate = saturatingUint64Add(row.Rate, sample.rate)
|
||||
row.RateKnown = true
|
||||
row.Reporting++
|
||||
}
|
||||
}
|
||||
}
|
||||
return row, samples, progress
|
||||
}
|
||||
|
||||
func crackTopTaskProgress(task *clientpb.CrackTask, status *crackStatusView, progressExact bool) (*big.Rat, *big.Rat, bool) {
|
||||
if task == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
weight := crackTopAssignedShardWeight(task)
|
||||
if weight == nil {
|
||||
weight = new(big.Rat)
|
||||
}
|
||||
state := task.GetState()
|
||||
if state == clientpb.CrackTaskState_CRACK_TASK_COMPLETED {
|
||||
if weight.Sign() <= 0 && status != nil {
|
||||
_, total, ok := crackTopProgressPair(status)
|
||||
if ok {
|
||||
weight.Set(total)
|
||||
}
|
||||
}
|
||||
if weight.Sign() > 0 {
|
||||
return big.NewRat(1, 1), weight, true
|
||||
}
|
||||
}
|
||||
|
||||
current, total, ok := crackTopProgressPair(status)
|
||||
if ok && (task.GetShardSkip() == 0 || progressExact) {
|
||||
fraction := new(big.Rat).Quo(current, total)
|
||||
if weight.Sign() <= 0 {
|
||||
weight.Set(total)
|
||||
}
|
||||
return fraction, weight, weight.Sign() > 0
|
||||
}
|
||||
if weight.Sign() > 0 && (state == clientpb.CrackTaskState_CRACK_TASK_QUEUED || state == clientpb.CrackTaskState_CRACK_TASK_LEASED) {
|
||||
return new(big.Rat), weight, true
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
func crackTopAssignedShardWeight(task *clientpb.CrackTask) *big.Rat {
|
||||
if task == nil || task.GetShardLimit() == 0 {
|
||||
return nil
|
||||
}
|
||||
return new(big.Rat).SetInt(new(big.Int).SetUint64(task.GetShardLimit()))
|
||||
}
|
||||
|
||||
func crackTopProgressPair(status *crackStatusView) (*big.Rat, *big.Rat, bool) {
|
||||
if status == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
current, currentOK := crackTopRat(status.ProgressCurrent)
|
||||
total, totalOK := crackTopRat(status.ProgressTotal)
|
||||
if !currentOK || !totalOK || total.Sign() <= 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
return current, total, true
|
||||
}
|
||||
|
||||
func crackTopRat(value string) (*big.Rat, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || len(value) > crackTopMaxProgressTextSize {
|
||||
return nil, false
|
||||
}
|
||||
for _, character := range value {
|
||||
if character < '0' || character > '9' {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
result := new(big.Rat)
|
||||
if _, ok := result.SetString(value); !ok {
|
||||
return nil, false
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
func crackTopStatusRate(status *crackStatusView) (uint64, bool) {
|
||||
if status == nil {
|
||||
return 0, false
|
||||
}
|
||||
var total uint64
|
||||
found := false
|
||||
for _, device := range status.Devices {
|
||||
rate, err := strconv.ParseUint(strings.TrimSpace(device.Speed), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
total = saturatingUint64Add(total, rate)
|
||||
}
|
||||
return total, found
|
||||
}
|
||||
|
||||
func crackTopStatusMaximum(status *crackStatusView, value func(crackDeviceView) string) (float64, bool) {
|
||||
if status == nil || value == nil {
|
||||
return 0, false
|
||||
}
|
||||
maximum := 0.0
|
||||
found := false
|
||||
for _, device := range status.Devices {
|
||||
number, err := strconv.ParseFloat(strings.TrimSpace(value(device)), 64)
|
||||
if err != nil || number < 0 || math.IsNaN(number) || math.IsInf(number, 0) {
|
||||
continue
|
||||
}
|
||||
if !found || number > maximum {
|
||||
maximum = number
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return maximum, found
|
||||
}
|
||||
|
||||
func crackTopTaskSampleIsLive(sample crackTopTaskSample, station *clientpb.Crackstation, now time.Time) bool {
|
||||
if station == nil || sample.task == nil || sample.statusErr != nil || sample.task.GetState() != clientpb.CrackTaskState_CRACK_TASK_RUNNING {
|
||||
return false
|
||||
}
|
||||
leaseExpiresAt := sample.task.GetLeaseExpiresAt()
|
||||
if now.IsZero() || leaseExpiresAt <= now.Unix() {
|
||||
return false
|
||||
}
|
||||
status := station.GetStatus()
|
||||
if status == nil {
|
||||
return false
|
||||
}
|
||||
if status.GetIsSyncing() || status.GetState() != clientpb.States_CRACKING {
|
||||
return false
|
||||
}
|
||||
currentJobID := strings.TrimSpace(status.GetCurrentCrackJobID())
|
||||
return currentJobID != "" && currentJobID == sample.jobID
|
||||
}
|
||||
|
||||
func crackTopStationExpectsTelemetry(station *clientpb.Crackstation) bool {
|
||||
if station == nil {
|
||||
return false
|
||||
}
|
||||
status := station.GetStatus()
|
||||
if status == nil {
|
||||
// The station is connected, but without status we cannot prove it is
|
||||
// idle. Keep the cluster total unknown instead of reporting a false zero.
|
||||
return true
|
||||
}
|
||||
if status.GetIsSyncing() {
|
||||
return false
|
||||
}
|
||||
return status.GetState() == clientpb.States_CRACKING
|
||||
}
|
||||
|
||||
func crackTopSamplePreferred(left, right crackTopTaskSample, station *clientpb.Crackstation, now time.Time) bool {
|
||||
leftLive := crackTopTaskSampleIsLive(left, station, now)
|
||||
rightLive := crackTopTaskSampleIsLive(right, station, now)
|
||||
if leftLive != rightLive {
|
||||
return leftLive
|
||||
}
|
||||
return crackTopSampleNewer(left, right)
|
||||
}
|
||||
|
||||
func crackTopSampleNewer(left, right crackTopTaskSample) bool {
|
||||
if !left.lastSeen.Equal(right.lastSeen) {
|
||||
return left.lastSeen.After(right.lastSeen)
|
||||
}
|
||||
leftRunning := left.task != nil && left.task.GetState() == clientpb.CrackTaskState_CRACK_TASK_RUNNING
|
||||
rightRunning := right.task != nil && right.task.GetState() == clientpb.CrackTaskState_CRACK_TASK_RUNNING
|
||||
if leftRunning != rightRunning {
|
||||
return leftRunning
|
||||
}
|
||||
if left.task == nil || right.task == nil {
|
||||
return left.task != nil
|
||||
}
|
||||
return left.task.GetAttempt() > right.task.GetAttempt()
|
||||
}
|
||||
|
||||
func crackTopCrackTask(task *clientpb.CrackTask, taskTelemetry map[string]crackTopTaskTelemetry) bool {
|
||||
if task == nil {
|
||||
return false
|
||||
}
|
||||
switch task.GetKind() {
|
||||
case clientpb.CrackTaskKind_CRACK_TASK_CRACK:
|
||||
return true
|
||||
case clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED:
|
||||
if task.GetShardLimit() != 0 || len(task.GetLatestStatusJSON()) != 0 {
|
||||
return true
|
||||
}
|
||||
metadata, ok := taskTelemetry[strings.TrimSpace(task.GetID())]
|
||||
return ok && (metadata.status != nil || metadata.parseErr)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func crackTopStationID(station *clientpb.Crackstation) string {
|
||||
if station == nil {
|
||||
return ""
|
||||
}
|
||||
if hostID := strings.TrimSpace(station.GetHostUUID()); hostID != "" {
|
||||
return hostID
|
||||
}
|
||||
if status := station.GetStatus(); status != nil {
|
||||
if hostID := strings.TrimSpace(status.GetHostUUID()); hostID != "" {
|
||||
return hostID
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(station.GetID())
|
||||
}
|
||||
|
||||
func crackTopTaskTime(task *clientpb.CrackTask) time.Time {
|
||||
if task == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
for _, value := range []int64{task.GetLastHeartbeatAt(), task.GetUpdatedAt(), task.GetStartedAt(), task.GetCreatedAt()} {
|
||||
if value > 0 {
|
||||
return time.Unix(value, 0)
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func crackTopParseTime(value string) time.Time {
|
||||
parsed, _ := time.Parse(time.RFC3339, strings.TrimSpace(value))
|
||||
return parsed
|
||||
}
|
||||
|
||||
func crackTopParseUnix(value int64) time.Time {
|
||||
if value <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(value, 0)
|
||||
}
|
||||
|
||||
func crackTopJobMode(job *clientpb.CrackJob) string {
|
||||
if job == nil || job.GetCommand() == nil {
|
||||
return ""
|
||||
}
|
||||
mode := int32(job.GetCommand().GetHashType())
|
||||
if job.GetCommand().HashMode != nil {
|
||||
mode = int32(job.GetCommand().GetHashMode())
|
||||
}
|
||||
if name, ok := hashcatHashTypeName(mode); ok {
|
||||
return name
|
||||
}
|
||||
return "mode " + strconv.FormatInt(int64(mode), 10)
|
||||
}
|
||||
|
||||
func crackTopShortID(value string) string {
|
||||
value = crackTopSafeCell(value)
|
||||
if value == "" {
|
||||
return "unknown"
|
||||
}
|
||||
if index := strings.IndexByte(value, '-'); index > 0 {
|
||||
value = value[:index]
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) > 8 {
|
||||
value = string(runes[:8])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func crackTopSafeCell(value string) string {
|
||||
value = ansi.Strip(value)
|
||||
value = strings.Map(func(r rune) rune {
|
||||
if !unicode.IsPrint(r) {
|
||||
return -1
|
||||
}
|
||||
if unicode.IsSpace(r) && r != ' ' {
|
||||
return ' '
|
||||
}
|
||||
return r
|
||||
}, value)
|
||||
value = strings.TrimSpace(value)
|
||||
const maximumRunes = 96
|
||||
runes := []rune(value)
|
||||
if len(runes) > maximumRunes {
|
||||
value = string(runes[:maximumRunes-1]) + "…"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func saturatingUint64Add(left, right uint64) uint64 {
|
||||
if math.MaxUint64-left < right {
|
||||
return math.MaxUint64
|
||||
}
|
||||
return left + right
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+598
-139
@@ -1,143 +1,602 @@
|
||||
package creds
|
||||
|
||||
// List of all available hash-types, for completions.
|
||||
// Unfortunately this list has to be maintained manually,
|
||||
// until someone devises a way to do so automatically.
|
||||
// hashTypes maps protobuf enum identifiers to the exact human-readable
|
||||
// names reported by the bundled Hashcat v7.1.2-armory.1 modules at commit
|
||||
// deadd52de22e2753e9caaff6d1b92e5a22a35626.
|
||||
var hashTypes = map[string]string{
|
||||
"MD5": "MD5", // Enum 0 in proto file.
|
||||
|
||||
"MD4": "MD4",
|
||||
"SHA1": "SHA1",
|
||||
"SHA2_224": "SHA2-224",
|
||||
"SHA2_256": "SHA2-256",
|
||||
"SHA2_384": "SHA2-384",
|
||||
"SHA2_512": "SHA2-512",
|
||||
"SHA3_224": "SHA3-224",
|
||||
"SHA3_256": "SHA3-256",
|
||||
"SHA3_384": "SHA3-384",
|
||||
"SHA3_512": "SHA3-512",
|
||||
"RIPEMD_160": "RIPEMD-160",
|
||||
"BLAKE2B_256": "BLAKE2b-512",
|
||||
"GOST_R_32_11_2012_256": "GOST R 34.11-2012 (Streebog) 256-bit, big-endian",
|
||||
"GOST_R_32_11_2012_512": "GOST R 34.11-2012 (Streebog) 512-bit, big-endian",
|
||||
"GOST_R_34_11_94": "GOST R 34.11-94",
|
||||
"GPG": "GPG (AES-128/AES-256 (SHA-1($pass)))",
|
||||
"HALF_MD5": "Half MD5",
|
||||
"KECCAK_224": "Keccak-224",
|
||||
"KECCAK_256": "Keccak-256",
|
||||
"KECCAK_384": "Keccak-384",
|
||||
"KECCAK_512": "Keccak-512",
|
||||
"WHIRLPOOL": "Whirlpool",
|
||||
"SIPHASH": "SipHash",
|
||||
"MD5_UTF16LE": "md5(utf16le($pass))",
|
||||
"SHA1_UTF16LE": "sha1(utf16le($pass))",
|
||||
"SHA256_UTF16LE": "sha256(utf16le($pass))",
|
||||
"SHA384_UTF16LE": "sha384(utf16le($pass))",
|
||||
"SHA512_UTF16LE": "sha512(utf16le($pass))",
|
||||
"BLAKE2B_512_PW_SALT": "BLAKE2b-512($pass.$salt)",
|
||||
"BLAKE2B_512_SALT_PW": "BLAKE2b-512($salt.$pass)",
|
||||
"MD5_PW_SALT": "md5($pass.$salt)",
|
||||
"MD5_SALT_PW": "md5($salt.$pass)",
|
||||
"MD5_SALT_PW_SALT": "md5($salt.$pass.$salt)",
|
||||
"MD5_SALT_MD5_PW": "md5($salt.md5($pass))",
|
||||
|
||||
// ... TODO fill in the rest ...,
|
||||
|
||||
"CRC32": "CRC32",
|
||||
"CRC32C": "CRC32B",
|
||||
"CRC64Jones": "CRC64-Jones",
|
||||
"JAVA_OBJECT": "Java object",
|
||||
"MURMUR": "MurmurHash",
|
||||
"MURMUR3": "MurmurHash3",
|
||||
"THREE_DES": "3DES (PT = $salt, key = $pass)",
|
||||
"DES": "DES (PT = $salt, key = $pass)",
|
||||
"AES_128_ECB": "AES-128-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_192_ECB": "AES-192-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_256_ECB": "AES-256-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"CHA_CHA_20": "ChaCha20",
|
||||
"LINUX_KERNEL_CRYPTO_API_24": "Linux Kernel Crypto API (2.4)",
|
||||
"SKIP_32": "Skip32 (PT = $salt, key = $pass)",
|
||||
"PBKDF2_HMAC_MD5": "PBKDF2-HMAC-MD5",
|
||||
"PBKDF2_HMAC_SHA1": "PBKDF2-HMAC-SHA1",
|
||||
"PBKDF2_HMAC_SHA256": "PBKDF2-HMAC-SHA256",
|
||||
"PBKDF2_HMAC_SHA512": "PBKDF2-HMAC-SHA512",
|
||||
"SCRYPT": "scrypt",
|
||||
"PHPASS": "phpass",
|
||||
"TACACS_PLUS": "TACACS+",
|
||||
"SIP_DIGEST": "SIP digest authentication (MD5)",
|
||||
"IKE_MD5": "IKE-PSK MD5",
|
||||
"IKE_SHA1": "IKE-PSK SHA1",
|
||||
"SNMP_V3_HMAC_MD5_96": "SNMPv3 HMAC-MD5-96",
|
||||
"SNMP_V3_HMAC_MD5_96__SHA1_96": "SNMPv3 HMAC-MD5-96 / HMAC-SHA1-96",
|
||||
"SNMP_V3_HMAC_SHA1_96": "SNMPv3 HMAC-SHA1-96",
|
||||
"SNMP_V3_HMAC_SHA224_128": "SNMPv3 HMAC-SHA224-128",
|
||||
"SNMP_V3_HMAC_SHA256_192": "SNMPv3 HMAC-SHA256-192",
|
||||
"SNMP_V3_HMAC_SHA384_256": "SNMPv3 HMAC-SHA384-256",
|
||||
"SNMP_V3_HMAC_SHA512_384": "SNMPv3 HMAC-SHA512-384",
|
||||
"WPA_EAPOL_PBKDF2": "WPA-EAPOL-PBKDF2",
|
||||
"WPA_EAPOL_PMK": "WPA-EAPOL-PMK",
|
||||
"WPA_PBKDF2_PMKID_EAPOL": "WPA-PBKDF2-PMKID+EAPOL",
|
||||
"WPA_PMK_PMKID_EAPOL": "WPA-PMK-PMKID+EAPOL",
|
||||
"WPA_PMKID_PBKDF2": "WPA-PMKID-PBKDF2",
|
||||
"WPA_PMKID_PMK": "WPA-PMKID-PMK",
|
||||
"IPMI2_PAKP_HMAC_SHA1": "IPMI2 RAKP HMAC-SHA1",
|
||||
"CRAM_MD5": "CRAM-MD5",
|
||||
"JWT": "JWT (JSON Web Token)",
|
||||
"RADMIN_3": "Radmin3",
|
||||
"KERBEROS_17_TGS_REP": "Kerberos 5, etype 17, TGS-REP",
|
||||
"KERBEROS_17_PREAUTH": "Kerberos 5, etype 17, Pre-Auth",
|
||||
"KERBEROS_17_DB": "Kerberos 5, etype 17, DB",
|
||||
"KERBEROS_18_TGS_REP": "Kerberos 5, etype 18, TGS-REP",
|
||||
"KERBEROS_18_PREAUTH": "Kerberos 5, etype 18, Pre-Auth",
|
||||
"KERBEROS_18_DB": "Kerberos 5, etype 18, DB",
|
||||
"KERBEROS_23_SA_REQ_PREAUTH": "Kerberos 5, etype 23, AS-REQ Pre-Auth",
|
||||
"KERBEROS_23_TGS_REP": "Kerberos 5, etype 23, TGS-REP",
|
||||
"KERBEROS_23_AS_REP": "Kerberos 5, etype 23, AS-REP",
|
||||
"NET_NTLM_V1": "NetNTLMv1 / NetNTLMv1+ESS",
|
||||
"NET_NTLM_V1_NT": "NetNTLMv1 / NetNTLMv1+ESS (NT)",
|
||||
"NET_NTLM_V2": "NetNTLMv2",
|
||||
"NET_NTLM_V2_NT": "NetNTLMv2 (NT)",
|
||||
"FLASK": "Flask Session Cookie ($salt.$salt.$pass)",
|
||||
"ISCSI_CHAP": "iSCSI CHAP authentication, MD5(CHAP)",
|
||||
"RACF": "Resource Access Control Facility",
|
||||
"AIX_SMD5": "AIX MD5",
|
||||
"AIX_SSHA1": "AIX SHA-1",
|
||||
"AIX_SSHA256": "AIX SHA-256",
|
||||
"AIX_SSHA512": "AIX SHA-512",
|
||||
"LM": "LM",
|
||||
"QNX_MD5": "QNX /etc/shadow (MD5)",
|
||||
"QNX_SHA256": "QNX /etc/shadow (SHA256)",
|
||||
"QNX_SHA512": "QNX /etc/shadow (SHA512)",
|
||||
"DPAPI_V1_CTX_1_AND_2": "DPAPI masterkey file v1 (context 1 and 2)",
|
||||
"DPAPI_V1_CTX_3": "DPAPI masterkey file v1 (context 3)",
|
||||
"DPAPI_V2_CTX_1_AND_2": "DPAPI masterkey file v2 (context 1)",
|
||||
"DPAPI_V2_CTX_3": "DPAPI masterkey file v2 (context 3)",
|
||||
"GRUB_2": "GRUB 2",
|
||||
"MS_AZURE_SYNC": "MS-AzureSync PBKDF2-HMAC-SHA256",
|
||||
"BSDI_CRYPT": "BSDi Crypt, Extended DES",
|
||||
"NTLM": "NTLM",
|
||||
"RADMIN2": "Radmin2",
|
||||
"SAMSUNG_ANDROID": "Samsung Android Password/PIN",
|
||||
"WINDOWS_HELLO_PIN": "Windows Hello PIN/Password",
|
||||
"WINDOWS_PHONE": "Windows Phone 8+ PIN/Password",
|
||||
"CISCO_ASA_MD5": "Cisco-ASA MD5",
|
||||
"CISCO_IOS_PBKDF2_SHA256": "Cisco-IOS $8$ (PBKDF2-SHA256)",
|
||||
"CISCO_IOS_SCRYPT": "Cisco-IOS $9$ (scrypt)",
|
||||
"CISCO_PIX_MD5": "Cisco-Pix MD5",
|
||||
"CITRIX_NETSCALER_SHA1": "Citrix NetScaler (SHA1)",
|
||||
"CITRIX_NETSCALER_SHA512": "Citrix NetScaler (SHA512)",
|
||||
"DCC": "Domain Cached Credentials (DCC), MS Cache",
|
||||
"DCC2": "Domain Cached Credentials 2 (DCC2), MS Cache 2",
|
||||
|
||||
// ...
|
||||
|
||||
"MACOS_10_8": "macOS v10.8+ (PBKDF2-SHA512)",
|
||||
|
||||
// ...
|
||||
|
||||
// INVALID = 9999; // Invalid hash type
|
||||
|
||||
// Out of order
|
||||
"BCRYPT_UNIX": "bcrypt",
|
||||
"SHA512_CRYPT_UNIX": "sha512crypt $6$, SHA512 (Unix)",
|
||||
"MD5": "MD5",
|
||||
"MD5_PW_SALT": "md5($pass.$salt)",
|
||||
"JOOMLA_2_5_18": "Joomla < 2.5.18",
|
||||
"POSTGRESQL": "PostgreSQL",
|
||||
"MD5_SALT_PW": "md5($salt.$pass)",
|
||||
"OSCOMMERCE_XT_COMMERCE": "osCommerce, xt:Commerce",
|
||||
"JUNIPER_NETSCREEN_SSG_SCREENOS": "Juniper NetScreen/SSG (ScreenOS)",
|
||||
"SKYPE": "Skype",
|
||||
"SOLARWINDS_SERV_U": "SolarWinds Serv-U",
|
||||
"MD5_UTF16LE_PASS_SALT": "md5(utf16le($pass).$salt)",
|
||||
"MD5_SALT_UTF16LE_PASS": "md5($salt.utf16le($pass))",
|
||||
"HMAC_MD5_KEY_PASS": "HMAC-MD5 (key = $pass)",
|
||||
"HMAC_MD5_KEY_SALT": "HMAC-MD5 (key = $salt)",
|
||||
"MD5_UTF16LE": "md5(utf16le($pass))",
|
||||
"SHA1": "SHA1",
|
||||
"NSLDAP_SHA_1_BASE64_NETSCAPE_LDAP_SHA": "nsldap, SHA-1(Base64), Netscape LDAP SHA",
|
||||
"SHA1_PASS_SALT": "sha1($pass.$salt)",
|
||||
"NSLDAPS_SSHA_1_BASE64_NETSCAPE_LDAP_SSHA": "nsldaps, SSHA-1(Base64), Netscape LDAP SSHA",
|
||||
"ORACLE_S_TYPE_ORACLE_11": "Oracle S: Type (Oracle 11+)",
|
||||
"SHA1_SALT_PASS": "sha1($salt.$pass)",
|
||||
"SMF_SIMPLE_MACHINES_FORUM_V1_1": "SMF (Simple Machines Forum) > v1.1",
|
||||
"MACOS_V10_4_MACOS_V10_5_MACOS_V10_6": "macOS v10.4, macOS v10.5, macOS v10.6",
|
||||
"DJANGO_SHA_1": "Django (SHA-1)",
|
||||
"ARUBAOS": "ArubaOS",
|
||||
"SHA1_UTF16LE_PASS_SALT": "sha1(utf16le($pass).$salt)",
|
||||
"MSSQL_2000": "MSSQL (2000)",
|
||||
"MSSQL_2005": "MSSQL (2005)",
|
||||
"PEOPLESOFT": "PeopleSoft",
|
||||
"SHA1_SALT_UTF16LE_PASS": "sha1($salt.utf16le($pass))",
|
||||
"EPISERVER_6_X_NET_4_MODE_141": "Episerver 6.x < .NET 4",
|
||||
"HMAC_SHA1_KEY_PASS": "HMAC-SHA1 (key = $pass)",
|
||||
"HMAC_SHA1_KEY_SALT": "HMAC-SHA1 (key = $salt)",
|
||||
"SHA1_UTF16LE": "sha1(utf16le($pass))",
|
||||
"MYSQL323": "MySQL323",
|
||||
"MYSQL4_1_MYSQL5": "MySQL4.1/MySQL5",
|
||||
"PHPASS": "phpass",
|
||||
"MD5CRYPT_MD5_UNIX_CISCO_IOS_1_MD5": "md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)",
|
||||
"JUNIPER_IVE": "Juniper IVE",
|
||||
"BLAKE2B_256": "BLAKE2b-512",
|
||||
"BLAKE2B_512_PW_SALT": "BLAKE2b-512($pass.$salt)",
|
||||
"BLAKE2B_512_SALT_PW": "BLAKE2b-512($salt.$pass)",
|
||||
"MD4": "MD4",
|
||||
"NTLM": "NTLM",
|
||||
"DCC": "Domain Cached Credentials (DCC), MS Cache",
|
||||
"SHA2_224": "SHA2-224",
|
||||
"SHA224_PASS_SALT": "sha224($pass.$salt)",
|
||||
"SHA224_SALT_PASS": "sha224($salt.$pass)",
|
||||
"SHA2_256": "SHA2-256",
|
||||
"SHA256_PASS_SALT": "sha256($pass.$salt)",
|
||||
"SSHA_256_BASE64_LDAP_SSHA256": "SSHA-256(Base64), LDAP {SSHA256}",
|
||||
"SHA256_SALT_PASS": "sha256($salt.$pass)",
|
||||
"HMAILSERVER": "hMailServer",
|
||||
"SHA256_UTF16LE_PASS_SALT": "sha256(utf16le($pass).$salt)",
|
||||
"SHA256_SALT_UTF16LE_PASS": "sha256($salt.utf16le($pass))",
|
||||
"EPISERVER_6_X_NET_4_MODE_1441": "Episerver 6.x >= .NET 4",
|
||||
"HMAC_SHA256_KEY_PASS": "HMAC-SHA256 (key = $pass)",
|
||||
"HMAC_SHA256_KEY_SALT": "HMAC-SHA256 (key = $salt)",
|
||||
"SHA256_UTF16LE": "sha256(utf16le($pass))",
|
||||
"DESCRYPT_DES_UNIX_TRADITIONAL_DES": "descrypt, DES (Unix), Traditional DES",
|
||||
"APACHE_APR1_MD5_MD5APR1_MD5_APR": "Apache $apr1$ MD5, md5apr1, MD5 (APR)",
|
||||
"SHA2_512": "SHA2-512",
|
||||
"SHA512_PASS_SALT": "sha512($pass.$salt)",
|
||||
"SSHA_512_BASE64_LDAP_SSHA512": "SSHA-512(Base64), LDAP {SSHA512}",
|
||||
"SHA512_SALT_PASS": "sha512($salt.$pass)",
|
||||
"MACOS_V10_7": "macOS v10.7",
|
||||
"SHA512_UTF16LE_PASS_SALT": "sha512(utf16le($pass).$salt)",
|
||||
"MSSQL_2012_2014": "MSSQL (2012, 2014)",
|
||||
"SHA512_SALT_UTF16LE_PASS": "sha512($salt.utf16le($pass))",
|
||||
"HMAC_SHA512_KEY_PASS": "HMAC-SHA512 (key = $pass)",
|
||||
"HMAC_SHA512_KEY_SALT": "HMAC-SHA512 (key = $salt)",
|
||||
"SHA512_UTF16LE": "sha512(utf16le($pass))",
|
||||
"SHA512_CRYPT_UNIX": "sha512crypt $6$, SHA512 (Unix)",
|
||||
"STDOUT": "STDOUT",
|
||||
"DCC2": "Domain Cached Credentials 2 (DCC2), MS Cache 2",
|
||||
"CISCO_PIX_MD5": "Cisco-PIX MD5",
|
||||
"CISCO_ASA_MD5": "Cisco-ASA MD5",
|
||||
"WPA_EAPOL_PBKDF2": "WPA-EAPOL-PBKDF2",
|
||||
"WPA_EAPOL_PMK": "WPA-EAPOL-PMK",
|
||||
"MD5_MD5_PASS": "md5(md5($pass))",
|
||||
"VBULLETIN_V3_8_5_MODE_2611": "vBulletin < v3.8.5",
|
||||
"PHPS": "PHPS",
|
||||
"MD5_MD5_PASS_SALT": "md5(md5($pass.$salt))",
|
||||
"VBULLETIN_V3_8_5_MODE_2711": "vBulletin >= v3.8.5",
|
||||
"MYBB_1_2_IPB2_INVISION_POWER_BOARD": "MyBB 1.2+, IPB2+ (Invision Power Board)",
|
||||
"LM": "LM",
|
||||
"ORACLE_H_TYPE_ORACLE_7": "Oracle H: Type (Oracle 7+)",
|
||||
"BCRYPT_UNIX": "bcrypt $2*$, Blowfish (Unix)",
|
||||
"MD5_MD5_MD5_PASS": "md5(md5(md5($pass)))",
|
||||
"MD5_MD5_MD5_PASS_SALT": "md5(md5(md5($pass)).$salt)",
|
||||
"MD5_SALT_MD5_PW": "md5($salt.md5($pass))",
|
||||
"MEDIAWIKI_B_TYPE": "MediaWiki B type",
|
||||
"MD5_SALT1_STRTOUPPER_MD5_SALT2_PASS": "md5($salt1.strtoupper(md5($salt2.$pass)))",
|
||||
"MD5_SALT_PW_SALT": "md5($salt.$pass.$salt)",
|
||||
"MD5_MD5_PASS_MD5_SALT": "md5(md5($pass).md5($salt))",
|
||||
"MD5_SALT_MD5_SALT_PASS": "md5($salt.md5($salt.$pass))",
|
||||
"MD5_SALT_MD5_PASS_SALT_MODE_4110": "md5($salt.md5($pass.$salt))",
|
||||
"MD5_STRTOUPPER_MD5_PASS": "md5(strtoupper(md5($pass)))",
|
||||
"MD5_SHA1_PASS": "md5(sha1($pass))",
|
||||
"MD5_SHA1_PASS_SALT_MODE_4410": "md5(sha1($pass).$salt)",
|
||||
"MD5_SHA1_PASS_SALT_MODE_4420": "md5(sha1($pass.$salt))",
|
||||
"MD5_SHA1_SALT_PASS": "md5(sha1($salt.$pass))",
|
||||
"SHA1_SHA1_PASS": "sha1(sha1($pass))",
|
||||
"SHA1_SHA1_PASS_SALT": "sha1(sha1($pass).$salt)",
|
||||
"SHA1_SALT_SHA1_PASS": "sha1($salt.sha1($pass))",
|
||||
"REDMINE": "Redmine",
|
||||
"PUNBB": "PunBB",
|
||||
"SHA1_MD5_PASS": "sha1(md5($pass))",
|
||||
"SHA1_MD5_PASS_SALT_MODE_4710": "sha1(md5($pass).$salt)",
|
||||
"HUAWEI_SHA1_MD5_PASS_SALT": "Huawei sha1(md5($pass).$salt)",
|
||||
"ISCSI_CHAP": "iSCSI CHAP authentication, MD5(CHAP)",
|
||||
"SHA1_SALT_PASS_SALT": "sha1($salt.$pass.$salt)",
|
||||
"SHA1_SHA1_SALT_PASS_SALT": "sha1(sha1($salt.$pass.$salt))",
|
||||
"HALF_MD5": "Half MD5",
|
||||
"PASSWORD_SAFE_V3": "Password Safe v3",
|
||||
"IKE_MD5": "IKE-PSK MD5",
|
||||
"IKE_SHA1": "IKE-PSK SHA1",
|
||||
"NET_NTLM_V1": "NetNTLMv1 / NetNTLMv1+ESS",
|
||||
"NET_NTLM_V2": "NetNTLMv2",
|
||||
"CISCO_IOS_TYPE_4_SHA256": "Cisco-IOS type 4 (SHA256)",
|
||||
"CISCO_ISE_HASHED_PASSWORD_SHA256": "Cisco-ISE Hashed Password (SHA256)",
|
||||
"SAMSUNG_ANDROID": "Samsung Android Password/PIN",
|
||||
"RIPEMD_160": "RIPEMD-160",
|
||||
"HMAC_RIPEMD160_KEY_PASS": "HMAC-RIPEMD160 (key = $pass)",
|
||||
"HMAC_RIPEMD160_KEY_SALT": "HMAC-RIPEMD160 (key = $salt)",
|
||||
"WHIRLPOOL": "Whirlpool",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT_LEGACY": "TrueCrypt RIPEMD160 + XTS 512 bit (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT_LEGACY": "TrueCrypt RIPEMD160 + XTS 1024 bit (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT_LEGACY": "TrueCrypt RIPEMD160 + XTS 1536 bit (legacy)",
|
||||
"TRUECRYPT_SHA512_XTS_512_BIT_LEGACY": "TrueCrypt SHA512 + XTS 512 bit (legacy)",
|
||||
"TRUECRYPT_SHA512_XTS_1024_BIT_LEGACY": "TrueCrypt SHA512 + XTS 1024 bit (legacy)",
|
||||
"TRUECRYPT_SHA512_XTS_1536_BIT_LEGACY": "TrueCrypt SHA512 + XTS 1536 bit (legacy)",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_512_BIT_LEGACY": "TrueCrypt Whirlpool + XTS 512 bit (legacy)",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1024_BIT_LEGACY": "TrueCrypt Whirlpool + XTS 1024 bit (legacy)",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1536_BIT_LEGACY": "TrueCrypt Whirlpool + XTS 1536 bit (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE_LEGACY": "TrueCrypt RIPEMD160 + XTS 512 bit + boot-mode (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE_LEGACY": "TrueCrypt RIPEMD160 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE_LEGACY": "TrueCrypt RIPEMD160 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"AIX_SMD5": "AIX {smd5}",
|
||||
"AIX_SSHA256": "AIX {ssha256}",
|
||||
"AIX_SSHA512": "AIX {ssha512}",
|
||||
"HASHCAT_1PASSWORD_AGILEKEYCHAIN": "1Password, agilekeychain",
|
||||
"AIX_SSHA1": "AIX {ssha1}",
|
||||
"LASTPASS_LASTPASS_SNIFFED": "LastPass + LastPass sniffed",
|
||||
"GOST_R_34_11_94": "GOST R 34.11-94",
|
||||
"FORTIGATE_FORTIOS": "FortiGate (FortiOS)",
|
||||
"MACOS_10_8": "macOS v10.8+ (PBKDF2-SHA512)",
|
||||
"GRUB_2": "GRUB 2",
|
||||
"IPMI2_PAKP_HMAC_SHA1": "IPMI2 RAKP HMAC-SHA1",
|
||||
"IPMI2_RAKP_HMAC_MD5": "IPMI2 RAKP HMAC-MD5",
|
||||
"SHA256CRYPT_5_SHA256_UNIX": "sha256crypt $5$, SHA256 (Unix)",
|
||||
"MYSQL_A_SHA256CRYPT": "MySQL $A$ (sha256crypt)",
|
||||
"KERBEROS_23_SA_REQ_PREAUTH": "Kerberos 5, etype 23, AS-REQ Pre-Auth",
|
||||
"SAP_CODVN_B_BCODE": "SAP CODVN B (BCODE)",
|
||||
"SAP_CODVN_B_BCODE_FROM_RFC_READ_TABLE": "SAP CODVN B (BCODE) from RFC_READ_TABLE",
|
||||
"SAP_CODVN_F_G_PASSCODE": "SAP CODVN F/G (PASSCODE)",
|
||||
"SAP_CODVN_F_G_PASSCODE_FROM_RFC_READ_TABLE": "SAP CODVN F/G (PASSCODE) from RFC_READ_TABLE",
|
||||
"DRUPAL7": "Drupal7",
|
||||
"SYBASE_ASE": "Sybase ASE",
|
||||
"CITRIX_NETSCALER_SHA1": "Citrix NetScaler (SHA1)",
|
||||
"HASHCAT_1PASSWORD_CLOUDKEYCHAIN": "1Password, cloudkeychain",
|
||||
"DNSSEC_NSEC3": "DNSSEC (NSEC3)",
|
||||
"WBB3_WOLTLAB_BURNING_BOARD": "WBB3 (Woltlab Burning Board)",
|
||||
"RACF": "RACF",
|
||||
"AS_400_DES": "AS/400 DES",
|
||||
"LOTUS_NOTES_DOMINO_5": "Lotus Notes/Domino 5",
|
||||
"LOTUS_NOTES_DOMINO_6": "Lotus Notes/Domino 6",
|
||||
"ANDROID_FDE_4_3": "Android FDE <= 4.3",
|
||||
"SCRYPT": "scrypt",
|
||||
"PASSWORD_SAFE_V2": "Password Safe v2",
|
||||
"LOTUS_NOTES_DOMINO_8": "Lotus Notes/Domino 8",
|
||||
"CISCO_IOS_PBKDF2_SHA256": "Cisco-IOS $8$ (PBKDF2-SHA256)",
|
||||
"CISCO_IOS_SCRYPT": "Cisco-IOS $9$ (scrypt)",
|
||||
"MS_OFFICE_2007": "MS Office 2007",
|
||||
"MS_OFFICE_2010": "MS Office 2010",
|
||||
"MS_OFFICE_2013": "MS Office 2013",
|
||||
"MS_OFFICE_2003_0_1_MD5_RC4": "MS Office <= 2003 $0/$1, MD5 + RC4",
|
||||
"MS_OFFICE_2003_0_1_MD5_RC4_COLLIDER_1": "MS Office <= 2003 $0/$1, MD5 + RC4, collider #1",
|
||||
"MS_OFFICE_2003_0_1_MD5_RC4_COLLIDER_2": "MS Office <= 2003 $0/$1, MD5 + RC4, collider #2",
|
||||
"MS_OFFICE_2003_3_4_SHA1_RC4": "MS Office <= 2003 $3/$4, SHA1 + RC4",
|
||||
"MS_OFFICE_2003_3_SHA1_RC4_COLLIDER_1": "MS Office <= 2003 $3, SHA1 + RC4, collider #1",
|
||||
"MS_OFFICE_2003_3_SHA1_RC4_COLLIDER_2": "MS Office <= 2003 $3, SHA1 + RC4, collider #2",
|
||||
"RADMIN2": "Radmin2",
|
||||
"DJANGO_PBKDF2_SHA256": "Django (PBKDF2-SHA256)",
|
||||
"SIPHASH": "SipHash",
|
||||
"CRAM_MD5": "CRAM-MD5",
|
||||
"SAP_CODVN_H_PWDSALTEDHASH_ISSHA_1": "SAP CODVN H (PWDSALTEDHASH) iSSHA-1",
|
||||
"PDF_1_1_1_3_ACROBAT_2_4": "PDF 1.1 - 1.3 (Acrobat 2 - 4)",
|
||||
"PDF_1_1_1_3_ACROBAT_2_4_COLLIDER_1": "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #1",
|
||||
"PDF_1_1_1_3_ACROBAT_2_4_COLLIDER_2": "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #2",
|
||||
"PDF_1_4_1_6_ACROBAT_5_8": "PDF 1.4 - 1.6 (Acrobat 5 - 8)",
|
||||
"PDF_1_3_1_6_ACROBAT_4_8_W_RC4_40": "PDF 1.3 - 1.6 (Acrobat 4 - 8) w/ RC4-40",
|
||||
"PDF_1_7_LEVEL_3_ACROBAT_9": "PDF 1.7 Level 3 (Acrobat 9)",
|
||||
"PDF_1_7_LEVEL_8_ACROBAT_10_11": "PDF 1.7 Level 8 (Acrobat 10 - 11)",
|
||||
"SHA2_384": "SHA2-384",
|
||||
"SHA384_PASS_SALT": "sha384($pass.$salt)",
|
||||
"SHA384_SALT_PASS": "sha384($salt.$pass)",
|
||||
"SHA384_UTF16LE_PASS_SALT": "sha384(utf16le($pass).$salt)",
|
||||
"SHA384_SALT_UTF16LE_PASS": "sha384($salt.utf16le($pass))",
|
||||
"SHA384_UTF16LE": "sha384(utf16le($pass))",
|
||||
"PBKDF2_HMAC_SHA256": "PBKDF2-HMAC-SHA256",
|
||||
"REDHAT_389_DS_LDAP_PBKDF2_HMAC_SHA256": "RedHat 389-DS LDAP (PBKDF2-HMAC-SHA256)",
|
||||
"PRESTASHOP": "PrestaShop",
|
||||
"POSTGRESQL_CRAM_MD5": "PostgreSQL CRAM (MD5)",
|
||||
"MYSQL_CRAM_SHA1": "MySQL CRAM (SHA1)",
|
||||
"BITCOIN_LITECOIN_WALLET_DAT": "Bitcoin/Litecoin wallet.dat",
|
||||
"SIP_DIGEST": "SIP digest authentication (MD5)",
|
||||
"CRC32": "CRC32",
|
||||
"HASHCAT_7_ZIP": "7-Zip",
|
||||
"GOST_R_32_11_2012_256": "GOST R 34.11-2012 (Streebog) 256-bit, big-endian",
|
||||
"HMAC_STREEBOG_256_KEY_PASS_BIG_ENDIAN": "HMAC-Streebog-256 (key = $pass), big-endian",
|
||||
"HMAC_STREEBOG_256_KEY_SALT_BIG_ENDIAN": "HMAC-Streebog-256 (key = $salt), big-endian",
|
||||
"GOST_R_32_11_2012_512": "GOST R 34.11-2012 (Streebog) 512-bit, big-endian",
|
||||
"HMAC_STREEBOG_512_KEY_PASS_BIG_ENDIAN": "HMAC-Streebog-512 (key = $pass), big-endian",
|
||||
"HMAC_STREEBOG_512_KEY_SALT_BIG_ENDIAN": "HMAC-Streebog-512 (key = $salt), big-endian",
|
||||
"PBKDF2_HMAC_MD5": "PBKDF2-HMAC-MD5",
|
||||
"PBKDF2_HMAC_SHA1": "PBKDF2-HMAC-SHA1",
|
||||
"ATLASSIAN_PBKDF2_HMAC_SHA1": "Atlassian (PBKDF2-HMAC-SHA1)",
|
||||
"PBKDF2_HMAC_SHA512": "PBKDF2-HMAC-SHA512",
|
||||
"APACHE_SHIRO_1_SHA_512": "Apache Shiro 1 SHA-512",
|
||||
"ECRYPTFS": "eCryptfs",
|
||||
"ORACLE_T_TYPE_ORACLE_12": "Oracle T: Type (Oracle 12+)",
|
||||
"BSDI_CRYPT": "BSDi Crypt, Extended DES",
|
||||
"RAR3_HP": "RAR3-hp",
|
||||
"COLDFUSION_10": "ColdFusion 10+",
|
||||
"BLOCKCHAIN_MY_WALLET": "Blockchain, My Wallet",
|
||||
"MS_AZURE_SYNC": "MS-AzureSync PBKDF2-HMAC-SHA256",
|
||||
"ANDROID_FDE_SAMSUNG_DEK": "Android FDE (Samsung DEK)",
|
||||
"RAR5": "RAR5",
|
||||
"KERBEROS_23_TGS_REP": "Kerberos 5, etype 23, TGS-REP",
|
||||
"AXCRYPT_1": "AxCrypt 1",
|
||||
"AXCRYPT_1_IN_MEMORY_SHA1": "AxCrypt 1 in-memory SHA1",
|
||||
"KEEPASS_KDBX_V2_V3": "KeePass (KDBX v2/v3)",
|
||||
"PEOPLESOFT_PS_TOKEN": "PeopleSoft PS_TOKEN",
|
||||
"WINZIP": "WinZip",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT_LEGACY": "VeraCrypt RIPEMD160 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT_LEGACY": "VeraCrypt RIPEMD160 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT_LEGACY": "VeraCrypt RIPEMD160 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_SHA512_XTS_512_BIT_LEGACY": "VeraCrypt SHA512 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_SHA512_XTS_1024_BIT_LEGACY": "VeraCrypt SHA512 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_SHA512_XTS_1536_BIT_LEGACY": "VeraCrypt SHA512 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_512_BIT_LEGACY": "VeraCrypt Whirlpool + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1024_BIT_LEGACY": "VeraCrypt Whirlpool + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1536_BIT_LEGACY": "VeraCrypt Whirlpool + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE_LEGACY": "VeraCrypt RIPEMD160 + XTS 512 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE_LEGACY": "VeraCrypt RIPEMD160 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE_LEGACY": "VeraCrypt RIPEMD160 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT_LEGACY": "VeraCrypt SHA256 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT_LEGACY": "VeraCrypt SHA256 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT_LEGACY": "VeraCrypt SHA256 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT_BOOT_MODE_LEGACY": "VeraCrypt SHA256 + XTS 512 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT_BOOT_MODE_LEGACY": "VeraCrypt SHA256 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT_BOOT_MODE_LEGACY": "VeraCrypt SHA256 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT_LEGACY": "VeraCrypt Streebog-512 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT_LEGACY": "VeraCrypt Streebog-512 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT_LEGACY": "VeraCrypt Streebog-512 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT_BOOT_MODE_LEGACY": "VeraCrypt Streebog-512 + XTS 512 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT_BOOT_MODE_LEGACY": "VeraCrypt Streebog-512 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT_BOOT_MODE_LEGACY": "VeraCrypt Streebog-512 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"WINDOWS_PHONE": "Windows Phone 8+ PIN/password",
|
||||
"OPENCART": "OpenCart",
|
||||
"DES": "DES (PT = $salt, key = $pass)",
|
||||
"THREE_DES": "3DES (PT = $salt, key = $pass)",
|
||||
"RACF_KDFAES": "RACF KDFAES",
|
||||
"SHA1_CX": "sha1(CX)",
|
||||
"LINUX_KERNEL_CRYPTO_API_24": "Linux Kernel Crypto API (2.4)",
|
||||
"LUKS_V1_LEGACY": "LUKS v1 (legacy)",
|
||||
"ITUNES_BACKUP_10_0_MODE_14700": "iTunes backup < 10.0",
|
||||
"ITUNES_BACKUP_10_0_MODE_14800": "iTunes backup >= 10.0",
|
||||
"SKIP_32": "Skip32 (PT = $salt, key = $pass)",
|
||||
"FILEZILLA_SERVER_0_9_55": "FileZilla Server >= 0.9.55",
|
||||
"JUNIPER_NETBSD_SHA1CRYPT": "Juniper/NetBSD sha1crypt",
|
||||
"BLOCKCHAIN_MY_WALLET_V2": "Blockchain, My Wallet, V2",
|
||||
"DPAPI_V1_CTX_1_AND_2": "DPAPI masterkey file v1 (context 1 and 2)",
|
||||
"DPAPI_V1_CTX_3": "DPAPI masterkey file v1 (context 3)",
|
||||
"CHA_CHA_20": "ChaCha20",
|
||||
"JKS_JAVA_KEY_STORE_PRIVATE_KEYS_SHA1": "JKS Java Key Store Private Keys (SHA1)",
|
||||
"ETHEREUM_WALLET_PBKDF2_HMAC_SHA256": "Ethereum Wallet, PBKDF2-HMAC-SHA256",
|
||||
"ETHEREUM_WALLET_SCRYPT": "Ethereum Wallet, SCRYPT",
|
||||
"DPAPI_V2_CTX_1_AND_2": "DPAPI masterkey file v2 (context 1 and 2)",
|
||||
"DPAPI_V2_CTX_3": "DPAPI masterkey file v2 (context 3)",
|
||||
"TRIPCODE": "Tripcode",
|
||||
"TACACS_PLUS": "TACACS+",
|
||||
"APPLE_SECURE_NOTES": "Apple Secure Notes",
|
||||
"ETHEREUM_PRE_SALE_WALLET_PBKDF2_HMAC_SHA256": "Ethereum Pre-Sale Wallet, PBKDF2-HMAC-SHA256",
|
||||
"CRAM_MD5_DOVECOT": "CRAM-MD5 Dovecot",
|
||||
"JWT": "JWT (JSON Web Token)",
|
||||
"PERL_MOJOLICIOUS_SESSION_COOKIE_HMAC_SHA256_V9_19": "Perl Mojolicious session cookie (HMAC-SHA256, >= v9.19)",
|
||||
"ELECTRUM_WALLET_SALT_TYPE_1_3": "Electrum Wallet (Salt-Type 1-3)",
|
||||
"FILEVAULT_2": "FileVault 2",
|
||||
"WPA_PMKID_PBKDF2": "WPA-PMKID-PBKDF2",
|
||||
"WPA_PMKID_PMK": "WPA-PMKID-PMK",
|
||||
"ANSIBLE_VAULT": "Ansible Vault",
|
||||
"GPG": "GPG (AES-128/AES-256 (SHA-1($pass)))",
|
||||
"GPG_AES_128_AES_256_SHA_512_PASS": "GPG (AES-128/AES-256 (SHA-512($pass)))",
|
||||
"GPG_AES_128_AES_256_SHA_256_PASS": "GPG (AES-128/AES-256 (SHA-256($pass)))",
|
||||
"GPG_CAST5_SHA_1_PASS": "GPG (CAST5 (SHA-1($pass)))",
|
||||
"GPG_AES_OCB_128_SHA_1_PASS": "GPG (AES-OCB-128 (SHA-1($pass)))",
|
||||
"PKZIP_COMPRESSED": "PKZIP (Compressed)",
|
||||
"PKZIP_UNCOMPRESSED": "PKZIP (Uncompressed)",
|
||||
"PKZIP_COMPRESSED_MULTI_FILE": "PKZIP (Compressed Multi-File)",
|
||||
"PKZIP_MIXED_MULTI_FILE": "PKZIP (Mixed Multi-File)",
|
||||
"PKZIP_MIXED_MULTI_FILE_CHECKSUM_ONLY": "PKZIP (Mixed Multi-File Checksum-Only)",
|
||||
"SHA3_224": "SHA3-224",
|
||||
"SHA3_256": "SHA3-256",
|
||||
"SHA3_384": "SHA3-384",
|
||||
"SHA3_512": "SHA3-512",
|
||||
"KECCAK_224": "Keccak-224",
|
||||
"KECCAK_256": "Keccak-256",
|
||||
"KECCAK_384": "Keccak-384",
|
||||
"KECCAK_512": "Keccak-512",
|
||||
"TOTP_HMAC_SHA1": "TOTP (HMAC-SHA1)",
|
||||
"KERBEROS_23_AS_REP": "Kerberos 5, etype 23, AS-REP",
|
||||
"APPLE_FILE_SYSTEM_APFS": "Apple File System (APFS)",
|
||||
"OPEN_DOCUMENT_FORMAT_ODF_1_2_SHA_256_AES": "Open Document Format (ODF) 1.2 (SHA-256, AES)",
|
||||
"SHA1_MD5_MD5_PASS": "sha1(md5(md5($pass)))",
|
||||
"OPEN_DOCUMENT_FORMAT_ODF_1_1_SHA_1_BLOWFISH": "Open Document Format (ODF) 1.1 (SHA-1, Blowfish)",
|
||||
"JAVA_OBJECT": "Java Object hashCode()",
|
||||
"BLOCKCHAIN_MY_WALLET_SECOND_PASSWORD_SHA256": "Blockchain, My Wallet, Second Password (SHA256)",
|
||||
"ANDROID_BACKUP": "Android Backup",
|
||||
"QNX_MD5": "QNX /etc/shadow (MD5)",
|
||||
"QNX_SHA256": "QNX /etc/shadow (SHA256)",
|
||||
"QNX_SHA512": "QNX /etc/shadow (SHA512)",
|
||||
"QNX_7_ETC_SHADOW_SHA512": "QNX 7 /etc/shadow (SHA512)",
|
||||
"SHA1_SALT1_PASS_SALT2": "sha1($salt1.$pass.$salt2)",
|
||||
"RUBY_ON_RAILS_RESTFUL_AUTHENTICATION": "Ruby on Rails Restful-Authentication",
|
||||
"KERBEROS_17_TGS_REP": "Kerberos 5, etype 17, TGS-REP",
|
||||
"KERBEROS_18_TGS_REP": "Kerberos 5, etype 18, TGS-REP",
|
||||
"KERBEROS_17_PREAUTH": "Kerberos 5, etype 17, Pre-Auth",
|
||||
"KERBEROS_18_PREAUTH": "Kerberos 5, etype 18, Pre-Auth",
|
||||
"DISKCRYPTOR_SHA512_XTS_512_BIT": "DiskCryptor SHA512 + XTS 512 bit",
|
||||
"DISKCRYPTOR_SHA512_XTS_1024_BIT": "DiskCryptor SHA512 + XTS 1024 bit",
|
||||
"DISKCRYPTOR_SHA512_XTS_1536_BIT": "DiskCryptor SHA512 + XTS 1536 bit",
|
||||
"PYTHON_PASSLIB_PBKDF2_SHA512": "Python passlib pbkdf2-sha512",
|
||||
"PYTHON_PASSLIB_PBKDF2_SHA256": "Python passlib pbkdf2-sha256",
|
||||
"PYTHON_PASSLIB_PBKDF2_SHA1": "Python passlib pbkdf2-sha1",
|
||||
"PKZIP_MASTER_KEY": "PKZIP Master Key",
|
||||
"PKZIP_MASTER_KEY_6_BYTE_OPTIMIZATION": "PKZIP Master Key (6 byte optimization)",
|
||||
"ORACLE_TRANSPORTATION_MANAGEMENT_SHA256": "Oracle Transportation Management (SHA256)",
|
||||
"SHA256_SHA256_PASS_SALT_MODE_20710": "sha256(sha256($pass).$salt)",
|
||||
"AUTHME_SHA256": "AuthMe sha256",
|
||||
"RSA_SECURITY_ANALYTICS_NETWITNESS_SHA256": "RSA Security Analytics / NetWitness (sha256)",
|
||||
"SHA256_SALT_SHA256_PASS": "sha256($salt.sha256($pass))",
|
||||
"SHA256_SHA256_PASS_SALT_MODE_20730": "sha256(sha256($pass.$salt))",
|
||||
"SHA256_MD5_PASS": "sha256(md5($pass))",
|
||||
"MD5_SHA1_PASS_MD5_PASS_SHA1_PASS": "md5(sha1($pass).md5($pass).sha1($pass))",
|
||||
"BITSHARES_V0_X_SHA512_SHA512_BIN_PASS": "BitShares v0.x - sha512(sha512_bin(pass))",
|
||||
"SHA1_MD5_PASS_SALT_MODE_21100": "sha1(md5($pass.$salt))",
|
||||
"MD5_SHA1_SALT_MD5_PASS": "md5(sha1($salt).md5($pass))",
|
||||
"MD5_SALT_SHA1_SALT_PASS": "md5($salt.sha1($salt.$pass))",
|
||||
"MD5_SALT1_SHA1_SALT2_PASS": "md5($salt1.sha1($salt2.$pass))",
|
||||
"SHA256_SHA256_BIN_PASS": "sha256(sha256_bin($pass))",
|
||||
"SHA256_SALT_SHA256_BIN_PASS": "sha256($salt.sha256_bin($pass))",
|
||||
"SOLARWINDS_ORION": "SolarWinds Orion",
|
||||
"SOLARWINDS_ORION_V2": "SolarWinds Orion v2",
|
||||
"WEB2PY_PBKDF2_SHA512": "Web2py pbkdf2-sha512",
|
||||
"ELECTRUM_WALLET_SALT_TYPE_4": "Electrum Wallet (Salt-Type 4)",
|
||||
"ELECTRUM_WALLET_SALT_TYPE_5": "Electrum Wallet (Salt-Type 5)",
|
||||
"MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_21900": "md5(md5(md5($pass.$salt1)).$salt2)",
|
||||
"WPA_PBKDF2_PMKID_EAPOL": "WPA-PBKDF2-PMKID+EAPOL",
|
||||
"WPA_PMK_PMKID_EAPOL": "WPA-PMK-PMKID+EAPOL",
|
||||
"BITLOCKER": "BitLocker",
|
||||
"CITRIX_NETSCALER_SHA512": "Citrix NetScaler (SHA512)",
|
||||
"SHA256_SALT_PASS_SALT": "sha256($salt.$pass.$salt)",
|
||||
"TELEGRAM_MOBILE_APP_PASSCODE_SHA256": "Telegram Mobile App Passcode (SHA256)",
|
||||
"AES_CRYPT_SHA256": "AES Crypt (SHA256)",
|
||||
"MULTIBIT_CLASSIC_KEY_MD5": "MultiBit Classic .key (MD5)",
|
||||
"TELEGRAM_DESKTOP_V2_1_14_PBKDF2_HMAC_SHA1": "Telegram Desktop < v2.1.14 (PBKDF2-HMAC-SHA1)",
|
||||
"MULTIBIT_HD_SCRYPT": "MultiBit HD (scrypt)",
|
||||
"SIMPLA_CMS_MD5_SALT_PASS_MD5_PASS": "Simpla CMS - md5($salt.$pass.md5($pass))",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_0": "RSA/DSA/EC/OpenSSH Private Keys ($0$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_6": "RSA/DSA/EC/OpenSSH Private Keys ($6$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_1_3": "RSA/DSA/EC/OpenSSH Private Keys ($1, $3$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_4": "RSA/DSA/EC/OpenSSH Private Keys ($4$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_5": "RSA/DSA/EC/OpenSSH Private Keys ($5$)",
|
||||
"SECUREZIP_AES_128": "SecureZIP AES-128",
|
||||
"SECUREZIP_AES_192": "SecureZIP AES-192",
|
||||
"SECUREZIP_AES_256": "SecureZIP AES-256",
|
||||
"APPLE_KEYCHAIN": "Apple Keychain",
|
||||
"XMPP_SCRAM_PBKDF2_SHA1": "XMPP SCRAM PBKDF2-SHA1",
|
||||
"APPLE_IWORK": "Apple iWork",
|
||||
"BITWARDEN": "Bitwarden",
|
||||
"AXCRYPT_2_AES_128": "AxCrypt 2 AES-128",
|
||||
"AXCRYPT_2_AES_256": "AxCrypt 2 AES-256",
|
||||
"RAR3_P_UNCOMPRESSED": "RAR3-p (Uncompressed)",
|
||||
"RAR3_P_COMPRESSED": "RAR3-p (Compressed)",
|
||||
"BESTCRYPT_V3_VOLUME_ENCRYPTION": "BestCrypt v3 Volume Encryption",
|
||||
"BESTCRYPT_V4_VOLUME_ENCRYPTION": "BestCrypt v4 Volume Encryption",
|
||||
"MONGODB_SERVERKEY_SCRAM_SHA_1": "MongoDB ServerKey SCRAM-SHA-1",
|
||||
"MONGODB_SERVERKEY_SCRAM_SHA_256": "MongoDB ServerKey SCRAM-SHA-256",
|
||||
"SHA1_SALT_SHA1_PASS_SALT": "sha1($salt.sha1($pass.$salt))",
|
||||
"PKCS_8_PRIVATE_KEYS_PBKDF2_HMAC_SHA1_3DES_AES": "PKCS#8 Private Keys (PBKDF2-HMAC-SHA1 + 3DES/AES)",
|
||||
"PKCS_8_PRIVATE_KEYS_PBKDF2_HMAC_SHA256_3DES_AES": "PKCS#8 Private Keys (PBKDF2-HMAC-SHA256 + 3DES/AES)",
|
||||
"TELEGRAM_DESKTOP_V2_1_14_PBKDF2_HMAC_SHA512": "Telegram Desktop >= v2.1.14 (PBKDF2-HMAC-SHA512)",
|
||||
"SQLCIPHER": "SQLCipher",
|
||||
"STUFFIT5": "Stuffit5",
|
||||
"UMBRACO_HMAC_SHA1": "Umbraco HMAC-SHA1",
|
||||
"DAHUA_AUTHENTICATION_MD5": "Dahua Authentication MD5",
|
||||
"BESDER_AUTHENTICATION_MD5": "Besder Authentication MD5",
|
||||
"SNMP_V3_HMAC_MD5_96__SHA1_96": "SNMPv3 HMAC-MD5-96/HMAC-SHA1-96",
|
||||
"SNMP_V3_HMAC_MD5_96": "SNMPv3 HMAC-MD5-96",
|
||||
"SNMP_V3_HMAC_SHA1_96": "SNMPv3 HMAC-SHA1-96",
|
||||
"MS_OFFICE_2016_SHEETPROTECTION": "MS Office 2016 - SheetProtection",
|
||||
"PDF_1_4_1_6_ACROBAT_5_8_USER_AND_OWNER_PASS": "PDF 1.4 - 1.6 (Acrobat 5 - 8) - user and owner pass",
|
||||
"STARGAZER_STELLAR_WALLET_XLM": "Stargazer Stellar Wallet XLM",
|
||||
"BCRYPT_MD5_PASS": "bcrypt(md5($pass))",
|
||||
"MURMUR": "MurmurHash",
|
||||
"BCRYPT_SHA1_PASS": "bcrypt(sha1($pass))",
|
||||
"KNX_IP_SECURE_DEVICE_AUTHENTICATION_CODE": "KNX IP Secure - Device Authentication Code",
|
||||
"MOZILLA_KEY3_DB": "Mozilla key3.db",
|
||||
"MOZILLA_KEY4_DB": "Mozilla key4.db",
|
||||
"OPENEDGE_PROGRESS_ENCODE": "OpenEdge Progress Encode",
|
||||
"FORTIGATE256_FORTIOS256": "FortiGate256 (FortiOS256)",
|
||||
"AES_128_ECB": "AES-128-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_192_ECB": "AES-192-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_256_ECB": "AES-256-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"IPHONE_PASSCODE_UID_KEY_SYSTEM_KEYBAG": "iPhone passcode (UID key + System Keybag)",
|
||||
"METAMASK_WALLET_NEEDS_ALL_DATA_CHECKS_AES_GCM_TAG": "MetaMask Wallet (needs all data, checks AES-GCM tag)",
|
||||
"METAMASK_WALLET_SHORT_HASH_PLAINTEXT_CHECK": "MetaMask Wallet (short hash, plaintext check)",
|
||||
"SNMP_V3_HMAC_SHA224_128": "SNMPv3 HMAC-SHA224-128",
|
||||
"SNMP_V3_HMAC_SHA256_192": "SNMPv3 HMAC-SHA256-192",
|
||||
"SNMP_V3_HMAC_SHA384_256": "SNMPv3 HMAC-SHA384-256",
|
||||
"NET_NTLM_V1_NT": "NetNTLMv1 / NetNTLMv1+ESS (NT)",
|
||||
"NET_NTLM_V2_NT": "NetNTLMv2 (NT)",
|
||||
"RUBY_ON_RAILS_RESTFUL_AUTH_ONE_ROUND_NO_SITEKEY": "Ruby on Rails Restful Auth (one round, no sitekey)",
|
||||
"SNMP_V3_HMAC_SHA512_384": "SNMPv3 HMAC-SHA512-384",
|
||||
"VMWARE_VMX_PBKDF2_HMAC_SHA1_AES_256_CBC": "VMware VMX (PBKDF2-HMAC-SHA1 + AES-256-CBC)",
|
||||
"VIRTUALBOX_PBKDF2_HMAC_SHA256_AES_128_XTS": "VirtualBox (PBKDF2-HMAC-SHA256 & AES-128-XTS)",
|
||||
"VIRTUALBOX_PBKDF2_HMAC_SHA256_AES_256_XTS": "VirtualBox (PBKDF2-HMAC-SHA256 & AES-256-XTS)",
|
||||
"MULTIBIT_CLASSIC_WALLET_SCRYPT": "MultiBit Classic .wallet (scrypt)",
|
||||
"MURMUR3": "MurmurHash3",
|
||||
"CRC32C": "CRC32C",
|
||||
"CRC64Jones": "CRC64Jones",
|
||||
"WINDOWS_HELLO_PIN": "Windows Hello PIN/Password",
|
||||
"EXODUS_DESKTOP_WALLET_SCRYPT": "Exodus Desktop Wallet (scrypt)",
|
||||
"TEAMSPEAK_3_CHANNEL_HASH": "Teamspeak 3 (channel hash)",
|
||||
"BCRYPT_SHA512_PASS": "bcrypt(sha512($pass))",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2PKH_COMPRESSED": "Bitcoin WIF private key (P2PKH), compressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2PKH_UNCOMPRESSED": "Bitcoin WIF private key (P2PKH), uncompressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2WPKH_BECH32_COMPRESSED": "Bitcoin WIF private key (P2WPKH, Bech32), compressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2WPKH_BECH32_UNCOMPRESSED": "Bitcoin WIF private key (P2WPKH, Bech32), uncompressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2SH_P2WPKH_COMPRESSED": "Bitcoin WIF private key (P2SH(P2WPKH)), compressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2SH_P2WPKH_UNCOMPRESSED": "Bitcoin WIF private key (P2SH(P2WPKH)), uncompressed",
|
||||
"POSTGRESQL_SCRAM_SHA_256": "PostgreSQL SCRAM-SHA-256",
|
||||
"AMAZON_AWS_SIGNATURE_VERSION_4": "Amazon AWS Signature Version 4",
|
||||
"KERBEROS_17_DB": "Kerberos 5, etype 17, DB",
|
||||
"KERBEROS_18_DB": "Kerberos 5, etype 18, DB",
|
||||
"SHA1_SALT_SHA1_UTF16LE_USERNAME_UTF16LE_PASS": "sha1($salt.sha1(utf16le($username).':'.utf16le($pass)))",
|
||||
"FLASK": "Flask Session Cookie ($salt.$salt.$pass)",
|
||||
"RADMIN_3": "Radmin3",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT": "TrueCrypt RIPEMD160 + XTS 512 bit",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT": "TrueCrypt RIPEMD160 + XTS 1024 bit",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT": "TrueCrypt RIPEMD160 + XTS 1536 bit",
|
||||
"TRUECRYPT_SHA512_XTS_512_BIT": "TrueCrypt SHA512 + XTS 512 bit",
|
||||
"TRUECRYPT_SHA512_XTS_1024_BIT": "TrueCrypt SHA512 + XTS 1024 bit",
|
||||
"TRUECRYPT_SHA512_XTS_1536_BIT": "TrueCrypt SHA512 + XTS 1536 bit",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_512_BIT": "TrueCrypt Whirlpool + XTS 512 bit",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1024_BIT": "TrueCrypt Whirlpool + XTS 1024 bit",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1536_BIT": "TrueCrypt Whirlpool + XTS 1536 bit",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE": "TrueCrypt RIPEMD160 + XTS 512 bit + boot-mode",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE": "TrueCrypt RIPEMD160 + XTS 1024 bit + boot-mode",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE": "TrueCrypt RIPEMD160 + XTS 1536 bit + boot-mode",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT": "VeraCrypt RIPEMD160 + XTS 512 bit",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT": "VeraCrypt RIPEMD160 + XTS 1024 bit",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT": "VeraCrypt RIPEMD160 + XTS 1536 bit",
|
||||
"VERACRYPT_SHA512_XTS_512_BIT": "VeraCrypt SHA512 + XTS 512 bit",
|
||||
"VERACRYPT_SHA512_XTS_1024_BIT": "VeraCrypt SHA512 + XTS 1024 bit",
|
||||
"VERACRYPT_SHA512_XTS_1536_BIT": "VeraCrypt SHA512 + XTS 1536 bit",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_512_BIT": "VeraCrypt Whirlpool + XTS 512 bit",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1024_BIT": "VeraCrypt Whirlpool + XTS 1024 bit",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1536_BIT": "VeraCrypt Whirlpool + XTS 1536 bit",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE": "VeraCrypt RIPEMD160 + XTS 512 bit + boot-mode",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE": "VeraCrypt RIPEMD160 + XTS 1024 bit + boot-mode",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE": "VeraCrypt RIPEMD160 + XTS 1536 bit + boot-mode",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT": "VeraCrypt SHA256 + XTS 512 bit",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT": "VeraCrypt SHA256 + XTS 1024 bit",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT": "VeraCrypt SHA256 + XTS 1536 bit",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT_BOOT_MODE": "VeraCrypt SHA256 + XTS 512 bit + boot-mode",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT_BOOT_MODE": "VeraCrypt SHA256 + XTS 1024 bit + boot-mode",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT_BOOT_MODE": "VeraCrypt SHA256 + XTS 1536 bit + boot-mode",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT": "VeraCrypt Streebog-512 + XTS 512 bit",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT": "VeraCrypt Streebog-512 + XTS 1024 bit",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT": "VeraCrypt Streebog-512 + XTS 1536 bit",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT_BOOT_MODE": "VeraCrypt Streebog-512 + XTS 512 bit + boot-mode",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT_BOOT_MODE": "VeraCrypt Streebog-512 + XTS 1024 bit + boot-mode",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT_BOOT_MODE": "VeraCrypt Streebog-512 + XTS 1536 bit + boot-mode",
|
||||
"LUKS_V1_SHA_1_AES": "LUKS v1 SHA-1 + AES",
|
||||
"LUKS_V1_SHA_1_SERPENT": "LUKS v1 SHA-1 + Serpent",
|
||||
"LUKS_V1_SHA_1_TWOFISH": "LUKS v1 SHA-1 + Twofish",
|
||||
"LUKS_V1_SHA_256_AES": "LUKS v1 SHA-256 + AES",
|
||||
"LUKS_V1_SHA_256_SERPENT": "LUKS v1 SHA-256 + Serpent",
|
||||
"LUKS_V1_SHA_256_TWOFISH": "LUKS v1 SHA-256 + Twofish",
|
||||
"LUKS_V1_SHA_512_AES": "LUKS v1 SHA-512 + AES",
|
||||
"LUKS_V1_SHA_512_SERPENT": "LUKS v1 SHA-512 + Serpent",
|
||||
"LUKS_V1_SHA_512_TWOFISH": "LUKS v1 SHA-512 + Twofish",
|
||||
"LUKS_V1_RIPEMD_160_AES": "LUKS v1 RIPEMD-160 + AES",
|
||||
"LUKS_V1_RIPEMD_160_SERPENT": "LUKS v1 RIPEMD-160 + Serpent",
|
||||
"LUKS_V1_RIPEMD_160_TWOFISH": "LUKS v1 RIPEMD-160 + Twofish",
|
||||
"TERRA_STATION_WALLET_AES256_CBC_PBKDF2_PASS": "Terra Station Wallet (AES256-CBC(PBKDF2($pass)))",
|
||||
"KEEPASS_KDBX_V2_V3_KEYFILE_ONLY": "KeePass (KDBX v2/v3) - keyfile only",
|
||||
"BISQ_WALLET_SCRYPT": "Bisq .wallet (scrypt)",
|
||||
"ENCSECURITY_DATAVAULT_PBKDF2_NO_KEYCHAIN": "ENCsecurity Datavault (PBKDF2/no keychain)",
|
||||
"ENCSECURITY_DATAVAULT_PBKDF2_KEYCHAIN": "ENCsecurity Datavault (PBKDF2/keychain)",
|
||||
"ENCSECURITY_DATAVAULT_MD5_NO_KEYCHAIN": "ENCsecurity Datavault (MD5/no keychain)",
|
||||
"ENCSECURITY_DATAVAULT_MD5_KEYCHAIN": "ENCsecurity Datavault (MD5/keychain)",
|
||||
"PYTHON_WERKZEUG_MD5_HMAC_MD5_KEY_SALT": "Python Werkzeug MD5 (HMAC-MD5 (key = $salt))",
|
||||
"PYTHON_WERKZEUG_SHA256_HMAC_SHA256_KEY_SALT": "Python Werkzeug SHA256 (HMAC-SHA256 (key = $salt))",
|
||||
"DANE_RFC7929_RFC8162_SHA2_256": "DANE RFC7929/RFC8162 SHA2-256",
|
||||
"MD5_MD5_SALT_MD5_MD5_PASS": "md5(md5($salt).md5(md5($pass)))",
|
||||
"BCRYPT_SHA256_PASS": "bcrypt(sha256($pass))",
|
||||
"BCRYPT_HMAC_SHA256_PASS": "bcrypt(HMAC-SHA256($pass))",
|
||||
"ANOPE_IRC_SERVICES_ENC_SHA256": "Anope IRC Services (enc_sha256)",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2PKH_COMPRESSED": "Bitcoin raw private key (P2PKH), compressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2PKH_UNCOMPRESSED": "Bitcoin raw private key (P2PKH), uncompressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2WPKH_BECH32_COMPRESSED": "Bitcoin raw private key (P2WPKH, Bech32), compressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2WPKH_BECH32_UNCOMPRESSED": "Bitcoin raw private key (P2WPKH, Bech32), uncompressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2SH_P2WPKH_COMPRESSED": "Bitcoin raw private key (P2SH(P2WPKH)), compressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2SH_P2WPKH_UNCOMPRESSED": "Bitcoin raw private key (P2SH(P2WPKH)), uncompressed",
|
||||
"BLAKE2S_256": "BLAKE2s-256",
|
||||
"SHANGMI_3_SM3": "ShangMi 3 (SM3)",
|
||||
"VEEAM_VBK": "Veeam VBK",
|
||||
"MS_SNTP": "MS SNTP",
|
||||
"SECURECRT_MASTERPASSPHRASE_V2": "SecureCRT MasterPassphrase v2",
|
||||
"DOMAIN_CACHED_CREDENTIALS_DCC_MS_CACHE_NT": "Domain Cached Credentials (DCC), MS Cache (NT)",
|
||||
"DOMAIN_CACHED_CREDENTIALS_2_DCC2_MS_CACHE_2_NT": "Domain Cached Credentials 2 (DCC2), MS Cache 2, (NT)",
|
||||
"MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_31700": "md5(md5(md5($pass).$salt1).$salt2)",
|
||||
"HASHCAT_1PASSWORD_MOBILEKEYCHAIN_1PASSWORD_8": "1Password, mobilekeychain (1Password 8)",
|
||||
"METAMASK_MOBILE_WALLET": "MetaMask Mobile Wallet",
|
||||
"NETIQ_SSPR_MD5": "NetIQ SSPR (MD5)",
|
||||
"NETIQ_SSPR_SHA1": "NetIQ SSPR (SHA1)",
|
||||
"NETIQ_SSPR_SHA_1_WITH_SALT": "NetIQ SSPR (SHA-1 with Salt)",
|
||||
"NETIQ_SSPR_SHA_256_WITH_SALT": "NetIQ SSPR (SHA-256 with Salt)",
|
||||
"ADOBE_AEM_SSPR_SHA_256_WITH_SALT": "Adobe AEM (SSPR, SHA-256 with Salt)",
|
||||
"NETIQ_SSPR_SHA_512_WITH_SALT": "NetIQ SSPR (SHA-512 with Salt)",
|
||||
"ADOBE_AEM_SSPR_SHA_512_WITH_SALT": "Adobe AEM (SSPR, SHA-512 with Salt)",
|
||||
"NETIQ_SSPR_PBKDF2WITHHMACSHA1": "NetIQ SSPR (PBKDF2WithHmacSHA1)",
|
||||
"NETIQ_SSPR_PBKDF2WITHHMACSHA256": "NetIQ SSPR (PBKDF2WithHmacSHA256)",
|
||||
"NETIQ_SSPR_PBKDF2WITHHMACSHA512": "NetIQ SSPR (PBKDF2WithHmacSHA512)",
|
||||
"KERBEROS_5_ETYPE_17_AS_REP": "Kerberos 5, etype 17, AS-REP",
|
||||
"KERBEROS_5_ETYPE_18_AS_REP": "Kerberos 5, etype 18, AS-REP",
|
||||
"EMPIRE_CMS_ADMIN_PASSWORD": "Empire CMS (Admin password)",
|
||||
"SHA512_SHA512_PASS_SALT": "sha512(sha512($pass).$salt)",
|
||||
"SHA512_SHA512_BIN_PASS_SALT": "sha512(sha512_bin($pass).$salt)",
|
||||
"DOGECHAIN_INFO_WALLET": "Dogechain.info Wallet",
|
||||
"CUBECART_WHIRLPOOL_SALT_PASS_SALT": "CubeCart (whirlpool($salt.$pass.$salt))",
|
||||
"KREMLIN_ENCRYPT_3_0_W_NEWDES": "Kremlin Encrypt 3.0 w/NewDES",
|
||||
"MD5_SHA1_MD5_PASS": "md5(sha1(md5($pass)))",
|
||||
"PBKDF1_SHA1": "PBKDF1-SHA1",
|
||||
"MD5_SALT1_PASS_SALT2": "md5($salt1.$pass.$salt2)",
|
||||
"MD5_SALT_MD5_PASS_SALT_MODE_33100": "md5($salt.md5($pass).$salt)",
|
||||
"HMAC_BLAKE2S_KEY_PASS": "HMAC-BLAKE2S (key = $pass)",
|
||||
"MEGA_NZ_PASSWORD_PROTECTED_LINK_PBKDF2_HMAC_SHA512": "mega.nz password-protected link (PBKDF2-HMAC-SHA512)",
|
||||
"RC4_40_BIT_DROPN": "RC4 40-bit DropN",
|
||||
"RC4_72_BIT_DROPN": "RC4 72-bit DropN",
|
||||
"RC4_104_BIT_DROPN": "RC4 104-bit DropN",
|
||||
"RIPEMD_320": "RIPEMD-320",
|
||||
"HMAC_RIPEMD320_KEY_PASS": "HMAC-RIPEMD320 (key = $pass)",
|
||||
"HMAC_RIPEMD320_KEY_SALT": "HMAC-RIPEMD320 (key = $salt)",
|
||||
"MICROSOFT_ONLINE_ACCOUNT_PBKDF2_HMAC_SHA256_AES256": "Microsoft Online Account (PBKDF2-HMAC-SHA256 + AES256)",
|
||||
"WBB4_WOLTLAB_BURNING_BOARD_BCRYPT_BCRYPT_PASS": "WBB4 (Woltlab Burning Board) [bcrypt(bcrypt($pass))]",
|
||||
"CITRIX_NETSCALER_PBKDF2_HMAC_SHA256": "Citrix NetScaler (PBKDF2-HMAC-SHA256)",
|
||||
"ARGON2": "Argon2",
|
||||
"LUKS_V2_ARGON2_SHA_256_AES": "LUKS v2 argon2 + SHA-256 + AES",
|
||||
"MURMURHASH64A": "MurmurHash64A",
|
||||
"MURMURHASH64A_ZERO_SEED": "MurmurHash64A (zero seed)",
|
||||
"MURMURHASH64A_TRUNCATED_ZERO_SEED": "MurmurHash64A truncated (zero seed)",
|
||||
"KEEPASS_ARGON2_KDBX_V4": "KeePass Argon2 (KDBX v4)",
|
||||
"KEEPASS_AESKDF_KDBX_V4": "KeePass AESKDF (KDBX v4)",
|
||||
"SHA224_SHA224_PASS": "sha224(sha224($pass))",
|
||||
"SHA224_SHA1_PASS": "sha224(sha1($pass))",
|
||||
"MD6_256": "MD6 (256)",
|
||||
"BLOCKCHAIN_MY_WALLET_LEGACY_WALLETS": "Blockchain, My Wallet, Legacy Wallets",
|
||||
"BLAKE2B_256_MODE_34800": "BLAKE2b-256",
|
||||
"BLAKE2B_256_PASS_SALT": "BLAKE2b-256($pass.$salt)",
|
||||
"BLAKE2B_256_SALT_PASS": "BLAKE2b-256($salt.$pass)",
|
||||
"SAP_CODVN_H_PWDSALTEDHASH_ISSHA512": "SAP CODVN H (PWDSALTEDHASH) isSHA512",
|
||||
"SM3CRYPT_SM3_SM3_UNIX": "sm3crypt $sm3$, SM3 (Unix)",
|
||||
"AS_400_SSHA1": "AS/400 SSHA1",
|
||||
"KERBEROS_5_ETYPE_23_TGS_REP_NT": "Kerberos 5, etype 23, TGS-REP (NT)",
|
||||
"KERBEROS_5_ETYPE_23_AS_REP_NT": "Kerberos 5, etype 23, AS-REP (NT)",
|
||||
"WORDPRESS_BCRYPT_HMAC_SHA384_PASS": "Wordpress bcrypt(hmac-sha384($pass))",
|
||||
"GOST12512CRYPT_GOST12512HASH_UNIX": "gost12512crypt [$gost12512hash$] (Unix)",
|
||||
"PHPASS_MD5_PASS": "phpass(md5($pass))",
|
||||
"SYMFONY_LEGACY_SHA256": "Symfony Legacy SHA256",
|
||||
"YESCRYPT": "yescrypt",
|
||||
"GOST_YESCRYPT": "gost-yescrypt",
|
||||
"SCRYPTCRYPT_SCRYPT_UNIX": "scryptcrypt, scrypt (Unix)",
|
||||
"KDE_KWALLET_4_13_PBKDF2_HMAC_SHA512_BLOWFISH": "KDE KWallet 4.13+ (PBKDF2-HMAC-SHA512, Blowfish)",
|
||||
"KDE_KWALLET_4_13_SHA_1_BLOWFISH": "KDE KWallet < 4.13 (SHA-1, Blowfish)",
|
||||
"ARGON2ID_BRIDGED_REFERENCE_IMPLEMENTATION_TUNINGS": "Argon2id [Bridged: reference implementation + tunings]",
|
||||
"SCRYPT_BRIDGED_SCRYPT_JANE_SMIX": "scrypt [Bridged: Scrypt-Jane SMix]",
|
||||
"SCRYPT_BRIDGED_SCRYPT_YESCRYPT": "scrypt [Bridged: Scrypt-Yescrypt]",
|
||||
"GENERIC_HASH_BRIDGED_PYTHON_INTERPRETER_FREE_THREADING": "Generic Hash [Bridged: Python Interpreter free-threading]",
|
||||
"GENERIC_HASH_BRIDGED_PYTHON_INTERPRETER_WITH_GIL": "Generic Hash [Bridged: Python Interpreter with GIL]",
|
||||
"GENERIC_HASH_BRIDGED_RUST": "Generic Hash [Bridged: Rust]",
|
||||
"PLAINTEXT": "Plaintext",
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package creds
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
const (
|
||||
pinnedHashcatVersion = "v7.1.2-armory.1"
|
||||
pinnedHashcatCommit = "deadd52de22e2753e9caaff6d1b92e5a22a35626"
|
||||
pinnedHashcatModuleCount = 595
|
||||
pinnedHashcatCatalogSHA256 = "1c041b6370a37ccf07ae71ff29205c56c67eeb7dbe1f7bbc134e0373ecd19e4f"
|
||||
pinnedHashcatModeIDsSHA256 = "bb206df105890129937d0c688101a3065aad2ca45eea7765bfda60c40f232a63"
|
||||
pinnedHashcatEnumsSHA256 = "989144b3d2a57f9659bc5aa53df42403ba78b08e5a6fab616d43c14e0352e023"
|
||||
)
|
||||
|
||||
type hashcatCatalogEntry struct {
|
||||
mode int32
|
||||
enumName string
|
||||
description string
|
||||
}
|
||||
|
||||
func TestHashcatV7CatalogCoverage(t *testing.T) {
|
||||
version := pinnedHashcatVersion + "@" + pinnedHashcatCommit
|
||||
validateHashcatCatalogCardinality(t, version)
|
||||
catalog := collectHashcatCatalog(t, version)
|
||||
validateHashTypeValueRoundTrips(t)
|
||||
validateHashTypeDescriptionParity(t)
|
||||
validateHashcatCatalogDigests(t, version, catalog)
|
||||
}
|
||||
|
||||
func validateHashcatCatalogCardinality(t *testing.T, version string) {
|
||||
t.Helper()
|
||||
|
||||
if got := len(clientpb.HashType_name); got != pinnedHashcatModuleCount+1 {
|
||||
t.Fatalf("%s HashType_name contains %d values, want %d modules plus INVALID", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
if got := len(clientpb.HashType_value); got != pinnedHashcatModuleCount+1 {
|
||||
t.Fatalf("%s HashType_value contains %d values, want %d modules plus INVALID", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
if got := len(hashTypes); got != pinnedHashcatModuleCount {
|
||||
t.Fatalf("%s hashTypes contains %d descriptions, want %d", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
}
|
||||
|
||||
func collectHashcatCatalog(t *testing.T, version string) []hashcatCatalogEntry {
|
||||
t.Helper()
|
||||
|
||||
catalog := make([]hashcatCatalogEntry, 0, pinnedHashcatModuleCount)
|
||||
seenDescriptions := make(map[string]int32, pinnedHashcatModuleCount)
|
||||
|
||||
for mode, enumName := range clientpb.HashType_name {
|
||||
if mode == int32(clientpb.HashType_INVALID) {
|
||||
if enumName != "INVALID" || mode != 9999 {
|
||||
t.Errorf("invalid sentinel is %d -> %q, want 9999 -> INVALID", mode, enumName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if mode < 0 {
|
||||
t.Errorf("HashType_name contains negative mode %d (%s)", mode, enumName)
|
||||
continue
|
||||
}
|
||||
|
||||
description, ok := hashTypes[enumName]
|
||||
if !ok {
|
||||
t.Errorf("hashTypes is missing %d -> %s", mode, enumName)
|
||||
continue
|
||||
}
|
||||
if previous, duplicate := seenDescriptions[description]; duplicate {
|
||||
t.Errorf("Hashcat name %q is shared by modes %d and %d", description, previous, mode)
|
||||
}
|
||||
seenDescriptions[description] = mode
|
||||
if got, ok := clientpb.HashType_value[enumName]; !ok || got != mode {
|
||||
t.Errorf("HashType_value[%q] = %d, %v; want %d, true", enumName, got, ok, mode)
|
||||
}
|
||||
if got := clientpb.HashType(mode).String(); got != enumName {
|
||||
t.Errorf("HashType(%d).String() = %q, want %q", mode, got, enumName)
|
||||
}
|
||||
catalog = append(catalog, hashcatCatalogEntry{mode: mode, enumName: enumName, description: description})
|
||||
}
|
||||
if got := len(catalog); got != pinnedHashcatModuleCount {
|
||||
t.Fatalf("%s catalog contains %d real modes, want %d", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
func validateHashTypeValueRoundTrips(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for enumName, mode := range clientpb.HashType_value {
|
||||
canonical, ok := clientpb.HashType_name[mode]
|
||||
if !ok || canonical != enumName {
|
||||
t.Errorf("HashType alias or orphan %q -> %d; canonical name is %q, %v", enumName, mode, canonical, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateHashTypeDescriptionParity(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for enumName, description := range hashTypes {
|
||||
mode, ok := clientpb.HashType_value[enumName]
|
||||
if !ok || mode == int32(clientpb.HashType_INVALID) {
|
||||
t.Errorf("hashTypes contains non-catalog key %q -> %q", enumName, description)
|
||||
}
|
||||
if description == "" {
|
||||
t.Errorf("hashTypes[%q] has an empty description", enumName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateHashcatCatalogDigests(t *testing.T, version string, catalog []hashcatCatalogEntry) {
|
||||
t.Helper()
|
||||
|
||||
sort.Slice(catalog, func(i, j int) bool { return catalog[i].mode < catalog[j].mode })
|
||||
var catalogRows strings.Builder
|
||||
var modeRows strings.Builder
|
||||
var enumRows strings.Builder
|
||||
for _, entry := range catalog {
|
||||
fmt.Fprintf(&catalogRows, "%d\t%s\n", entry.mode, entry.description)
|
||||
fmt.Fprintf(&modeRows, "%05d\n", entry.mode)
|
||||
fmt.Fprintf(&enumRows, "%d\t%s\t%s\n", entry.mode, entry.enumName, entry.description)
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(catalogRows.String()))); got != pinnedHashcatCatalogSHA256 {
|
||||
t.Errorf("%s mode/name catalog SHA-256 = %s, want %s", version, got, pinnedHashcatCatalogSHA256)
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(modeRows.String()))); got != pinnedHashcatModeIDsSHA256 {
|
||||
t.Errorf("%s mode ID SHA-256 = %s, want %s", version, got, pinnedHashcatModeIDsSHA256)
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(enumRows.String()))); got != pinnedHashcatEnumsSHA256 {
|
||||
t.Errorf("%s mode/enum/name SHA-256 = %s, want %s", version, got, pinnedHashcatEnumsSHA256)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashcatIdentifierCollisionResolution(t *testing.T) {
|
||||
want := map[int32]string{
|
||||
141: "EPISERVER_6_X_NET_4_MODE_141",
|
||||
600: "BLAKE2B_256",
|
||||
1441: "EPISERVER_6_X_NET_4_MODE_1441",
|
||||
2611: "VBULLETIN_V3_8_5_MODE_2611",
|
||||
2711: "VBULLETIN_V3_8_5_MODE_2711",
|
||||
4110: "MD5_SALT_MD5_PASS_SALT_MODE_4110",
|
||||
4410: "MD5_SHA1_PASS_SALT_MODE_4410",
|
||||
4420: "MD5_SHA1_PASS_SALT_MODE_4420",
|
||||
4710: "SHA1_MD5_PASS_SALT_MODE_4710",
|
||||
14700: "ITUNES_BACKUP_10_0_MODE_14700",
|
||||
14800: "ITUNES_BACKUP_10_0_MODE_14800",
|
||||
20710: "SHA256_SHA256_PASS_SALT_MODE_20710",
|
||||
20730: "SHA256_SHA256_PASS_SALT_MODE_20730",
|
||||
21100: "SHA1_MD5_PASS_SALT_MODE_21100",
|
||||
21900: "MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_21900",
|
||||
31700: "MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_31700",
|
||||
31800: "HASHCAT_1PASSWORD_MOBILEKEYCHAIN_1PASSWORD_8",
|
||||
33100: "MD5_SALT_MD5_PASS_SALT_MODE_33100",
|
||||
34800: "BLAKE2B_256_MODE_34800",
|
||||
6600: "HASHCAT_1PASSWORD_AGILEKEYCHAIN",
|
||||
8200: "HASHCAT_1PASSWORD_CLOUDKEYCHAIN",
|
||||
11600: "HASHCAT_7_ZIP",
|
||||
}
|
||||
for mode, enumName := range want {
|
||||
if got := clientpb.HashType_name[mode]; got != enumName {
|
||||
t.Errorf("HashType_name[%d] = %q, want collision-safe %q", mode, got, enumName)
|
||||
}
|
||||
}
|
||||
|
||||
groups := map[string][]int32{}
|
||||
for mode, enumName := range clientpb.HashType_name {
|
||||
if mode == int32(clientpb.HashType_INVALID) {
|
||||
continue
|
||||
}
|
||||
description, ok := hashTypes[enumName]
|
||||
if !ok {
|
||||
t.Fatalf("hashTypes is missing collision input %d -> %s", mode, enumName)
|
||||
}
|
||||
base := normalizeHashcatIdentifier(description)
|
||||
groups[base] = append(groups[base], mode)
|
||||
}
|
||||
collisions := 0
|
||||
for _, modes := range groups {
|
||||
if len(modes) > 1 {
|
||||
collisions++
|
||||
}
|
||||
}
|
||||
if collisions != 8 {
|
||||
t.Errorf("normalized catalog contains %d collision groups, want 8", collisions)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeHashcatIdentifier(name string) string {
|
||||
var builder strings.Builder
|
||||
underscore := false
|
||||
for _, r := range strings.ToUpper(name) {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
builder.WriteRune(r)
|
||||
underscore = false
|
||||
continue
|
||||
}
|
||||
if builder.Len() > 0 && !underscore {
|
||||
builder.WriteByte('_')
|
||||
underscore = true
|
||||
}
|
||||
}
|
||||
return strings.Trim(builder.String(), "_")
|
||||
}
|
||||
@@ -228,6 +228,14 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
|
||||
DialTimeout: 30 * time.Second,
|
||||
}
|
||||
tcpProxy.AddRoute(bindAddr, channelProxy)
|
||||
if err := tcpProxy.Start(); err != nil {
|
||||
channelProxy.Stop()
|
||||
_ = tcpProxy.Close()
|
||||
return nil, errors.Join(
|
||||
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
|
||||
terminateStartedCursedProcess(con, session.ID, chromeExec.GetPid()),
|
||||
)
|
||||
}
|
||||
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
|
||||
|
||||
curse := &core.CursedProcess{
|
||||
@@ -241,7 +249,7 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
|
||||
}
|
||||
core.CursedProcesses.Store(bindPort, curse)
|
||||
go func() {
|
||||
err := tcpProxy.Run()
|
||||
err := tcpProxy.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Proxy error %s", err)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func selectDebugTarget(targets []overlord.ChromeDebugTarget, con *console.Sliver
|
||||
if selected == "" {
|
||||
return nil
|
||||
}
|
||||
selectedID := strings.Split(selected, " ")[0]
|
||||
selectedID, _, _ := strings.Cut(selected, " ")
|
||||
selectedTarget := id2target[selectedID]
|
||||
return &selectedTarget
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package cursed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
@@ -190,6 +191,14 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
|
||||
DialTimeout: 30 * time.Second,
|
||||
}
|
||||
tcpProxy.AddRoute(bindAddr, channelProxy)
|
||||
if err := tcpProxy.Start(); err != nil {
|
||||
channelProxy.Stop()
|
||||
_ = tcpProxy.Close()
|
||||
return nil, errors.Join(
|
||||
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
|
||||
terminateStartedCursedProcess(con, session.ID, electronExec.GetPid()),
|
||||
)
|
||||
}
|
||||
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
|
||||
|
||||
curse := &core.CursedProcess{
|
||||
@@ -202,7 +211,7 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
|
||||
}
|
||||
core.CursedProcesses.Store(bindPort, curse)
|
||||
go func() {
|
||||
err := tcpProxy.Run()
|
||||
err := tcpProxy.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Proxy error %s", err)
|
||||
}
|
||||
|
||||
@@ -20,20 +20,47 @@ package cursed
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const cursedStartCleanupTimeout = 30 * time.Second
|
||||
|
||||
func terminateStartedCursedProcess(con *console.SliverClient, sessionID string, pid uint32) error {
|
||||
const maxTerminatePID = uint32(1<<31 - 1)
|
||||
if con == nil || con.Rpc == nil || sessionID == "" || pid == 0 || pid > maxTerminatePID {
|
||||
return errors.New("cannot terminate incompletely tracked cursed process")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cursedStartCleanupTimeout)
|
||||
defer cancel()
|
||||
response, err := con.Rpc.Terminate(ctx, &sliverpb.TerminateReq{
|
||||
Request: &commonpb.Request{SessionID: sessionID},
|
||||
Pid: int32(pid),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("terminate started cursed process %d: %w", pid, err)
|
||||
}
|
||||
if response.GetResponse().GetErr() != "" {
|
||||
return fmt.Errorf("terminate started cursed process %d: %s", pid, response.GetResponse().GetErr())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursedChromeCmd - Execute a .NET assembly in-memory.
|
||||
func CursedCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
// Collect existing curses from core
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Commands returns the docs command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newDocsCommand(consts.SliverCoreHelpGroup, con)}
|
||||
}
|
||||
|
||||
// ServerCommands returns the docs command for the top-level client REPL.
|
||||
func ServerCommands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newDocsCommand(consts.GenericHelpGroup, con)}
|
||||
}
|
||||
|
||||
func newDocsCommand(groupID string, con *console.SliverClient) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: consts.DocsStr,
|
||||
Short: "Browse the embedded Sliver docs in a TUI",
|
||||
Long: help.GetHelpFor([]string{consts.DocsStr}),
|
||||
Args: cobra.NoArgs,
|
||||
GroupID: groupID,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
DocsCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// DocsCmd launches the embedded docs browser TUI.
|
||||
func DocsCmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
entries, err := loadDocEntries()
|
||||
if err != nil {
|
||||
if con != nil {
|
||||
con.PrintErrorf("Failed to load embedded docs: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
model := newDocsModel(entries)
|
||||
|
||||
width, height := 100, 32
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
width, height = w, h
|
||||
}
|
||||
|
||||
program := tea.NewProgram(
|
||||
model,
|
||||
tea.WithWindowSize(width, height),
|
||||
tea.WithColorProfile(colorprofile.TrueColor),
|
||||
)
|
||||
if _, err := program.Run(); err != nil && con != nil {
|
||||
con.PrintErrorf("Docs TUI error: %s\n", err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,255 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
func sampleDocs() []docEntry {
|
||||
return []docEntry{
|
||||
{
|
||||
Name: "Getting Started",
|
||||
Content: "# Getting Started\n\n- first step\n- second step",
|
||||
Description: "Getting Started",
|
||||
},
|
||||
{
|
||||
Name: "Operators",
|
||||
Content: "# Operators\n\nTeam workflows",
|
||||
Description: "Team workflows",
|
||||
},
|
||||
{
|
||||
Name: "Networking",
|
||||
Content: "# Networking\n\nTransport details",
|
||||
Description: "Transport details",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMarkdownWithGlowRendersBulletLists(t *testing.T) {
|
||||
rendered, err := renderMarkdownWithGlow(48, "# Title\n\n- first item")
|
||||
if err != nil {
|
||||
t.Fatalf("renderMarkdownWithGlow(): %v", err)
|
||||
}
|
||||
|
||||
plain := ansi.Strip(rendered)
|
||||
if !strings.Contains(plain, "Title") {
|
||||
t.Fatalf("expected heading text, got %q", plain)
|
||||
}
|
||||
if !strings.Contains(plain, "• first item") {
|
||||
t.Fatalf("expected glow-style bullet rendering, got %q", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeMarkdownPrefersFirstMeaningfulLine(t *testing.T) {
|
||||
summary := summarizeMarkdown("# Title\n\n- Bullet\n\nParagraph")
|
||||
if summary != "Title" {
|
||||
t.Fatalf("expected first heading summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeMarkdownStripsMarkdownAndUnicodeSymbols(t *testing.T) {
|
||||
summary := summarizeMarkdown("⚠️ **IMPORTANT:** This content was NOT created by the Sliver authors.")
|
||||
if summary != "IMPORTANT: This content was NOT created by the Sliver authors." {
|
||||
t.Fatalf("expected sanitized browser summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelDefaultsToGettingStarted(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
if model.currentDocName != "Getting Started" {
|
||||
t.Fatalf("expected Getting Started to be selected, got %q", model.currentDocName)
|
||||
}
|
||||
plain := ansi.Strip(model.viewer.View())
|
||||
if !strings.Contains(plain, "first step") {
|
||||
t.Fatalf("expected initial viewer content to render selected doc, got %q", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelSlashFocusesBrowserAndStartsFiltering(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
model.focus = docsFocusViewer
|
||||
|
||||
updated, cmd := model.Update(tea.KeyPressMsg{Text: "/", Code: '/'})
|
||||
if cmd == nil {
|
||||
t.Fatal("expected slash to focus the search input")
|
||||
}
|
||||
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.focus != docsFocusBrowser {
|
||||
t.Fatalf("expected slash to move focus to browser, got %v", updatedModel.focus)
|
||||
}
|
||||
if updatedModel.filterState != docsFilterStateFiltering {
|
||||
t.Fatalf("expected slash to enter filtering state, got %v", updatedModel.filterState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelFilteringUpdatesVisibleEntries(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("oper")
|
||||
model.updateFilteredEntries(false)
|
||||
|
||||
visible := model.visibleEntries()
|
||||
if len(visible) != 1 || visible[0].Name != "Operators" {
|
||||
t.Fatalf("expected Operators to be the only filtered result, got %+v", visible)
|
||||
}
|
||||
if model.currentDocName != "Operators" {
|
||||
t.Fatalf("expected filtering to sync selection to Operators, got %q", model.currentDocName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelFilteringMatchesContentAndNormalizedTerms(t *testing.T) {
|
||||
entries := []docEntry{
|
||||
{
|
||||
Name: "HTTP C2",
|
||||
Content: "# HTTP C2\n\nUse the staging listener and HTTPS transports.",
|
||||
Description: "Transport details",
|
||||
},
|
||||
{
|
||||
Name: "Networking",
|
||||
Content: "# Networking\n\nTCP and UDP details",
|
||||
Description: "Network basics",
|
||||
},
|
||||
}
|
||||
model := newDocsModel(entries)
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("https transport")
|
||||
model.updateFilteredEntries(false)
|
||||
if len(model.visibleEntries()) != 1 || model.visibleEntries()[0].Name != "HTTP C2" {
|
||||
t.Fatalf("expected HTTP C2 to match multi-term query, got %+v", model.visibleEntries())
|
||||
}
|
||||
|
||||
model.browserFilter.SetValue("https-transport")
|
||||
model.updateFilteredEntries(false)
|
||||
if len(model.visibleEntries()) != 1 || model.visibleEntries()[0].Name != "HTTP C2" {
|
||||
t.Fatalf("expected punctuation normalization to match HTTP C2, got %+v", model.visibleEntries())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelFilteringRanksTitleMatchesFirst(t *testing.T) {
|
||||
entries := []docEntry{
|
||||
{
|
||||
Name: "Advanced Setup",
|
||||
Content: "This section explains listener configuration.",
|
||||
},
|
||||
{
|
||||
Name: "Listener Configuration",
|
||||
Content: "Configure network settings.",
|
||||
},
|
||||
}
|
||||
model := newDocsModel(entries)
|
||||
model.applyWindowSize(120, 32)
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("listener")
|
||||
model.updateFilteredEntries(false)
|
||||
|
||||
visible := model.visibleEntries()
|
||||
if len(visible) != 2 {
|
||||
t.Fatalf("expected both documents to match, got %+v", visible)
|
||||
}
|
||||
if visible[0].Name != "Listener Configuration" {
|
||||
t.Fatalf("expected title match to rank first, got %q", visible[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelEnterMovesFocusToViewer(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.focus != docsFocusViewer {
|
||||
t.Fatalf("expected enter to focus viewer, got %v", updatedModel.focus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelEscClearsAppliedFilter(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("net")
|
||||
model.finishFiltering(false)
|
||||
|
||||
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.filterState != docsFilterStateUnfiltered {
|
||||
t.Fatalf("expected esc to clear the filter, got %v", updatedModel.filterState)
|
||||
}
|
||||
if len(updatedModel.visibleEntries()) != len(sampleDocs()) {
|
||||
t.Fatalf("expected filter clear to restore all docs, got %d visible", len(updatedModel.visibleEntries()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelBrowserBodyPinsHelpToBottom(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 28)
|
||||
|
||||
body := ansi.Strip(model.renderBrowserBody(model.browserWidth, model.browserHeight))
|
||||
lines := strings.Split(body, "\n")
|
||||
if len(lines) != model.browserHeight {
|
||||
t.Fatalf("expected browser body height %d, got %d lines", model.browserHeight, len(lines))
|
||||
}
|
||||
if !strings.Contains(lines[len(lines)-1], "q quit") {
|
||||
t.Fatalf("expected help controls on the last browser line, got %q", lines[len(lines)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsWindowPollSchedulesResizeAndKeepsPolling(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(100, 28)
|
||||
|
||||
updated, cmd := model.Update(docsWindowPollMsg{width: 120, height: 34})
|
||||
if cmd == nil {
|
||||
t.Fatal("expected window poll to schedule resize handling")
|
||||
}
|
||||
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.width != 100 || updatedModel.height != 28 {
|
||||
t.Fatalf("expected poll tick to leave dimensions unchanged until WindowSizeMsg, got %dx%d", updatedModel.width, updatedModel.height)
|
||||
}
|
||||
|
||||
msg := cmd()
|
||||
batch, ok := msg.(tea.BatchMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected poll tick to return tea.BatchMsg, got %T", msg)
|
||||
}
|
||||
if len(batch) != 2 {
|
||||
t.Fatalf("expected poll tick batch size 2, got %d", len(batch))
|
||||
}
|
||||
|
||||
var sawWindowSize bool
|
||||
var sawPollTick bool
|
||||
for _, subcmd := range batch {
|
||||
if subcmd == nil {
|
||||
continue
|
||||
}
|
||||
submsg := subcmd()
|
||||
switch msg := submsg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
sawWindowSize = true
|
||||
if msg.Width != 120 || msg.Height != 34 {
|
||||
t.Fatalf("expected resize 120x34, got %dx%d", msg.Width, msg.Height)
|
||||
}
|
||||
case docsWindowPollMsg:
|
||||
sawPollTick = true
|
||||
}
|
||||
}
|
||||
|
||||
if !sawWindowSize {
|
||||
t.Fatal("expected poll tick to emit WindowSizeMsg")
|
||||
}
|
||||
if !sawPollTick {
|
||||
t.Fatal("expected poll tick to schedule another poll")
|
||||
}
|
||||
}
|
||||
@@ -396,10 +396,7 @@ func (m *editorModel) renderLine(index int) string {
|
||||
return ""
|
||||
}
|
||||
prefixWidth := m.lineNumberWidth()
|
||||
contentWidth := totalWidth - prefixWidth
|
||||
if contentWidth < 0 {
|
||||
contentWidth = 0
|
||||
}
|
||||
contentWidth := max(totalWidth-prefixWidth, 0)
|
||||
prefix := m.linePrefix(index, prefixWidth)
|
||||
|
||||
if index >= len(m.lines) {
|
||||
@@ -508,10 +505,7 @@ func (m *editorModel) lineNumberWidth() int {
|
||||
if !m.showLineNumbers {
|
||||
return 0
|
||||
}
|
||||
total := len(m.lines)
|
||||
if total < 1 {
|
||||
total = 1
|
||||
}
|
||||
total := max(len(m.lines), 1)
|
||||
return len(fmt.Sprintf("%d", total)) + 1
|
||||
}
|
||||
|
||||
@@ -559,10 +553,7 @@ func (m *editorModel) ensureCursorVisible() {
|
||||
m.top = 0
|
||||
}
|
||||
|
||||
width := m.textWidth() - m.lineNumberWidth()
|
||||
if width < 1 {
|
||||
width = 1
|
||||
}
|
||||
width := max(m.textWidth()-m.lineNumberWidth(), 1)
|
||||
if m.col < m.left {
|
||||
m.left = m.col
|
||||
} else if m.col >= m.left+width {
|
||||
@@ -1125,20 +1116,13 @@ func lastIndexOfRunes(haystack, needle []rune, end int) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func baseName(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
clean := strings.ReplaceAll(path, "\\", "/")
|
||||
if idx := strings.LastIndex(clean, "/"); idx != -1 {
|
||||
return clean[idx+1:]
|
||||
if _, after, ok := strings.CutLast(clean, "/"); ok {
|
||||
return after
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
Loaded 100 of 5151 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user