Compare commits

...
104 Commits
Author SHA1 Message Date
hack-ita 6ccfd6a257 docs: add HackITA Sliver C2 community guide (#2434) 2026-11-08 09:31:01 -08:00
Joe 4c2e8d4bda Simplify unit tests and strengthen deterministic coverage (#2433) 2026-11-06 15:32:31 -08:00
Joe 9f8f1d11be Fix DNS canary embedding and detection (#2432)
* Preserve DNS canaries in obfuscated implants

* Refactor canary regression assertions for lint

* Fix DNS canary detection and trigger persistence
2026-11-05 09:42:05 -08:00
Joe 6cf3ad78bd Revert Dependabot docs dependency updates (#2425) 2026-10-24 21:27:15 -07:00
Joe 56e140826f build(deps): combine compatible Dependabot updates (#2424) 2026-10-24 20:29:44 -07:00
Joe ad6ba0a61b Merge pull request #2423 from BishopFox/feat/protobuf-obfuscation-hardening
Harden protobuf obfuscation generation
2026-10-24 11:29:43 -07:00
moloch-- 9e1c1e7c67 Obfuscate all implant protobuf field tags 2026-10-24 09:45:23 -07:00
moloch-- a36b0b1d1d Refactor protobuf rewrites to satisfy lint complexity 2026-10-24 08:44:06 -07:00
moloch-- 4c1350d8cb Harden protobuf obfuscation generation 2026-10-24 08:39:08 -07:00
Joe 3e409bdc91 Merge pull request #2422 from BishopFox/feat/implant-size-consolidated
Reduce implant sizes across obfuscated and release builds
2026-10-23 08:30:02 -07:00
moloch-- 46eb1c22a4 feat: compact obfuscated amd64 executable alignment 2026-10-22 16:08:32 -07:00
moloch-- f857903c83 Strip non-debug plain implant executables 2026-10-22 16:04:49 -07:00
moloch-- ee53c01313 Reduce WASM extension gzip dependency in implants 2026-10-22 16:04:33 -07:00
moloch-- 14a43e6510 Reduce obfuscated implant size with shared short literal decoding 2026-10-22 13:46:04 -07:00
moloch-- 931e3f9046 Refresh bundled Garble when asset source changes 2026-10-22 13:45:46 -07:00
moloch-- a6d5e97d45 Pin Garble v1.27.3a signed release 2026-10-22 12:55:46 -07:00
moloch-- f0ef60e25f Reduce obfuscated implant size with shared long literal decoding 2026-10-22 12:50:04 -07:00
Joe 9fdc66e4a8 Merge pull request #2414 from BishopFox/feat/go-garble-1.27.3
Update embedded Go and Garble to 1.27.3
2026-10-22 10:43:57 -07:00
moloch-- ab960fd4ed Make SOCKS receive test shutdown deterministic 2026-10-22 08:42:19 -07:00
moloch-- 34fc1c5ad6 Update embedded Go and Garble to 1.27.3 2026-10-22 08:23:51 -07:00
Joe ac65aa3708 Merge pull request #2412 from BishopFox/feat/fix-initial-prompt
Fix update reminder corrupting the initial console prompt
2026-10-21 15:30:21 -07:00
moloch-- 73bf782656 Avoid shadowing print in startup regression test 2026-10-21 08:19:39 -07:00
moloch-- a0a303ae89 Fix update reminder corrupting the initial console prompt 2026-10-21 08:16:22 -07:00
Joe 5383bd67b5 Merge pull request #2408 from skinnypuppy/fix/mcp-keepalive
client/cli: keep the MCP bridge alive across idle gRPC channels
2026-10-19 12:50:42 -07:00
Joe 73ea63fd44 Merge pull request #2405 from rob-cshub/feat/non-inter-stage
Add non-interactive support for 'implants stage'
2026-10-19 12:03:33 -07:00
Joe 3ef04241a1 Merge pull request #2410 from BishopFox/dependabot/github_actions/actions/setup-go-7
build(deps): bump actions/setup-go from 6 to 7
2026-10-19 12:03:07 -07:00
dependabot[bot] 402b5459e6 build(deps): bump actions/setup-go from 6 to 7
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6 to 7.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-19 18:38:45 +00:00
Joe 7c56553372 Merge pull request #2409 from BishopFox/dependabot/github_actions/actions/configure-pages-6
build(deps): bump actions/configure-pages from 5 to 6
2026-10-19 11:36:44 -07:00
dependabot[bot] a40754eaac build(deps): bump actions/configure-pages from 5 to 6
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 5 to 6.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-19 03:03:00 +00:00
skinnypuppy 042bb6bccc client/cli: keep the MCP bridge alive across idle gRPC channels
gRPC-Go closes client transports after its default 30-minute idle
timeout (no RPCs in flight) and moves the channel to Idle; the next
RPC transparently reconnects. The MCP bridge installed the
connection-lost watchdog, which treats any Idle transition as a lost
connection and exits the process - so `sliver-client mcp` reliably
died ~30 minutes after connecting, forcing the MCP host to reconnect.

Do not install the watchdog for the MCP bridge (an idle channel simply
reconnects on the next tool call; a down server surfaces as an RPC
error to the MCP host instead of a dead bridge process) and enable the
client keepalive pings that #2273 added but never wired up, so dead
connections are detected quickly while the channel is in use.

Client keepalive pings do not reset the idle timer, so this cannot be
fixed by configuration alone.
2026-10-18 14:44:56 -06:00
Robert Pretschner 8bc7273cad Add non-interactive mode to 'implants stage' 2026-10-15 14:11:36 +02:00
Joe 8e5d2f9dbe Merge pull request #2406 from BishopFox/feat/go-garble-1.27.2
Update embedded Go and Garble to 1.27.2
2026-10-14 08:51:22 -07:00
moloch-- 52864c481f Fix lint toolchain for Go 1.27.2 2026-10-14 07:04:43 -07:00
moloch-- 2f6fca1eac Update embedded Go and Garble to 1.27.2 2026-10-13 21:58:13 -07:00
Joe 481b5b0534 Merge pull request #2397 from BishopFox/dependabot/github_actions/actions/checkout-7
build(deps): bump actions/checkout from 6 to 7
2026-10-12 12:48:13 -07:00
Joe 65efd3f2fd Merge pull request #2398 from BishopFox/dependabot/github_actions/actions/upload-pages-artifact-5
build(deps): bump actions/upload-pages-artifact from 4 to 5
2026-10-12 12:47:57 -07:00
Joe 58c5f33cf5 Merge pull request #2399 from BishopFox/dependabot/github_actions/actions/cache-6
build(deps): bump actions/cache from 5 to 6
2026-10-12 12:47:43 -07:00
Joe 3e20660ab0 Merge pull request #2400 from BishopFox/dependabot/github_actions/actions/setup-node-7
build(deps): bump actions/setup-node from 6 to 7
2026-10-12 12:47:28 -07:00
Joe acb6cd2d77 Merge pull request #2401 from BishopFox/dependabot/github_actions/actions/deploy-pages-5
build(deps): bump actions/deploy-pages from 4 to 5
2026-10-12 12:47:13 -07:00
dependabot[bot] 7f82085467 build(deps): bump actions/deploy-pages from 4 to 5
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:24 +00:00
dependabot[bot] 1ac72de74d build(deps): bump actions/setup-node from 6 to 7
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:20 +00:00
dependabot[bot] 94df7a3e58 build(deps): bump actions/cache from 5 to 6
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:15 +00:00
dependabot[bot] 329e44dfd9 build(deps): bump actions/upload-pages-artifact from 4 to 5
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:08 +00:00
dependabot[bot] 5d303fca79 build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:01 +00:00
Joe 90f759dac8 Merge pull request #2395 from BishopFox/feat/defensive-security-fixes
Random Bug Fixes
2026-10-10 10:05:26 -07:00
Joe 1fc9e256c0 Merge pull request #2396 from zwanski2019/feature/docs-search-relevance
Improve embedded docs search relevance
2026-10-10 09:48:26 -07:00
moloch-- 13935b2e68 fix(cli): preserve configuration import exit codes 2026-10-10 09:34:17 -07:00
moloch-- 1db7b99685 fix(websites): preserve content in metadata-only requests 2026-10-10 09:34:08 -07:00
zwanski2019andCopilot b48e3fc537 Improve embedded docs search relevance
Search document content using normalized multi-term queries and rank title matches above body matches. Add focused tests for content search and ranking.

Co-authored-by: Copilot <[email protected]>
2026-10-09 11:30:59 +01:00
moloch-- 10ec67d274 fix(logging): omit SQLite connection strings from startup logs 2026-10-08 09:07:43 -07:00
moloch-- 52f5517c2a fix(tests): make regression assertions portable to Windows 2026-10-08 08:47:37 -07:00
moloch-- c4ed103182 fix(ci): resolve lint findings in defensive fixes 2026-10-08 08:47:37 -07:00
moloch-- c04b0884a5 fix(tests): run discovered docs and protobuf packages 2026-10-08 08:47:37 -07:00
moloch-- 893fe4ccda fix(client): propagate configuration import failures 2026-10-08 08:47:37 -07:00
moloch-- b43ef0fabe fix(client): keep diagnostic log open through command execution 2026-10-08 08:47:37 -07:00
moloch-- 25d2bffb5a fix(update): select Windows executable release assets 2026-10-08 08:47:37 -07:00
moloch-- 534763f087 fix(websites): persist zero-byte content replacements 2026-10-08 08:47:37 -07:00
moloch-- ec733974de fix(websites): target MIME updates by existing content path 2026-10-08 08:47:37 -07:00
moloch-- d9811baa7e fix(update): preserve prior download until verified replacement 2026-10-08 08:47:37 -07:00
moloch-- d8b360ecea fix(config): preserve database connection values in driver DSNs 2026-10-08 08:47:37 -07:00
moloch-- 12c55be611 fix(config): propagate failed saves and atomically replace files 2026-10-08 08:47:37 -07:00
moloch-- 3a147f1e01 fix(logging): redact administrative secrets from audit and RPC logs 2026-10-08 08:47:36 -07:00
moloch-- 5b03320a32 fix(auth): complete operator revocation after cleanup errors 2026-10-06 21:47:06 -07:00
moloch-- 1cc6d35dc6 fix(websites): restrict recursive removal to selected path 2026-10-06 21:34:23 -07:00
moloch-- 1c5d8ab5a9 Add Sliver GUI download to docs landing page 2026-10-05 17:50:28 -07:00
moloch-- 04a71e6d7d Merge branch 'feat/docs-npm-updates' 2026-10-03 21:07:03 -07:00
moloch-- bb8b90be56 Update docs 2026-10-03 21:06:48 -07:00
Joe 7a2b58ade8 Merge pull request #2391 from BishopFox/feat/docs-npm-updates
build(docs): update npm dependencies
2026-10-03 13:45:51 -07:00
moloch-- 854143b593 build(docs): update npm dependencies 2026-10-03 13:32:35 -07:00
Joe d3068394bf Merge pull request #2390 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/next-16.3.6
build(deps): bump next from 16.3.3 to 16.3.6 in /docs/sliver-docs
2026-10-03 13:30:04 -07:00
Joe d80db8a932 Merge pull request #2388 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/dompurify-3.4.16
build(deps): bump dompurify from 3.4.13 to 3.4.16 in /docs/sliver-docs
2026-10-03 13:29:56 -07:00
Joe 179ff728dd Merge pull request #2389 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/multi-05c505ab13
build(deps): bump brace-expansion in /docs/sliver-docs
2026-10-03 13:29:45 -07:00
moloch-- 422044af0d fix(ci): preserve pinned CNA fixture bytes on Windows 2026-10-03 10:17:29 -07:00
dependabot[bot] 7dbdd1931c build(deps): bump next from 16.3.3 to 16.3.6 in /docs/sliver-docs
Bumps [next](https://github.com/vercel/next.js) from 16.3.3 to 16.3.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.3...v16.3.6)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.3.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-03 00:56:48 +00:00
dependabot[bot] 8b7e1b63ab build(deps): bump brace-expansion in /docs/sliver-docs
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 1.1.16 to 1.1.21
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.16...v1.1.21)

Updates `brace-expansion` from 5.0.8 to 5.0.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.16...v1.1.21)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.21
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 5.0.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-03 00:55:54 +00:00
dependabot[bot] ed5c89253c build(deps): bump dompurify from 3.4.13 to 3.4.16 in /docs/sliver-docs
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.13 to 3.4.16.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.13...3.4.16)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.16
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-03 00:55:48 +00:00
Joe 06b5a56e74 Merge pull request #2387 from BishopFox/feat/console-multiline-piped-commands
fix(console): run pasted and piped commands in sequence
2026-10-02 17:53:01 -07:00
moloch-- 704912d25a fix(console): resolve CLI lint findings 2026-10-01 20:52:56 -07:00
moloch-- ea1fe95408 fix(console): run pasted and piped commands in sequence 2026-10-01 20:28:48 -07:00
moloch-- 8fe54f243e Tweak readme 2026-09-26 11:19:40 -07:00
moloch-- 4fa6fe86fe Fix doc names 2026-09-26 11:08:55 -07:00
moloch-- 23c5a0b164 Update docs with details on cross-platform bof/coff support 2026-09-26 11:04:41 -07:00
Joe 7b8dafcf55 Merge pull request #2380 from MKMithun2806/fix/client-console-non-tty-oom
fix(client): refuse interactive console on non-TTY stdin
2026-09-20 14:14:35 -07:00
moloch-- 4e6531fa98 test: wait for tunnel deadline cleanup 2026-09-20 11:10:07 -07:00
moloch-- 6e9b2a9820 fix(client): skip interactive loop after non-TTY rc 2026-09-20 10:48:23 -07:00
MKMithun2806 b11f85d4d7 Merge remote-tracking branch 'upstream/master' into fix/client-console-non-tty-oom
# Conflicts:
#	client/console/console.go
2026-09-20 10:17:07 +00:00
MKMithun2806 23f1341684 test(client): add unit tests for non-TTY interactive console guard
Exercises the nonTTYGuard helper introduced in the previous commit
to verify:
- error returned when stdin is not a TTY and no --rc is provided
- nil returned when --rc content is supplied (rc already ran)
- nil returned when stdin is a TTY (normal interactive path)
2026-09-20 09:49:28 +00:00
MKMithun2806 e68e163456 fix(client): refuse interactive console on non-TTY stdin (#2379)
When stdin is not a terminal, the interactive console run-loop
spins with ENOTTY from readline, rebuilding the full command tree
every iteration and consuming memory unboundedly (300-400 MB/s RSS).

Fail closed before entering the loop:
- Non-TTY without --rc: return a clear error.
- Non-TTY with --rc: rc script already ran, return nil without
  calling App.Start() so --rc remains usable for automation.

Uses golang.org/x/term (already vendored) for the TTY check.
No vendor, server, or command-tree changes.
2026-09-20 09:49:02 +00:00
Joe f8430cecf7 Merge pull request #2378 from BishopFox/feat/rportfwd-first-frame-master
fix: preserve reverse port forward first frames
2026-09-19 21:27:44 -07:00
moloch-- 276a30fbaa test: normalize Windows corpus line endings 2026-09-19 19:54:18 -07:00
moloch-- 3aa8a9f93c fix: satisfy reverse precreate lint checks 2026-09-19 19:35:41 -07:00
moloch-- fc3fb7386a fix: preserve reverse port forward first frames
(cherry picked from commit a89c03bb87)
2026-09-19 19:26:43 -07:00
moloch-- 6edea618e9 Remove .gitattributes 2026-09-19 18:33:37 -07:00
moloch-- e920f2f8ea Add RegistryType to read resp 2026-09-19 18:24:15 -07:00
Joe 5b4a03c9c3 Merge pull request #2375 from BishopFox/feat/dependabot-consolidated-2026-09-15
build(deps): consolidate nine Dependabot updates
2026-09-15 15:36:28 -07:00
moloch-- e93fabecd6 build(deps): consolidate open Dependabot updates 2026-09-15 14:35:45 -07:00
Joe b2cae511e6 Merge pull request #2372 from jimmexploit/add-service-name-flag
Add --service-name flag to generate for Windows service implants
2026-09-14 07:34:13 -07:00
Joe 2e8e53789f Merge pull request #2374 from BishopFox/crackstation/top2
Redesign crack top as a real-time dashboard
2026-09-12 17:38:53 -07:00
moloch-- 6a08fb2f7c Fix crack top lint findings 2026-09-12 17:11:48 -07:00
moloch-- 76d38d4677 Improve 'crack top' view 2026-09-12 16:53:33 -07:00
moloch-- 3f87964397 Redesign crack top 2026-09-12 15:39:43 -07:00
moloch-- bb19ba0665 Implement crack cancel/pause/resume 2026-09-12 12:37:01 -07:00
Joe 262851373d Merge pull request #2373 from BishopFox/crackstation/top
Add crack top dashboard and benchmark cache policy
2026-09-12 10:31:15 -07:00
Ubuntu 40df2701d1 Add --service-name flag for Windows service implants 2026-09-12 05:12:33 +00:00
1734 changed files with 117635 additions and 63078 deletions

No files matched your search

-4
View File
@@ -1,4 +0,0 @@
# Preserve pinned upstream source and attribution files byte-for-byte.
client/command/opfor/testdata/corpus/** -text
client/command/opfor/testdata/corpus/operatorskit/*.cna whitespace=-trailing-space,-blank-at-eof
vendor/github.com/sliverarmory/opfor/internal/regexp2/ATTRIB whitespace=-trailing-space
+5 -5
View File
@@ -17,7 +17,7 @@ jobs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -56,14 +56,14 @@ jobs:
timeout-minutes: 45
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
go-version: "1.27.3"
- name: Install Minisign
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
+5 -5
View File
@@ -16,13 +16,13 @@ jobs:
steps:
# Setup go environment
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
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
+12 -12
View File
@@ -73,12 +73,12 @@ jobs:
timeout-minutes: 30
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
@@ -118,7 +118,7 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
@@ -128,7 +128,7 @@ jobs:
printf 'darwin/%s\n' '${{ matrix.goarch }}' > e2e-results/target-darwin-${{ matrix.goarch }}.txt
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
@@ -210,7 +210,7 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
@@ -220,7 +220,7 @@ jobs:
printf 'linux/%s\n' '${{ matrix.goarch }}' > e2e-results/target-linux-${{ matrix.goarch }}.txt
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
@@ -302,7 +302,7 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
@@ -312,7 +312,7 @@ jobs:
printf 'windows/%s\n' '${{ matrix.goarch }}' > e2e-results/target-windows-${{ matrix.goarch }}.txt
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
@@ -390,12 +390,12 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
@@ -465,12 +465,12 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
+6 -6
View File
@@ -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
+5 -5
View File
@@ -39,18 +39,18 @@ jobs:
CGO_ENABLED: "0"
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
go-version: "1.27.3"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.13.1
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/...
+6 -6
View File
@@ -26,10 +26,10 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
@@ -78,10 +78,10 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
@@ -130,10 +130,10 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
+4 -4
View File
@@ -52,7 +52,7 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
@@ -65,7 +65,7 @@ jobs:
> 'shellcode-results/target-${{ matrix.target_os }}-${{ matrix.target_arch }}.txt'
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
@@ -131,12 +131,12 @@ jobs:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
+16 -16
View File
@@ -27,10 +27,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
go-version: "1.27.3"
- name: OS Packages
run: |
@@ -38,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
@@ -73,16 +73,16 @@ jobs:
runs-on: windows-latest
timeout-minutes: 180
steps:
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
go-version: "1.27.3"
- name: Install make
run: choco install make -y --no-progress
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
@@ -117,13 +117,13 @@ jobs:
runs-on: macos-latest
timeout-minutes: 180
steps:
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
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
@@ -155,13 +155,13 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
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
View File
@@ -9,6 +9,7 @@ run:
- osusergo
- netgo
- go_sqlite
- sliver_generate
# Raw implant entry points are Go templates; lint their supporting packages.
- sliver_lint
+2 -2
View File
@@ -199,7 +199,7 @@ clean-all: clean
clean:
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
.downloaded_assets:
.downloaded_assets: util/assets/constants.go util/assets/garble.go
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
touch ./.downloaded_assets
@@ -324,7 +324,7 @@ clean-all: clean
clean:
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
.downloaded_assets:
.downloaded_assets: util/assets/constants.go util/assets/garble.go
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
@type NUL > .downloaded_assets
+6 -19
View File
@@ -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.
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.
[![Release](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [![golangci-lint](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](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.
+30 -3
View File
@@ -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,11 @@ 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)
@@ -96,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))
}
}
+23
View File
@@ -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
View File
@@ -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
}
+46
View File
@@ -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)
}
})
}
}
}
+53
View File
@@ -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)
}
}
+25 -17
View File
@@ -22,15 +22,18 @@ 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{
@@ -49,20 +52,22 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
return err
}
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
}
rcScript, err := ReadRCScript(cmd)
if err != nil {
fmt.Printf("Failed to read rc script: %s\n", err)
return nil
return err
}
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)
@@ -70,7 +75,7 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
var ln *grpc.ClientConn
// Don't clobber output when simply running an implant command from system shell.
if run {
if run && stdinTTY {
rpc, ln, err = connectWithSpinner(os.Stdout, target, func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
return transport.MTLSConnectWithStatus(config, statusFn)
})
@@ -78,10 +83,13 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
rpc, ln, err = transport.MTLSConnect(config)
}
if err != nil {
fmt.Printf("Connection to server failed %s\n", err)
return nil
return fmt.Errorf("connection to server failed: %w", err)
}
return console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
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
+3 -1
View File
@@ -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
View File
@@ -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
}
+125
View File
@@ -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)
}
}
+14 -2
View File
@@ -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,7 +40,6 @@ func mcpCmd(con *console.SliverClient) *cobra.Command {
return runMCP(cmd, con)
},
}
mcpCmd.Flags().String("config", "", "path to client config file")
return mcpCmd
}
@@ -54,6 +55,14 @@ 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)
@@ -62,7 +71,10 @@ func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
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 {
+4 -18
View File
@@ -156,13 +156,6 @@ func TestMessageBlockLabelIgnoresUnknownOperatorPlaceholders(t *testing.T) {
}
}
func TestTranscriptSpeakerStyleUsesPrimaryThemeForUserMessages(t *testing.T) {
styles := transcriptSpeakerStyle("alice", "user")
assertColorEqual(t, styles.border.GetForeground(), clienttheme.Primary())
assertColorEqual(t, styles.label.GetBackground(), clienttheme.Primary())
}
func TestThinkingLevelOptionFocusedStyleUsesDarkerPrimaryShade(t *testing.T) {
model := newAIModel(nil, aiContext{}, nil)
@@ -544,19 +537,12 @@ func TestRenderHeaderUsesSingleCompactRow(t *testing.T) {
}
}
func TestTranscriptSpeakerPaletteVariesAcrossUsers(t *testing.T) {
seen := map[int]struct{}{}
for _, label := range []string{"alice", "bob", "charlie", "dana", "erin", "frank"} {
seen[transcriptSpeakerPaletteIndex(label, "user")] = struct{}{}
}
if len(seen) < 2 {
t.Fatalf("expected distinct users to map to more than one transcript color, got %d palette entries", len(seen))
}
}
func TestTranscriptSpeakerStyleUsesPrimaryBorderForUserMessages(t *testing.T) {
userStyles := transcriptSpeakerStyle("alice", "user")
userBorder := transcriptSpeakerStyle("alice", "user").border.Render("│")
assertColorEqual(t, userStyles.border.GetForeground(), clienttheme.Primary())
assertColorEqual(t, userStyles.label.GetBackground(), clienttheme.Primary())
userBorder := userStyles.border.Render("│")
wantUserBorder := lipgloss.NewStyle().Foreground(clienttheme.Primary()).Render("│")
if userBorder != wantUserBorder {
t.Fatalf("expected user message border to use theme primary color")
-1
View File
@@ -1 +0,0 @@
package armory
+4 -4
View File
@@ -9,9 +9,9 @@ Implements the 'crack' command group for the Sliver client console. Handlers map
- `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 and renders detailed task progress, device telemetry, and recovered credentials.
- `crack-top.go` – Runs the full-screen Bubble Tea monitor, including responsive job/worker panes and a Huh-backed status filter.
- `crack-top-loader.go` – Loads one lean, target-free snapshot containing every active job, bounded recent history, and connected crackstation telemetry.
- `crack-top-telemetry.go` – Calculates weighted shard progress plus per-worker and global live hash rates without treating cached benchmarks as current throughput.
- `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.
+50
View File
@@ -99,11 +99,61 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
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{
@@ -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)
}
}
+249
View File
@@ -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)
}
}
+420 -2
View File
@@ -46,6 +46,20 @@ import (
)
type crackJobFetcher func(context.Context, string) (*clientpb.CrackJob, error)
type crackJobDeleteFunc func(context.Context, *clientpb.CrackJob) (*commonpb.Empty, error)
type crackJobConfirmFunc func(string) (bool, error)
type crackJobMutationFunc func(context.Context, *clientpb.CrackJob) (*clientpb.CrackJob, error)
type crackJobLifecycleAction struct {
name string
pastTense string
noEligibleJobsMessage string
eligible func(*clientpb.CrackJob) bool
eligibilityDescription string
expectedStatus clientpb.CrackJobStatus
confirmationPrompt func(*clientpb.CrackJob) string
mutate crackJobMutationFunc
}
var errNoCrackJobs = errors.New("no crack jobs")
@@ -128,6 +142,407 @@ func CrackJobCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
}
}
// CrackJobCancelCmd permanently stops an active or paused cracking job.
//
//nolint:revive // Keep the established exported command-handler name for compatibility.
func CrackJobCancelCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
runCrackJobLifecycleCmd(cmd, con, args, newCrackJobCancelAction(
func(parent context.Context, job *clientpb.CrackJob) (*clientpb.CrackJob, error) {
ctx, cancel := crackCommandContext(parent, cmd)
defer cancel()
return con.Rpc.CrackJobCancel(ctx, job)
},
))
}
// CrackJobPauseCmd pauses an active cracking job, allowing its crackstations to
// accept other work.
//
//nolint:revive // Keep the established exported command-handler name for compatibility.
func CrackJobPauseCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
runCrackJobLifecycleCmd(cmd, con, args, newCrackJobPauseAction(
func(parent context.Context, job *clientpb.CrackJob) (*clientpb.CrackJob, error) {
ctx, cancel := crackCommandContext(parent, cmd)
defer cancel()
return con.Rpc.CrackJobPause(ctx, job)
},
))
}
// CrackJobResumeCmd makes a paused cracking job eligible for scheduling again.
//
//nolint:revive // Keep the established exported command-handler name for compatibility.
func CrackJobResumeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
runCrackJobLifecycleCmd(cmd, con, args, newCrackJobResumeAction(
func(parent context.Context, job *clientpb.CrackJob) (*clientpb.CrackJob, error) {
ctx, cancel := crackCommandContext(parent, cmd)
defer cancel()
return con.Rpc.CrackJobResume(ctx, job)
},
))
}
func newCrackJobCancelAction(mutate crackJobMutationFunc) crackJobLifecycleAction {
return crackJobLifecycleAction{
name: "cancel",
pastTense: "cancelled",
noEligibleJobsMessage: "No cancellable crack jobs",
eligible: crackJobCancellable,
eligibilityDescription: "in-progress or paused jobs",
expectedStatus: clientpb.CrackJobStatus_CANCELLED,
confirmationPrompt: crackJobCancelPrompt,
mutate: mutate,
}
}
func newCrackJobPauseAction(mutate crackJobMutationFunc) crackJobLifecycleAction {
return crackJobLifecycleAction{
name: "pause",
pastTense: "paused",
noEligibleJobsMessage: "No pausable crack jobs",
eligible: crackJobPausable,
eligibilityDescription: "in-progress jobs",
expectedStatus: clientpb.CrackJobStatus_PAUSED,
confirmationPrompt: crackJobPausePrompt,
mutate: mutate,
}
}
func newCrackJobResumeAction(mutate crackJobMutationFunc) crackJobLifecycleAction {
return crackJobLifecycleAction{
name: "resume",
pastTense: "resumed",
noEligibleJobsMessage: "No resumable crack jobs",
eligible: crackJobResumable,
eligibilityDescription: "paused jobs",
expectedStatus: clientpb.CrackJobStatus_IN_PROGRESS,
mutate: mutate,
}
}
func runCrackJobLifecycleCmd(
cmd *cobra.Command,
con *console.SliverClient,
args []string,
action crackJobLifecycleAction,
) {
jobID, err := resolveCrackJobID(args, func() (*clientpb.CrackJobs, error) {
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
defer cancel()
jobs, err := con.Rpc.CrackJobs(ctx, &commonpb.Empty{})
if err != nil || jobs == nil {
return jobs, err
}
return &clientpb.CrackJobs{Jobs: filterCrackJobs(jobs.GetJobs(), action.eligible)}, nil
}, forms.CrackJobSelectForm)
if errors.Is(err, errNoCrackJobs) {
con.PrintInfof("%s\n", action.noEligibleJobsMessage)
return
}
if errors.Is(err, forms.ErrUserAborted) {
return
}
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
fetchCtx, fetchCancel := crackCommandContext(cmd.Context(), cmd)
job, err := con.Rpc.CrackJobByID(fetchCtx, &clientpb.CrackJob{ID: jobID})
fetchCancel()
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
updated, changed, err := applyFetchedCrackJobLifecycleAction(cmd.Context(), jobID, job, action, promptCrackJobConfirmation)
if errors.Is(err, forms.ErrUserAborted) {
return
}
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if changed {
con.PrintInfof("Crack job %s status: %s\n", safeCrackCell(strings.TrimSpace(updated.GetID())), safeCrackCell(updated.GetStatus().String()))
}
}
func applyFetchedCrackJobLifecycleAction(
ctx context.Context,
requestedID string,
job *clientpb.CrackJob,
action crackJobLifecycleAction,
confirm crackJobConfirmFunc,
) (*clientpb.CrackJob, bool, error) {
if err := validateFetchedCrackJobID(requestedID, job); err != nil {
return nil, false, err
}
return applyCrackJobLifecycleAction(ctx, job, action, confirm)
}
func applyCrackJobLifecycleAction(
ctx context.Context,
job *clientpb.CrackJob,
action crackJobLifecycleAction,
confirm crackJobConfirmFunc,
) (*clientpb.CrackJob, bool, error) {
if job == nil {
return nil, false, errors.New("server returned an empty crack job")
}
jobID := strings.TrimSpace(job.GetID())
if jobID == "" {
return nil, false, errors.New("crack job ID cannot be empty")
}
if action.name == "" || action.pastTense == "" || action.eligible == nil || action.eligibilityDescription == "" {
return nil, false, errors.New("crack job lifecycle action is incomplete")
}
if !action.eligible(job) {
status := safeCrackCell(job.GetStatus().String())
if status == "" {
status = "UNKNOWN"
}
return nil, false, fmt.Errorf("crack job %s is %s; only %s can be %s",
safeCrackCell(jobID), status, action.eligibilityDescription, action.pastTense)
}
if action.confirmationPrompt != nil {
if confirm == nil {
return nil, false, fmt.Errorf("crack job %s confirmation is unavailable", action.name)
}
confirmed, err := confirm(action.confirmationPrompt(job))
if err != nil {
return nil, false, err
}
if !confirmed {
return nil, false, nil
}
}
if action.mutate == nil {
return nil, false, fmt.Errorf("crack job %s operation is unavailable", action.name)
}
if ctx == nil {
ctx = context.Background()
}
updated, err := action.mutate(ctx, &clientpb.CrackJob{ID: jobID})
if err != nil {
return nil, false, err
}
if updated == nil {
return nil, false, fmt.Errorf("server returned an empty crack job after %s", action.name)
}
updatedID := strings.TrimSpace(updated.GetID())
if updatedID == "" {
return nil, false, fmt.Errorf("server returned a crack job without an ID after %s", action.name)
}
if updatedID != jobID {
return nil, false, fmt.Errorf("server returned crack job %s after %s request for %s",
safeCrackCell(updatedID), action.name, safeCrackCell(jobID))
}
if updated.GetUpdatedAt() <= 0 {
return nil, false, fmt.Errorf("server returned crack job state without an update timestamp after %s", action.name)
}
if updated.GetStatus() != action.expectedStatus {
return nil, false, fmt.Errorf("server returned unexpected crack job status %s after %s (want %s)",
safeCrackCell(updated.GetStatus().String()), action.name, safeCrackCell(action.expectedStatus.String()))
}
return updated, true, nil
}
func promptCrackJobConfirmation(prompt string) (bool, error) {
confirmed := false
if err := forms.Confirm(prompt, &confirmed); err != nil {
return false, err
}
return confirmed, nil
}
func crackJobCancelPrompt(job *clientpb.CrackJob) string {
return fmt.Sprintf(
"Cancel crack job %q (%s)? Unfinished work will be discarded.",
safeCrackCell(strings.TrimSpace(job.GetID())),
safeCrackCell(job.GetStatus().String()),
)
}
func crackJobPausePrompt(job *clientpb.CrackJob) string {
return fmt.Sprintf(
"Pause crack job %q (%s)? Running attempts may be stopped and restarted from the beginning of their shards when resumed.",
safeCrackCell(strings.TrimSpace(job.GetID())),
safeCrackCell(job.GetStatus().String()),
)
}
// CrackJobRmCmd permanently removes one terminal cracking job and its related
// tasks and results.
//
//nolint:revive // Keep the established exported command-handler name for compatibility.
func CrackJobRmCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
jobID, err := resolveCrackJobID(args, func() (*clientpb.CrackJobs, error) {
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
defer cancel()
jobs, err := con.Rpc.CrackJobs(ctx, &commonpb.Empty{})
if err != nil || jobs == nil {
return jobs, err
}
return &clientpb.CrackJobs{Jobs: terminalCrackJobs(jobs.GetJobs())}, nil
}, forms.CrackJobSelectForm)
if errors.Is(err, errNoCrackJobs) {
con.PrintInfof("No removable crack jobs\n")
return
}
if errors.Is(err, forms.ErrUserAborted) {
return
}
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
fetchCtx, fetchCancel := crackCommandContext(cmd.Context(), cmd)
job, err := con.Rpc.CrackJobByID(fetchCtx, &clientpb.CrackJob{ID: jobID})
fetchCancel()
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
removed, err := removeFetchedCrackJob(cmd.Context(), jobID, job, promptCrackJobRemoval, func(parent context.Context, job *clientpb.CrackJob) (*commonpb.Empty, error) {
deleteCtx, deleteCancel := crackCommandContext(parent, cmd)
defer deleteCancel()
return con.Rpc.CrackJobDelete(deleteCtx, job)
})
if errors.Is(err, forms.ErrUserAborted) {
return
}
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if removed {
con.PrintInfof("Removed crack job %s\n", safeCrackCell(job.GetID()))
}
}
func removeFetchedCrackJob(
ctx context.Context,
requestedID string,
job *clientpb.CrackJob,
confirm crackJobConfirmFunc,
deleteJob crackJobDeleteFunc,
) (bool, error) {
if err := validateFetchedCrackJobID(requestedID, job); err != nil {
return false, err
}
return removeCrackJob(ctx, job, confirm, deleteJob)
}
func validateFetchedCrackJobID(requestedID string, job *clientpb.CrackJob) error {
requestedID = strings.TrimSpace(requestedID)
if requestedID == "" {
return errors.New("requested crack job ID cannot be empty")
}
if job == nil {
return errors.New("server returned an empty crack job")
}
returnedID := strings.TrimSpace(job.GetID())
if returnedID == "" {
return errors.New("server returned a crack job without an ID")
}
if returnedID != requestedID {
return fmt.Errorf("server returned crack job %s for request %s",
safeCrackCell(returnedID), safeCrackCell(requestedID))
}
return nil
}
func promptCrackJobRemoval(prompt string) (bool, error) {
confirmed := false
if err := forms.Confirm(prompt, &confirmed); err != nil {
return false, err
}
return confirmed, nil
}
func removeCrackJob(
ctx context.Context,
job *clientpb.CrackJob,
confirm crackJobConfirmFunc,
deleteJob crackJobDeleteFunc,
) (bool, error) {
if job == nil {
return false, errors.New("server returned an empty crack job")
}
jobID := strings.TrimSpace(job.GetID())
if jobID == "" {
return false, errors.New("crack job ID cannot be empty")
}
if !crackJobTerminal(job) {
status := safeCrackCell(job.GetStatus().String())
if status == "" {
status = "UNKNOWN"
}
return false, fmt.Errorf("crack job %s is %s; only completed, failed, or cancelled jobs can be removed",
safeCrackCell(jobID), status)
}
if confirm == nil {
return false, errors.New("crack job removal confirmation is unavailable")
}
confirmed, err := confirm(crackJobRemovalPrompt(job))
if err != nil {
return false, err
}
if !confirmed {
return false, nil
}
if deleteJob == nil {
return false, errors.New("crack job remover is unavailable")
}
if ctx == nil {
ctx = context.Background()
}
_, err = deleteJob(ctx, &clientpb.CrackJob{ID: jobID})
if err != nil {
return false, err
}
return true, nil
}
func crackJobRemovalPrompt(job *clientpb.CrackJob) string {
if job == nil {
return "Permanently delete this crack job?"
}
taskCount := len(job.GetTasks())
resultCount := job.GetResultCount()
return fmt.Sprintf(
"Permanently delete crack job %q (%s), including %d %s and %d %s?",
safeCrackCell(strings.TrimSpace(job.GetID())),
safeCrackCell(job.GetStatus().String()),
taskCount,
pluralCrackJobRemovalItem(uint64(taskCount), "task"),
resultCount,
pluralCrackJobRemovalItem(resultCount, "result"),
)
}
func pluralCrackJobRemovalItem(count uint64, singular string) string {
if count == 1 {
return singular
}
return singular + "s"
}
func terminalCrackJobs(jobs []*clientpb.CrackJob) []*clientpb.CrackJob {
return filterCrackJobs(jobs, crackJobTerminal)
}
func filterCrackJobs(jobs []*clientpb.CrackJob, include func(*clientpb.CrackJob) bool) []*clientpb.CrackJob {
filtered := make([]*clientpb.CrackJob, 0, len(jobs))
for _, job := range jobs {
if job != nil && include != nil && include(job) {
filtered = append(filtered, job)
}
}
return filtered
}
func resolveCrackJobID(
args []string,
listJobs func() (*clientpb.CrackJobs, error),
@@ -181,7 +596,10 @@ func crackCommandContext(parent context.Context, cmd *cobra.Command) (context.Co
if parent == nil {
parent = context.Background()
}
timeoutSeconds, _ := cmd.Flags().GetInt64("timeout")
timeoutSeconds, err := cmd.Flags().GetInt64("timeout")
if err != nil {
timeoutSeconds, _ = cmd.InheritedFlags().GetInt64("timeout")
}
if timeoutSeconds <= 0 {
return parent, func() {}
}
@@ -246,7 +664,7 @@ func crackJobStatusStyle(status string) console.TextStyle {
return console.StyleBoldSuccess
case strings.HasSuffix(status, "FAILED"):
return console.StyleBoldDanger
case strings.HasSuffix(status, "CANCELLED"), strings.HasSuffix(status, "CANCELED"):
case strings.HasSuffix(status, "CANCELLED"), strings.HasSuffix(status, "CANCELED"), strings.HasSuffix(status, "PAUSED"):
return console.StyleBoldGray
case status == "IN_PROGRESS", strings.HasSuffix(status, "RUNNING"), strings.HasSuffix(status, "LEASED"):
return console.StyleBoldWarning
+1
View File
@@ -321,6 +321,7 @@ func TestCrackJobStatusStyleUsesSemanticPalette(t *testing.T) {
{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},
}
+3 -411
View File
@@ -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)
}
+35 -3
View File
@@ -22,8 +22,28 @@ type crackJobCompletionEntry struct {
//
//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 := crackJobCompletionEntries(con)
entries, err := crackJobCompletionEntriesFiltered(con, include)
if err != nil {
return carapace.ActionMessage("failed to fetch crack jobs: %s", err.Error())
}
@@ -37,6 +57,14 @@ func CrackJobIDCompleter(con *console.SliverClient) carapace.Action {
}
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
}
@@ -46,7 +74,7 @@ func registerCrackJobIDCompletion(cmd *cobra.Command, con *console.SliverClient)
return nil, cobra.ShellCompDirectiveNoFileComp
}
entries, err := crackJobCompletionEntries(con)
entries, err := crackJobCompletionEntriesFiltered(con, include)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
@@ -61,6 +89,10 @@ func registerCrackJobIDCompletion(cmd *cobra.Command, con *console.SliverClient)
}
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
}
@@ -74,7 +106,7 @@ func crackJobCompletionEntries(con *console.SliverClient) ([]crackJobCompletionE
entries := make([]crackJobCompletionEntry, 0, len(jobs.GetJobs()))
for _, job := range jobs.GetJobs() {
if job == nil || strings.TrimSpace(job.GetID()) == "" {
if job == nil || strings.TrimSpace(job.GetID()) == "" || (include != nil && !include(job)) {
continue
}
entries = append(entries, crackJobCompletionEntry{
@@ -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])
}
+16
View File
@@ -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)
}
+518
View File
@@ -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)
}
}
+78
View File
@@ -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)
}
+99
View File
@@ -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)
}
}
@@ -1,4 +1,4 @@
package crack
package top
import (
"context"
@@ -1,4 +1,4 @@
package crack
package top
import (
"context"
File diff suppressed because it is too large. Load diff
+598
View File
@@ -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)
}
+123
View File
@@ -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)
}
})
}
}
+172
View File
@@ -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
}
@@ -1,4 +1,4 @@
package crack
package top
import (
"errors"
@@ -20,6 +20,9 @@ type crackTopDashboard struct {
Jobs []crackTopJobRow
Workers []crackTopWorkerRow
ActiveJobs int
QueuedTasks int
LeasedTasks int
RunningTasks int
OnlineWorkers int
Unavailable int
Recovered uint64
@@ -161,7 +164,23 @@ func buildCrackTopDashboard(snapshot *crackTopSnapshot) crackTopDashboard {
row, samples, progress := crackTopJobTelemetry(job, stations, snapshot.RefreshedAt, snapshot.taskTelemetry)
dashboard.Jobs = append(dashboard.Jobs, row)
dashboard.Recovered = saturatingUint64Add(dashboard.Recovered, row.Results)
if !crackJobTerminal(job) {
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++
@@ -358,11 +377,13 @@ func crackTopJobTelemetry(
row.ProgressKnown = true
row.ProgressComplete = true
}
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++
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
@@ -1,4 +1,5 @@
package crack
// Package top implements the real-time Crack Top terminal dashboard.
package top
import (
"context"
@@ -35,6 +36,8 @@ const (
crackTopWideWidth = 104
crackTopHeaderHeight = 5
crackTopFooterHeight = 1
crackTopOverviewWidth = crackTopDefaultWidth
crackTopOverviewHeight = 30
crackTopMinimumInterval = 250 * time.Millisecond
crackTopEventDebounce = 500 * time.Millisecond
crackTopToastDuration = 5 * time.Second
@@ -49,6 +52,21 @@ var (
)
type crackTopFetchFunc func(context.Context) (*crackTopSnapshot, error)
type crackTopJobActionFunc func(context.Context, string) (*clientpb.CrackJob, error)
type crackTopJobAction string
const (
crackTopJobActionDelete crackTopJobAction = "delete"
crackTopJobActionPause crackTopJobAction = "pause"
crackTopJobActionResume crackTopJobAction = "resume"
crackTopJobActionCancel crackTopJobAction = "cancel"
)
type crackTopPendingJobAction struct {
action crackTopJobAction
jobID string
}
type crackTopSnapshotMsg struct {
snapshot *crackTopSnapshot
@@ -75,6 +93,21 @@ type crackTopToastExpiredMsg struct {
generation uint64
}
type crackTopJobActionCompletedMsg struct {
action crackTopJobAction
jobID string
status clientpb.CrackJobStatus
updatedAt int64
hasStatus bool
err error
}
type crackTopJobStatusOverride struct {
status clientpb.CrackJobStatus
updatedAt int64
equalVersionDisagreementSeen bool
}
type crackTopWindowPollMsg struct {
width int
height int
@@ -93,6 +126,7 @@ const (
const (
crackTopFilterAll crackTopFilter = "all"
crackTopFilterActive crackTopFilter = "active"
crackTopFilterPaused crackTopFilter = "paused"
crackTopFilterCompleted crackTopFilter = "completed"
crackTopFilterFailed crackTopFilter = "failed"
)
@@ -109,6 +143,7 @@ type crackTopStyles struct {
danger lipgloss.Style
selected lipgloss.Style
filterCard lipgloss.Style
actionCard lipgloss.Style
}
type crackTopModel struct {
@@ -128,6 +163,8 @@ type crackTopModel struct {
eventCancel context.CancelFunc
snapshot *crackTopSnapshot
dashboard crackTopDashboard
history []crackTopHistorySample
journal []crackTopJournalEntry
lastError string
eventStreamClosed bool
selectedJobID string
@@ -138,6 +175,11 @@ type crackTopModel struct {
filter crackTopFilter
filterDraft crackTopFilter
filterForm *huh.Form
jobActions map[crackTopJobAction]crackTopJobActionFunc
pendingJobAction crackTopPendingJobAction
runningJobAction crackTopPendingJobAction
deletedJobIDs map[string]struct{}
jobStatusOverrides map[string]crackTopJobStatusOverride
toast string
toastLevel string
toastGeneration uint64
@@ -162,7 +204,10 @@ func CrackTopCmd(cmd *cobra.Command, con *console.SliverClient, _ []string) {
return
}
timeoutSeconds, _ := cmd.Flags().GetInt64("timeout")
timeoutSeconds, timeoutErr := cmd.Flags().GetInt64("timeout")
if timeoutErr != nil {
timeoutSeconds, _ = cmd.InheritedFlags().GetInt64("timeout")
}
perCallTimeout := time.Duration(0)
if timeoutSeconds > 0 {
perCallTimeout = time.Duration(timeoutSeconds) * time.Second
@@ -179,6 +224,29 @@ func CrackTopCmd(cmd *cobra.Command, con *console.SliverClient, _ []string) {
return loadCrackTopSnapshot(fetchCtx, con.Rpc, perCallTimeout, time.Time{})
}
model := newCrackTopModel(ctx, fetch, listener, pollInterval)
model.jobActions = map[crackTopJobAction]crackTopJobActionFunc{
crackTopJobActionDelete: func(actionCtx context.Context, jobID string) (*clientpb.CrackJob, error) {
actionCtx, actionCancel := crackTopCallContext(actionCtx, perCallTimeout)
defer actionCancel()
_, err := con.Rpc.CrackJobDelete(actionCtx, &clientpb.CrackJob{ID: jobID})
return nil, err
},
crackTopJobActionPause: func(actionCtx context.Context, jobID string) (*clientpb.CrackJob, error) {
actionCtx, actionCancel := crackTopCallContext(actionCtx, perCallTimeout)
defer actionCancel()
return con.Rpc.CrackJobPause(actionCtx, &clientpb.CrackJob{ID: jobID})
},
crackTopJobActionResume: func(actionCtx context.Context, jobID string) (*clientpb.CrackJob, error) {
actionCtx, actionCancel := crackTopCallContext(actionCtx, perCallTimeout)
defer actionCancel()
return con.Rpc.CrackJobResume(actionCtx, &clientpb.CrackJob{ID: jobID})
},
crackTopJobActionCancel: func(actionCtx context.Context, jobID string) (*clientpb.CrackJob, error) {
actionCtx, actionCancel := crackTopCallContext(actionCtx, perCallTimeout)
defer actionCancel()
return con.Rpc.CrackJobCancel(actionCtx, &clientpb.CrackJob{ID: jobID})
},
}
width, height := crackTopTerminalSize()
options := []tea.ProgramOption{
tea.WithContext(ctx),
@@ -199,6 +267,13 @@ func CrackTopCmd(cmd *cobra.Command, con *console.SliverClient, _ []string) {
}
}
func crackTopCallContext(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
if timeout > 0 {
return context.WithTimeout(ctx, timeout)
}
return context.WithCancel(ctx)
}
func newCrackTopModel(ctx context.Context, fetch crackTopFetchFunc, listener <-chan *clientpb.Event, pollInterval time.Duration) *crackTopModel {
if ctx == nil {
ctx = context.Background()
@@ -240,6 +315,9 @@ func newCrackTopStyles() crackTopStyles {
Border(lipgloss.RoundedBorder()).
BorderForeground(clienttheme.Primary()).
Padding(1, 2),
actionCard: lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
Padding(1, 2),
}
}
@@ -260,6 +338,12 @@ func (m *crackTopModel) Update(message tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.width = max(1, msg.Width)
m.height = max(1, msg.Height)
if (m.width < crackTopMinWidth || m.height < crackTopMinHeight) && m.pendingJobAction.jobID != "" {
// The compact-size warning replaces the entire dashboard, including
// confirmation modals. Never leave a hidden destructive prompt
// actionable while the operator cannot see it.
m.pendingJobAction = crackTopPendingJobAction{}
}
if m.filterForm != nil {
m.resizeFilterForm()
updated, command := m.filterForm.Update(msg)
@@ -282,16 +366,33 @@ func (m *crackTopModel) Update(message tea.Msg) (tea.Model, tea.Cmd) {
case crackTopSnapshotMsg:
m.refreshing = false
previousError := m.lastError
hadSnapshot := m.snapshot != nil
if msg.snapshot != nil {
previousDashboard := m.dashboard
m.suppressDeletedJobs(msg.snapshot)
m.applyJobStatusOverrides(msg.snapshot)
m.snapshot = mergeCrackTopSnapshot(m.snapshot, msg.snapshot)
m.dashboard = buildCrackTopDashboard(m.snapshot)
m.recordDashboardObservation(previousDashboard, hadSnapshot, m.snapshot.RefreshedAt)
m.normalizeJobSelection()
m.normalizeWorkerSelection()
}
if msg.err != nil {
m.lastError = crackTopWarningText(msg.err.Error())
if msg.snapshot == nil {
m.recordRefreshFailure(time.Now(), previousError == "")
}
} else {
m.lastError = ""
if previousError != "" && msg.snapshot != nil && hadSnapshot {
m.appendJournal(crackTopJournalEntry{
at: m.snapshot.RefreshedAt,
level: "success",
label: "LIVE",
message: "snapshot refresh restored",
})
}
}
if m.refreshQueued {
m.refreshQueued = false
@@ -333,18 +434,30 @@ func (m *crackTopModel) Update(message tea.Msg) (tea.Model, tea.Cmd) {
return m, m.startRefresh()
case crackTopToastMsg:
m.toast = crackTopWarningText(msg.message)
m.toastLevel = strings.ToLower(strings.TrimSpace(msg.level))
m.toastGeneration++
generation := m.toastGeneration
if m.toastCancel != nil {
m.toastCancel()
}
toastCtx, cancel := context.WithCancel(m.ctx)
m.toastCancel = cancel
return m, tea.Batch(
waitForCrackTopEventCmd(m.listener),
crackTopTimerCmd(toastCtx, crackTopToastDuration, crackTopToastExpiredMsg{generation: generation}),
m.showToast(msg.level, msg.message),
)
case crackTopJobActionCompletedMsg:
if msg.action != m.runningJobAction.action || msg.jobID != m.runningJobAction.jobID {
return m, nil
}
m.runningJobAction = crackTopPendingJobAction{}
if msg.err != nil {
return m, m.showToast("error", fmt.Sprintf("Failed to %s crack job %s: %s", msg.action, crackTopShortID(msg.jobID), msg.err))
}
if msg.action == crackTopJobActionDelete {
m.removeJob(msg.jobID)
} else if msg.hasStatus {
m.overrideJobStatus(msg.jobID, msg.status, msg.updatedAt)
} else {
return m, m.showToast("error", fmt.Sprintf("Failed to %s crack job %s: server returned no job state", msg.action, crackTopShortID(msg.jobID)))
}
m.recordJobAction(msg)
return m, tea.Batch(
m.showToast("success", fmt.Sprintf("%s crack job %s", crackTopJobActionPastTense(msg.action), crackTopShortID(msg.jobID))),
m.requestRefresh(),
)
case crackTopToastExpiredMsg:
@@ -375,6 +488,15 @@ func (m *crackTopModel) Update(message tea.Msg) (tea.Model, tea.Cmd) {
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
if m.pendingJobAction.jobID != "" {
switch msg.String() {
case "y", "Y":
return m, m.confirmJobAction()
case "n", "N", "esc":
m.pendingJobAction = crackTopPendingJobAction{}
}
return m, nil
}
if m.filterForm != nil && msg.String() == "esc" {
m.filterForm = nil
return m, nil
@@ -424,6 +546,14 @@ func (m *crackTopModel) Update(message tea.Msg) (tea.Model, tea.Cmd) {
}
case "f":
return m, m.openFilter()
case "d", "delete":
return m, m.openJobAction(crackTopJobActionDelete, true)
case "p":
return m, m.openJobAction(crackTopJobActionPause, true)
case "u":
return m, m.openJobAction(crackTopJobActionResume, false)
case "c":
return m, m.openJobAction(crackTopJobActionCancel, true)
case "r":
return m, m.requestRefresh()
}
@@ -439,6 +569,15 @@ func (m *crackTopModel) View() tea.View {
content := lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, message)
return crackTopView(content)
}
if width >= crackTopOverviewWidth && height >= crackTopOverviewHeight {
content := m.renderOverview(width, height)
if m.filterForm != nil {
content = m.renderFilterModal(content, width, height)
} else if m.pendingJobAction.jobID != "" {
content = m.renderJobActionModal(content, width, height)
}
return crackTopView(content)
}
header := m.renderHeader(width)
bodyHeight := height - crackTopHeaderHeight - crackTopFooterHeight
var body string
@@ -469,6 +608,8 @@ func (m *crackTopModel) View() tea.View {
content := lipgloss.JoinVertical(lipgloss.Left, header, body, footer)
if m.filterForm != nil {
content = m.renderFilterModal(content, width, height)
} else if m.pendingJobAction.jobID != "" {
content = m.renderJobActionModal(content, width, height)
}
return crackTopView(content)
}
@@ -541,6 +682,308 @@ func (m *crackTopModel) scheduleEventRefresh() tea.Cmd {
return crackTopTimerCmd(timerCtx, delay, crackTopEventRefreshMsg{generation: generation})
}
func (m *crackTopModel) showToast(level, message string) tea.Cmd {
m.toast = crackTopWarningText(message)
m.toastLevel = strings.ToLower(strings.TrimSpace(level))
m.toastGeneration++
generation := m.toastGeneration
if m.toastCancel != nil {
m.toastCancel()
}
toastCtx, cancel := context.WithCancel(m.ctx)
m.toastCancel = cancel
return crackTopTimerCmd(toastCtx, crackTopToastDuration, crackTopToastExpiredMsg{generation: generation})
}
func (m *crackTopModel) openJobAction(action crackTopJobAction, confirm bool) tea.Cmd {
if m.width < crackTopMinWidth || m.height < crackTopMinHeight {
// View renders only the compact-size warning at these dimensions, so a
// confirmation could not be shown safely.
return nil
}
if m.runningJobAction.jobID != "" {
return m.showToast("warning", fmt.Sprintf(
"Cannot %s another crack job while a %s operation is in progress for job %s",
action,
m.runningJobAction.action,
crackTopShortID(m.runningJobAction.jobID),
))
}
if m.focus != crackTopFocusJobs {
return m.showToast("warning", fmt.Sprintf("Select the jobs pane before trying to %s a crack job", action))
}
job, ok := m.selectedJob()
if !ok {
return m.showToast("warning", "No crack job is selected")
}
if warning := crackTopJobActionEligibilityWarning(action, job.Status); warning != "" {
return m.showToast("warning", warning)
}
if m.jobActions[action] == nil {
return m.showToast("error", fmt.Sprintf("Crack job %s is unavailable", action))
}
pending := crackTopPendingJobAction{action: action, jobID: job.ID}
if confirm {
m.pendingJobAction = pending
return nil
}
return m.startJobAction(pending)
}
func (m *crackTopModel) selectedJob() (crackTopJobRow, bool) {
for _, job := range m.filteredJobs() {
if job.ID == m.selectedJobID {
return job, true
}
}
return crackTopJobRow{}, false
}
func crackTopJobStatusDeletable(status clientpb.CrackJobStatus) bool {
switch status {
case clientpb.CrackJobStatus_COMPLETED, clientpb.CrackJobStatus_FAILED, clientpb.CrackJobStatus_CANCELLED:
return true
default:
return false
}
}
func crackTopJobActionEligibilityWarning(action crackTopJobAction, status clientpb.CrackJobStatus) string {
switch action {
case crackTopJobActionDelete:
if !crackTopJobStatusDeletable(status) {
return "Only completed, failed, or cancelled crack jobs can be deleted"
}
case crackTopJobActionPause:
if status != clientpb.CrackJobStatus_IN_PROGRESS {
return "Only in-progress crack jobs can be paused"
}
case crackTopJobActionResume:
if status != clientpb.CrackJobStatus_PAUSED {
return "Only paused crack jobs can be resumed"
}
case crackTopJobActionCancel:
if status != clientpb.CrackJobStatus_IN_PROGRESS && status != clientpb.CrackJobStatus_PAUSED {
return "Only in-progress or paused crack jobs can be cancelled"
}
default:
return "Unknown crack job action"
}
return ""
}
func (m *crackTopModel) confirmJobAction() tea.Cmd {
pending := m.pendingJobAction
m.pendingJobAction = crackTopPendingJobAction{}
return m.startJobAction(pending)
}
func (m *crackTopModel) startJobAction(pending crackTopPendingJobAction) tea.Cmd {
actionJob := m.jobActions[pending.action]
if pending.jobID == "" || actionJob == nil {
return nil
}
m.runningJobAction = pending
if m.toastCancel != nil {
m.toastCancel()
m.toastCancel = nil
}
m.toast = ""
m.toastLevel = ""
ctx := m.ctx
return func() tea.Msg {
job, err := actionJob(ctx, pending.jobID)
if err == nil {
err = crackTopValidateJobActionResult(pending, job)
}
message := crackTopJobActionCompletedMsg{action: pending.action, jobID: pending.jobID, err: err}
if err == nil && job != nil {
message.status = job.GetStatus()
message.updatedAt = job.GetUpdatedAt()
message.hasStatus = true
}
return message
}
}
func crackTopValidateJobActionResult(pending crackTopPendingJobAction, job *clientpb.CrackJob) error {
if pending.action == crackTopJobActionDelete {
return nil
}
if job == nil {
return errors.New("server returned no job state")
}
if job.GetID() != pending.jobID {
return fmt.Errorf("server returned state for unexpected job %q", crackTopWarningText(job.GetID()))
}
if job.GetUpdatedAt() <= 0 {
return errors.New("server returned job state without an update timestamp")
}
expected := clientpb.CrackJobStatus_IN_PROGRESS
switch pending.action {
case crackTopJobActionPause:
expected = clientpb.CrackJobStatus_PAUSED
case crackTopJobActionCancel:
expected = clientpb.CrackJobStatus_CANCELLED
case crackTopJobActionResume:
expected = clientpb.CrackJobStatus_IN_PROGRESS
default:
return fmt.Errorf("unknown crack job action %q", pending.action)
}
if job.GetStatus() != expected {
return fmt.Errorf("server returned unexpected job status %s (want %s)", job.GetStatus(), expected)
}
return nil
}
func crackTopJobActionPastTense(action crackTopJobAction) string {
switch action {
case crackTopJobActionDelete:
return "Deleted"
case crackTopJobActionPause:
return "Paused"
case crackTopJobActionResume:
return "Resumed"
case crackTopJobActionCancel:
return "Cancelled"
default:
return "Updated"
}
}
func crackTopJobActionPresentParticiple(action crackTopJobAction) string {
switch action {
case crackTopJobActionDelete:
return "deleting"
case crackTopJobActionPause:
return "pausing"
case crackTopJobActionResume:
return "resuming"
case crackTopJobActionCancel:
return "cancelling"
default:
return "updating"
}
}
func (m *crackTopModel) removeJob(jobID string) {
if m.deletedJobIDs == nil {
m.deletedJobIDs = map[string]struct{}{}
}
m.deletedJobIDs[jobID] = struct{}{}
if m.snapshot == nil {
return
}
removeCrackTopJobFromSnapshot(m.snapshot, jobID)
m.dashboard = buildCrackTopDashboard(m.snapshot)
m.normalizeJobSelection()
m.normalizeWorkerSelection()
}
func (m *crackTopModel) suppressDeletedJobs(snapshot *crackTopSnapshot) {
if snapshot == nil || len(m.deletedJobIDs) == 0 {
return
}
for jobID := range m.deletedJobIDs {
found := false
for _, job := range snapshot.Jobs {
if job != nil && job.GetID() == jobID {
found = true
break
}
}
if found {
removeCrackTopJobFromSnapshot(snapshot, jobID)
} else {
delete(m.deletedJobIDs, jobID)
}
}
}
func (m *crackTopModel) overrideJobStatus(jobID string, status clientpb.CrackJobStatus, updatedAt int64) {
if m.jobStatusOverrides == nil {
m.jobStatusOverrides = map[string]crackTopJobStatusOverride{}
}
m.jobStatusOverrides[jobID] = crackTopJobStatusOverride{status: status, updatedAt: updatedAt}
if m.snapshot == nil {
return
}
for _, job := range m.snapshot.Jobs {
if job == nil || job.GetID() != jobID {
continue
}
job.Status = status
if updatedAt > job.GetUpdatedAt() {
job.UpdatedAt = updatedAt
}
break
}
m.dashboard = buildCrackTopDashboard(m.snapshot)
m.normalizeJobSelection()
m.normalizeWorkerSelection()
}
// An action can complete while a snapshot requested before the action is still
// in flight. Keep the authoritative RPC result visible until a snapshot either
// observes that state or advances beyond the action that produced it.
func (m *crackTopModel) applyJobStatusOverrides(snapshot *crackTopSnapshot) {
if snapshot == nil || len(m.jobStatusOverrides) == 0 {
return
}
for jobID, override := range m.jobStatusOverrides {
found := false
for _, job := range snapshot.Jobs {
if job == nil || job.GetID() != jobID {
continue
}
found = true
if job.GetStatus() == override.status || job.GetUpdatedAt() > override.updatedAt {
delete(m.jobStatusOverrides, jobID)
break
}
// UpdatedAt is second-resolution on the wire. One equal-version
// disagreement can be the snapshot that was already in flight when the
// action completed; a repeated disagreement is authoritative evidence
// of another lifecycle action in that same second.
if job.GetUpdatedAt() == override.updatedAt {
if override.equalVersionDisagreementSeen {
delete(m.jobStatusOverrides, jobID)
break
}
override.equalVersionDisagreementSeen = true
m.jobStatusOverrides[jobID] = override
}
job.Status = override.status
if override.updatedAt > job.GetUpdatedAt() {
job.UpdatedAt = override.updatedAt
}
break
}
if !found {
delete(m.jobStatusOverrides, jobID)
}
}
}
func removeCrackTopJobFromSnapshot(snapshot *crackTopSnapshot, jobID string) {
if snapshot == nil {
return
}
jobs := make([]*clientpb.CrackJob, 0, len(snapshot.Jobs))
for _, job := range snapshot.Jobs {
if job == nil || job.GetID() != jobID {
jobs = append(jobs, job)
continue
}
for _, task := range job.GetTasks() {
if task != nil {
delete(snapshot.taskTelemetry, task.GetID())
}
}
}
snapshot.Jobs = jobs
}
func crackTopTimerCmd(ctx context.Context, delay time.Duration, message tea.Msg) tea.Cmd {
return func() tea.Msg {
timer := time.NewTimer(max(time.Millisecond, delay))
@@ -609,7 +1052,9 @@ func (m *crackTopModel) filteredJobs() []crackTopJobRow {
matches := false
switch m.filter {
case crackTopFilterActive:
matches = row.Status == clientpb.CrackJobStatus_IN_PROGRESS
matches = row.Status == clientpb.CrackJobStatus_IN_PROGRESS || row.Status == clientpb.CrackJobStatus_PAUSED
case crackTopFilterPaused:
matches = row.Status == clientpb.CrackJobStatus_PAUSED
case crackTopFilterCompleted:
matches = row.Status == clientpb.CrackJobStatus_COMPLETED
case crackTopFilterFailed:
@@ -722,7 +1167,8 @@ func (m *crackTopModel) openFilter() tea.Cmd {
Description("Choose which loaded crack jobs appear. Use / to search.").
Options(
huh.NewOption("All loaded jobs", crackTopFilterAll),
huh.NewOption("Active", crackTopFilterActive),
huh.NewOption("Active or paused", crackTopFilterActive),
huh.NewOption("Paused", crackTopFilterPaused),
huh.NewOption("Completed", crackTopFilterCompleted),
huh.NewOption("Failed or cancelled", crackTopFilterFailed),
).
@@ -766,13 +1212,72 @@ func (m *crackTopModel) renderFilterModal(background string, width, height int)
return lipgloss.NewCanvas(width, height).Compose(compositor).Render()
}
func (m *crackTopModel) renderJobActionModal(background string, width, height int) string {
prompt := m.pendingJobAction
contentWidth := max(18, min(68, width-8))
title := "Update crack job?"
description := "Apply this action to the selected crack job?"
warning := ""
confirm := "y confirm"
titleStyle := m.styles.warning
border := clienttheme.Warning()
switch prompt.action {
case crackTopJobActionDelete:
title = "Delete crack job?"
description = "Permanently delete this terminal job and all stored tasks and results?"
warning = "This cannot be undone."
confirm = "y delete"
titleStyle = m.styles.danger
border = clienttheme.Danger()
case crackTopJobActionPause:
title = "Pause crack job?"
description = "Pause this job and release its current crackstation work?"
warning = "An in-flight task attempt may restart when the job is resumed."
confirm = "y pause"
case crackTopJobActionCancel:
title = "Cancel crack job?"
description = "Stop this job and release all assigned crackstation work?"
warning = "Cancelled jobs cannot be resumed."
confirm = "y cancel"
titleStyle = m.styles.danger
border = clienttheme.Danger()
}
lines := []string{
titleStyle.Render(title),
m.styles.normal.Width(contentWidth).Render(description),
m.styles.primary.Render(prompt.jobID),
}
if warning != "" {
lines = append(lines, m.styles.warning.Render(warning))
}
lines = append(lines, m.styles.muted.Render(confirm+" • n/esc cancel"))
content := lipgloss.JoinVertical(
lipgloss.Left,
lines...,
)
card := m.styles.actionCard.BorderForeground(border).Width(contentWidth).Render(content)
card = crackTopFitBlock(card, width, height)
left := max(0, (width-lipgloss.Width(card))/2)
top := max(0, (height-lipgloss.Height(card))/2)
compositor := lipgloss.NewCompositor(
lipgloss.NewLayer(background).Z(0),
lipgloss.NewLayer(card).X(left).Y(top).Z(1),
)
return lipgloss.NewCanvas(width, height).Compose(compositor).Render()
}
func (m *crackTopModel) renderHeader(width int) string {
innerWidth := max(1, width-4)
activity := ""
if m.refreshing {
activity = m.spinner.View() + " refreshing"
} else if m.snapshot != nil && !m.snapshot.RefreshedAt.IsZero() {
if m.snapshot != nil && !m.snapshot.RefreshedAt.IsZero() {
activity = "updated " + m.snapshot.RefreshedAt.Local().Format("15:04:05")
if m.lastError != "" {
activity = "last snapshot " + m.snapshot.RefreshedAt.Local().Format("15:04:05")
} else if m.authoritativeSnapshotPending() {
activity = "job state live • task data syncing"
}
} else if m.lastError != "" {
activity = "no snapshot"
} else {
activity = "waiting for data"
}
@@ -792,31 +1297,43 @@ func (m *crackTopModel) renderHeader(width int) string {
if !m.dashboard.ClusterComplete && m.dashboard.ExpectedWorkers > 0 {
clusterRate += fmt.Sprintf(" (%d/%d live)", m.dashboard.ReportingWorkers, m.dashboard.ExpectedWorkers)
}
metrics := fmt.Sprintf(
"ACTIVE %d JOBS %d loaded WORKERS %d/%d online CLUSTER %s RECOVERED %d",
m.dashboard.ActiveJobs,
len(m.dashboard.Jobs),
m.dashboard.OnlineWorkers,
totalWorkers,
clusterRate,
m.dashboard.Recovered,
)
metrics := "ACTIVE — JOBS — WORKERS — CLUSTER — RECOVERED —"
if m.snapshot != nil {
metrics = fmt.Sprintf(
"ACTIVE %d JOBS %d loaded WORKERS %d/%d online CLUSTER %s RECOVERED %d",
m.dashboard.ActiveJobs,
len(m.dashboard.Jobs),
m.dashboard.OnlineWorkers,
totalWorkers,
clusterRate,
m.dashboard.Recovered,
)
if m.lastError != "" || m.authoritativeSnapshotPending() {
metrics = "LAST " + metrics
}
}
lineTwo := crackTopFitLine(m.styles.normal.Render(metrics), innerWidth)
progressLabel := "GLOBAL "
progressText := " awaiting keyspace"
if m.dashboard.ProgressKnown {
progressText := " awaiting first snapshot"
progressKnown := m.snapshot != nil && m.lastError == "" && !m.authoritativeSnapshotPending() && m.dashboard.ProgressKnown
if m.lastError != "" {
progressText = " current data unavailable"
} else if m.authoritativeSnapshotPending() {
progressText = " task data syncing"
} else if progressKnown {
progressText = fmt.Sprintf(" %5.1f%%", m.dashboard.Progress*100)
} else if m.dashboard.ActiveJobs == 0 {
} else if m.snapshot != nil && m.dashboard.ActiveJobs == 0 {
progressText = " no active jobs"
} else {
} else if m.snapshot != nil {
progressText = " awaiting keyspace"
if m.dashboard.ProgressJobs > 0 {
progressText = fmt.Sprintf(" %d/%d jobs ready", m.dashboard.ProgressJobs, m.dashboard.ActiveJobs)
}
}
progressWidth := max(6, innerWidth-ansi.StringWidth(progressLabel)-ansi.StringWidth(progressText))
bar := m.renderUnknownProgress(progressWidth)
if m.dashboard.ProgressKnown {
if progressKnown {
bar = m.renderProgressBar(progressWidth, m.dashboard.Progress, clientpb.CrackJobStatus_IN_PROGRESS)
}
lineThree := crackTopFitLine(m.styles.heading.Render(progressLabel)+bar+m.styles.muted.Render(progressText), innerWidth)
@@ -834,6 +1351,11 @@ func (m *crackTopModel) renderJobsPane(width, height int) string {
capacity := max(1, (innerHeight-1)/2)
start, end := crackTopVisibleRange(len(jobs), m.jobCursor, capacity)
header := fmt.Sprintf("JOBS %d shown • all active + recent history • filter: %s", len(jobs), m.filter)
if m.authoritativeSnapshotPending() {
header = fmt.Sprintf("JOBS %d shown • TASK DATA LAST/SYNCING • filter: %s", len(jobs), m.filter)
} else if m.lastError != "" && m.snapshot != nil {
header = fmt.Sprintf("JOBS %d shown • LAST SNAPSHOT • filter: %s", len(jobs), m.filter)
}
if len(jobs) > capacity {
header += fmt.Sprintf(" • %d-%d", start+1, end)
}
@@ -843,7 +1365,7 @@ func (m *crackTopModel) renderJobsPane(width, height int) string {
}
for index := start; index < end; index++ {
row := jobs[index]
selected := row.ID == m.selectedJobID
selected := row.ID == m.selectedJobID && m.focus == crackTopFocusJobs
cursor := " "
idStyle := m.styles.primary
if selected {
@@ -904,6 +1426,11 @@ func (m *crackTopModel) renderWorkersPane(width, height int) string {
capacity := max(1, (innerHeight-1)/2)
start, end := crackTopVisibleRange(len(workers), m.workerCursor, capacity)
header := fmt.Sprintf("CRACKSTATIONS %d online", m.dashboard.OnlineWorkers)
if m.authoritativeSnapshotPending() {
header += " • LAST SNAPSHOT/SYNCING"
} else if m.lastError != "" && m.snapshot != nil {
header += " • LAST SNAPSHOT"
}
if len(workers) > capacity {
header += fmt.Sprintf(" • %d-%d/%d", start+1, end, len(workers))
}
@@ -915,7 +1442,7 @@ func (m *crackTopModel) renderWorkersPane(width, height int) string {
worker := workers[index]
cursor := " "
identityStyle := m.styles.primary
if worker.ID == m.selectedWorkerID {
if worker.ID == m.selectedWorkerID && m.focus == crackTopFocusWorkers {
cursor = m.styles.selected.Render("› ")
identityStyle = m.styles.selected
}
@@ -939,6 +1466,13 @@ func (m *crackTopModel) renderWorkersPane(width, height int) string {
lines = append(lines, crackTopFitLine(first, innerWidth))
details := make([]string, 0, 5)
if !worker.LastSeen.IsZero() {
seen := "telemetry "
if worker.TelemetryLive {
seen = "seen "
}
details = append(details, seen+worker.LastSeen.Local().Format("15:04:05"))
}
if worker.JobID != "" {
details = append(details, "job "+crackTopShortID(worker.JobID))
} else if worker.Online && strings.EqualFold(state, "IDLE") {
@@ -947,21 +1481,14 @@ func (m *crackTopModel) renderWorkersPane(width, height int) string {
details = append(details, "status unavailable")
}
if worker.Devices > 0 {
details = append(details, fmt.Sprintf("%d devices", worker.Devices))
details = append(details, fmt.Sprintf("%d dev", worker.Devices))
}
if worker.HasTemp {
details = append(details, fmt.Sprintf("%.0f C", worker.Temperature))
details = append(details, fmt.Sprintf("%.0f°C", worker.Temperature))
}
if worker.HasUtil {
details = append(details, fmt.Sprintf("%.0f%% util", worker.Utilization))
}
if !worker.LastSeen.IsZero() {
seen := "last telemetry "
if worker.TelemetryLive {
seen = "seen "
}
details = append(details, seen+worker.LastSeen.Local().Format("15:04:05"))
}
if len(details) == 0 {
details = append(details, "no live device telemetry")
}
@@ -975,7 +1502,7 @@ func (m *crackTopModel) renderWorkersPane(width, height int) string {
}
func (m *crackTopModel) renderFooter(width int) string {
message := "tab pane • ↑/k ↓/j select • g/G first/last • f filter • r refresh • q quit"
message := "tab pane • ↑/k ↓/j • p pause • u resume • c cancel • d delete • f filter • r refresh • q quit"
style := m.styles.muted
if m.toast != "" {
message = m.toast
@@ -992,6 +1519,14 @@ func (m *crackTopModel) renderFooter(width int) string {
default:
style = m.styles.primary
}
} else if m.runningJobAction.jobID != "" {
progress := crackTopJobActionPresentParticiple(m.runningJobAction.action)
message = fmt.Sprintf(
"%s crack job %s…",
strings.ToUpper(progress[:1])+progress[1:],
crackTopShortID(m.runningJobAction.jobID),
)
style = m.styles.warning
} else if m.lastError != "" {
retained := "no snapshot available"
if m.snapshot != nil {
@@ -1012,6 +1547,8 @@ func (m *crackTopModel) renderJobStatus(status clientpb.CrackJobStatus) string {
return m.styles.danger.Render(text)
case clientpb.CrackJobStatus_CANCELLED:
return m.styles.muted.Render(text)
case clientpb.CrackJobStatus_PAUSED:
return m.styles.secondary.Render(text)
default:
return m.styles.warning.Render(text)
}
@@ -1043,6 +1580,8 @@ func (m *crackTopModel) renderProgressBar(width int, fraction float64, status cl
fill = clienttheme.Danger()
case clientpb.CrackJobStatus_CANCELLED:
fill = clienttheme.DefaultMod(500)
case clientpb.CrackJobStatus_PAUSED:
fill = clienttheme.Secondary()
}
bar := progress.New(
progress.WithWidth(max(1, width)),
@@ -1073,7 +1612,7 @@ func crackTopPanel(width, height int, border color.Color, content string) string
Width(width).
Height(height).
Padding(0, 1).
Border(lipgloss.RoundedBorder()).
Border(lipgloss.NormalBorder()).
BorderForeground(border).
Render(strings.Join(lines, "\n"))
}
@@ -1,4 +1,4 @@
package crack
package top
import (
"context"
@@ -392,7 +392,7 @@ func TestCrackTopHidesStaleAndOfflineDeviceTelemetry(t *testing.T) {
model.height = 30
model.normalizeJobSelection()
plain := ansi.Strip(model.View().Content)
for _, hidden := range []string{"111 H/s", "222 H/s", "99 C", "88 C", "95% util", "94% util"} {
for _, hidden := range []string{"111 H/s", "222 H/s", "99 C", "88 C", "99°C", "88°C", "95% util", "94% util"} {
if strings.Contains(plain, hidden) {
t.Errorf("view exposed stale/offline telemetry %q:\n%s", hidden, plain)
}
@@ -618,6 +618,7 @@ func TestCrackTopViewIsFullScreenBoundedAndShowsLiveTelemetry(t *testing.T) {
width, height int
}{
{name: "wide", width: 132, height: 36},
{name: "overview default", width: crackTopDefaultWidth, height: crackTopDefaultHeight},
{name: "narrow", width: 90, height: 28},
{name: "minimum", width: crackTopMinWidth, height: crackTopMinHeight},
{name: "tiny", width: 31, height: 8},
@@ -663,7 +664,7 @@ func TestCrackTopViewIsFullScreenBoundedAndShowsLiveTelemetry(t *testing.T) {
"Beta Rig [worker02]",
"2.00 kH/s",
"2.50 MH/s",
"2 devices",
"2 dev",
} {
if !strings.Contains(plain, expected) {
t.Errorf("wide view does not contain %q:\n%s", expected, plain)
@@ -974,6 +975,8 @@ func crackTopRunModelCommands(t *testing.T, model *crackTopModel, command tea.Cm
//nolint:gocyclo // This test intentionally follows the interactive filter through apply and cancel transitions.
func TestCrackTopHuhFilterAppliesAndEscapeCancels(t *testing.T) {
model := newCrackTopModel(context.Background(), nil, nil, time.Second)
model.snapshot = &crackTopSnapshot{RefreshedAt: time.Now()}
model.refreshing = false
model.dashboard = crackTopDashboard{Jobs: []crackTopJobRow{
{ID: "active", Status: clientpb.CrackJobStatus_IN_PROGRESS},
{ID: "done", Status: clientpb.CrackJobStatus_COMPLETED},
+77 -14
View File
@@ -68,9 +68,12 @@ const (
)
type docEntry struct {
Name string
Content string
Description string
Name string
Content string
Description string
searchTitle string
searchDescription string
searchContent string
}
type docsHelpEntry struct {
@@ -181,8 +184,12 @@ func newDocsModel(entries []docEntry) *docsModel {
styles: newDocsStyles(),
}
for _, entry := range entries {
model.entriesByName[entry.Name] = entry
for i := range model.entries {
entry := &model.entries[i]
entry.searchTitle = normalizeDocSearchText(entry.Name)
entry.searchDescription = normalizeDocSearchText(entry.Description)
entry.searchContent = normalizeDocSearchText(entry.Content)
model.entriesByName[entry.Name] = *entry
}
if idx := preferredDocIndex(entries); idx >= 0 {
@@ -522,13 +529,23 @@ func (m *docsModel) updateFilteredEntries(preferCurrent bool) {
return
}
filtered := make([]docEntry, 0, len(m.entries))
type rankedEntry struct {
entry docEntry
score int
}
filtered := make([]rankedEntry, 0, len(m.entries))
for _, entry := range m.entries {
if matchesDocQuery(entry, query) {
filtered = append(filtered, entry)
if score, matches := docQueryScore(entry, query); matches {
filtered = append(filtered, rankedEntry{entry: entry, score: score})
}
}
m.filteredEntries = filtered
sort.SliceStable(filtered, func(i, j int) bool {
return filtered[i].score > filtered[j].score
})
m.filteredEntries = make([]docEntry, len(filtered))
for i, result := range filtered {
m.filteredEntries[i] = result.entry
}
m.updateBrowserPagination()
if preferCurrent && m.selectDocInVisible(m.currentDocName) {
@@ -539,12 +556,58 @@ func (m *docsModel) updateFilteredEntries(preferCurrent bool) {
m.syncSelectionFromBrowser()
}
func matchesDocQuery(entry docEntry, query string) bool {
if query == "" {
return true
func docQueryScore(entry docEntry, query string) (int, bool) {
normalizedQuery := strings.TrimSpace(normalizeDocSearchText(query))
if normalizedQuery == "" {
return 0, true
}
haystack := strings.ToLower(entry.Name + ".md\n" + entry.Description)
return strings.Contains(haystack, query)
title := entry.searchTitle
terms := strings.Fields(normalizedQuery)
for _, term := range terms {
if !strings.Contains(title, term) &&
!strings.Contains(entry.searchDescription, term) &&
!strings.Contains(entry.searchContent, term) {
return 0, false
}
}
score := 0
switch {
case title == normalizedQuery:
score += 1000
case strings.HasPrefix(title, normalizedQuery):
score += 500
case strings.Contains(title, normalizedQuery):
score += 300
}
for _, term := range terms {
if strings.Contains(title, term) {
score += 100
}
if strings.Contains(entry.searchDescription, term) {
score += 30
}
if strings.Contains(entry.searchContent, term) {
score++
}
}
return score, true
}
func normalizeDocSearchText(value string) string {
value = strings.ToLower(value)
value = strings.Map(func(r rune) rune {
switch {
case unicode.IsLetter(r), unicode.IsDigit(r):
return r
case unicode.IsSpace(r):
return ' '
default:
return ' '
}
}, value)
return strings.Join(strings.Fields(value), " ")
}
func (m *docsModel) moveBrowserCursorUp() {
+56
View File
@@ -106,6 +106,62 @@ func TestDocsModelFilteringUpdatesVisibleEntries(t *testing.T) {
}
}
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)
+9 -10
View File
@@ -3,6 +3,8 @@ package exec
import (
"testing"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
@@ -87,14 +89,11 @@ func TestShouldConvertExecuteShellcodePE(t *testing.T) {
func newExecuteShellcodeFlagsTestCommand(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "execute-shellcode"}
cmd.Flags().Uint32("shellcode-entropy", 1, "")
cmd.Flags().Bool("shellcode-compress", false, "")
cmd.Flags().Uint32("shellcode-exitopt", 1, "")
cmd.Flags().Uint32("shellcode-bypass", 3, "")
cmd.Flags().Uint32("shellcode-headers", 1, "")
cmd.Flags().Bool("shellcode-thread", false, "")
cmd.Flags().Bool("shellcode-unicode", false, "")
cmd.Flags().Uint32("shellcode-oep", 0, "")
return cmd
for _, cmd := range Commands(&console.SliverClient{}) {
if cmd.Name() == consts.ExecuteShellcodeStr {
return cmd
}
}
t.Fatalf("missing %q command", consts.ExecuteShellcodeStr)
return nil
}
+54
View File
@@ -0,0 +1,54 @@
package generate
import (
"strconv"
"testing"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
func TestCanariesCommandRegistered(t *testing.T) {
root := &cobra.Command{Use: "sliver"}
root.AddCommand(Commands(&console.SliverClient{})...)
cmd, _, err := root.Find([]string{consts.CanariesStr})
if err != nil {
t.Fatalf("find canaries command: %v", err)
}
if cmd.Parent() != root {
t.Fatal("canaries command is not registered at the root")
}
if cmd.Run == nil {
t.Fatal("canaries command has no handler")
}
if cmd.GroupID != consts.PayloadsHelpGroup {
t.Errorf("canaries group = %q, want %q", cmd.GroupID, consts.PayloadsHelpGroup)
}
for _, want := range []struct {
name string
shorthand string
valueType string
defaultValue string
}{
{name: "burned", shorthand: "b", valueType: "bool", defaultValue: "false"},
{name: "timeout", shorthand: "t", valueType: "int64", defaultValue: strconv.Itoa(flags.DefaultTimeout)},
} {
flag := cmd.Flags().Lookup(want.name)
if flag == nil {
t.Fatalf("canaries command is missing --%s", want.name)
}
if flag.Shorthand != want.shorthand {
t.Errorf("--%s shorthand = %q, want %q", want.name, flag.Shorthand, want.shorthand)
}
if flag.Value.Type() != want.valueType {
t.Errorf("--%s type = %q, want %q", want.name, flag.Value.Type(), want.valueType)
}
if flag.DefValue != want.defaultValue {
t.Errorf("--%s default = %q, want %q", want.name, flag.DefValue, want.defaultValue)
}
}
}
+5 -2
View File
@@ -288,13 +288,15 @@ func Commands(con *console.SliverClient) []*cobra.Command {
implantBuildsCmd.AddCommand(implantsRmCmd)
implantsStageCmd := &cobra.Command{
Use: consts.StageStr,
Use: consts.StageStr + " [implant-name] ...",
Short: "Serve a previously generated build",
Long: help.GetHelpFor([]string{consts.ImplantBuildsStr, consts.StageStr}),
Args: cobra.ArbitraryArgs,
Run: func(cmd *cobra.Command, args []string) {
ImplantsStageCmd(cmd, con, args)
},
}
carapace.Gen(implantsStageCmd).PositionalAnyCompletion(ImplantBuildNameCompleter(con))
implantBuildsCmd.AddCommand(implantsStageCmd)
canariesCmd := &cobra.Command{
@@ -311,7 +313,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
return []*cobra.Command{generateCmd, regenerateCmd, profilesCmd, implantBuildsCmd}
return []*cobra.Command{generateCmd, regenerateCmd, profilesCmd, implantBuildsCmd, canariesCmd}
}
// coreImplantFlags binds all flags common to all sliver implant types.
@@ -368,6 +370,7 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
bindSpoofMetadataFlag(f)
f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'archive' (for Go c-archives), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)")
f.String("service-name", "", "service name to register the implant under (service format only)")
// Shellcode generation options:
// - Windows: Donut
+5
View File
@@ -405,6 +405,10 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
// Default to exe
configFormat = clientpb.OutputFormat_EXECUTABLE
}
serviceName, _ := cmd.Flags().GetString("service-name")
if !isService {
serviceName = ""
}
targetOSF, _ := cmd.Flags().GetString("os")
targetOS := strings.ToLower(targetOSF)
@@ -541,6 +545,7 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
IsService: isService,
IsShellcode: isShellcode,
Exports: exports,
ServiceName: serviceName,
RunAtLoad: runAtLoad,
NetGoEnabled: netGo,
+17
View File
@@ -37,6 +37,23 @@ func ImplantsStageCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
return
}
if len(args) > 0 {
selected := []string{}
for _, name := range args {
if _, ok := builds.Configs[name]; !ok {
con.PrintErrorf("No implant build found with name '%s'\n", name)
return
}
selected = append(selected, name)
}
_, err = con.Rpc.StageImplantBuild(context.Background(), &clientpb.ImplantStageReq{Build: selected})
if err != nil {
con.PrintErrorf("Failed to serve implant %s\n", err)
return
}
return
}
options := []string{}
for name := range builds.Configs {
options = append(options, name)
@@ -0,0 +1,2 @@
# Preserve pinned upstream script bytes for SHA-256 verification on every OS.
*.cna -text
+22 -1
View File
@@ -20,6 +20,7 @@ package registry
import (
"context"
"encoding/binary"
"fmt"
"os"
"path/filepath"
@@ -151,7 +152,27 @@ func PrintRegRead(regRead *sliverpb.RegistryRead, con *console.SliverClient) {
con.PrintErrorf("%s\n", regRead.Response.Err)
return
}
con.Println(regRead.Value)
con.Println(formatRegRead(regRead))
}
func formatRegRead(regRead *sliverpb.RegistryRead) string {
switch regRead.Type {
case sliverpb.RegistryType_Binary:
return fmt.Sprintf("%v", regRead.Binary)
case sliverpb.RegistryType_DWORD:
if len(regRead.Binary) == 4 {
return fmt.Sprintf("0x%08x", binary.LittleEndian.Uint32(regRead.Binary))
}
return fmt.Sprintf("%v", regRead.Binary)
case sliverpb.RegistryType_QWORD:
if len(regRead.Binary) == 8 {
return fmt.Sprintf("0x%08x", binary.LittleEndian.Uint64(regRead.Binary))
}
return fmt.Sprintf("%v", regRead.Binary)
default:
// Unknown preserves compatibility with responses from older implants.
return regRead.Value
}
}
func writeHiveDump(data []byte, encoder string, fileName string, saveLoot bool, lootName string, lootType string, lootFileName string, con *console.SliverClient) {
+54
View File
@@ -0,0 +1,54 @@
package registry
import (
"testing"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
func TestFormatRegRead(t *testing.T) {
tests := []struct {
name string
response *sliverpb.RegistryRead
want string
}{
{
name: "legacy response",
response: &sliverpb.RegistryRead{Value: "legacy value"},
want: "legacy value",
},
{
name: "string",
response: &sliverpb.RegistryRead{Type: sliverpb.RegistryType_String, Value: "string value"},
want: "string value",
},
{
name: "binary",
response: &sliverpb.RegistryRead{Type: sliverpb.RegistryType_Binary, Binary: []byte{0x00, 0x7f, 0x80, 0xff}},
want: "[0 127 128 255]",
},
{
name: "dword",
response: &sliverpb.RegistryRead{Type: sliverpb.RegistryType_DWORD, Binary: []byte{0xde, 0xc0, 0x17, 0x5a}},
want: "0x5a17c0de",
},
{
name: "qword",
response: &sliverpb.RegistryRead{Type: sliverpb.RegistryType_QWORD, Binary: []byte{0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01}},
want: "0x123456789abcdef",
},
{
name: "malformed dword",
response: &sliverpb.RegistryRead{Type: sliverpb.RegistryType_DWORD, Binary: []byte{0x01}},
want: "[1]",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := formatRegRead(test.response); got != test.want {
t.Fatalf("formatRegRead() = %q, want %q", got, test.want)
}
})
}
}
@@ -0,0 +1,74 @@
package update
import (
"testing"
"github.com/bishopfox/sliver/client/version"
)
func TestFindWindowsExecutableAssetForArchitecture(t *testing.T) {
for _, tc := range []struct {
prefix string
arch string
wrongArch string
}{
{prefix: "sliver-client", arch: "amd64", wrongArch: "arm64"},
{prefix: "sliver-client", arch: "arm64", wrongArch: "386"},
{prefix: "sliver-client", arch: "386", wrongArch: "amd64"},
{prefix: "sliver-server", arch: "amd64", wrongArch: "arm64"},
{prefix: "sliver-server", arch: "arm64", wrongArch: "amd64"},
} {
t.Run(tc.prefix+"/"+tc.arch, func(t *testing.T) {
want := tc.prefix + "_windows-" + tc.arch + ".exe"
otherPrefix := "sliver-server"
if tc.prefix == otherPrefix {
otherPrefix = "sliver-client"
}
assets := releaseAssets(
want+".minisig",
otherPrefix+"_windows-"+tc.arch+".exe",
tc.prefix+"_windows-"+tc.wrongArch+".exe",
want,
)
got := findAssetFor(tc.prefix, assetSuffixes("windows", tc.arch), assets)
if got == nil || got.Name != want {
t.Fatalf("findAssetFor(%s, windows/%s) = %v, want %q", tc.prefix, tc.arch, got, want)
}
})
}
}
func TestFindNonWindowsReleaseAssets(t *testing.T) {
for _, tc := range []struct {
name string
prefix string
goos string
arch string
want string
}{
{name: "linux zip", prefix: "sliver-client", goos: "linux", arch: "amd64", want: "sliver-client_linux-amd64.zip"},
{name: "linux extensionless", prefix: "sliver-server", goos: "linux", arch: "arm64", want: "sliver-server_linux-arm64"},
{name: "darwin zip", prefix: "sliver-client", goos: "darwin", arch: "arm64", want: "sliver-client_macos-arm64.zip"},
{name: "darwin extensionless", prefix: "sliver-server", goos: "darwin", arch: "amd64", want: "sliver-server_darwin-amd64"},
} {
t.Run(tc.name, func(t *testing.T) {
assets := releaseAssets(tc.want+".minisig", tc.want)
got := findAssetFor(tc.prefix, assetSuffixes(tc.goos, tc.arch), assets)
if got == nil || got.Name != tc.want {
t.Fatalf("findAssetFor(%s, %s/%s) = %v, want %q", tc.prefix, tc.goos, tc.arch, got, tc.want)
}
})
}
}
func releaseAssets(names ...string) []version.Asset {
assets := make([]version.Asset, 0, len(names))
for _, name := range names {
assets = append(assets, version.Asset{
Name: name,
BrowserDownloadURL: "https://example.invalid/releases/v1/" + name,
})
}
return assets
}
+7 -15
View File
@@ -1,3 +1,4 @@
// Package update implements release download and verification commands.
package update
/*
@@ -243,6 +244,11 @@ func assetSuffixes(goos, goarch string) []string {
add(fmt.Sprintf("_%s.zip", goos))
add(fmt.Sprintf("_%s", goos))
if goos == "windows" {
add(fmt.Sprintf("_windows-%s.exe", goarch))
add("_windows.exe")
}
if goos == "darwin" {
add(fmt.Sprintf("_macos-%s.zip", goarch))
add(fmt.Sprintf("_macos-%s", goarch))
@@ -332,21 +338,19 @@ func downloadAssetWithSignature(con *console.SliverClient, client *http.Client,
if err != nil {
return fmt.Errorf("download asset: %w", err)
}
defer func() { _ = os.Remove(assetTempPath) }()
sigData, err := downloadBytesWithRetries(client, sigURL, sigLimit, "signature")
if err != nil {
cleanupAssetFiles(assetTempPath, finalPath)
return fmt.Errorf("download signature: %w", err)
}
if err := verifyMinisignSignature(assetTempPath, sigData, assetFileName, publicKey); err != nil {
cleanupAssetFiles(assetTempPath, finalPath)
return err
}
con.PrintSuccessf("Signature verified for %s\n", finalPath)
if err := replaceFile(assetTempPath, finalPath); err != nil {
os.Remove(assetTempPath)
return err
}
return nil
@@ -628,17 +632,5 @@ func trustedCommentFile(trustedComment string) string {
}
func replaceFile(src, dst string) error {
if err := os.Remove(dst); err != nil && !os.IsNotExist(err) {
return err
}
return os.Rename(src, dst)
}
func cleanupAssetFiles(tempPath, finalPath string) {
if tempPath != "" {
_ = os.Remove(tempPath)
}
if finalPath != "" {
_ = os.Remove(finalPath)
}
}
+122
View File
@@ -0,0 +1,122 @@
package update
import (
"bytes"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/bishopfox/sliver/client/version"
"github.com/bishopfox/sliver/util/minisign"
)
func TestDownloadAssetWithSignaturePreservesExistingArtifactOnFailure(t *testing.T) {
const assetName = "sliver-client_linux-amd64.zip"
oldContent := []byte("previous verified download")
newContent := []byte("unverified replacement")
for _, tc := range []struct {
name string
signatureStatus int
signatureBody string
wantError string
}{
{name: "signature download fails", signatureStatus: http.StatusNotFound, wantError: "download signature"},
{name: "signature is malformed", signatureStatus: http.StatusOK, signatureBody: "not a minisign signature", wantError: "signature verification failed"},
} {
t.Run(tc.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/" + assetName:
_, _ = w.Write(newContent)
case "/" + assetName + ".minisig":
w.WriteHeader(tc.signatureStatus)
_, _ = w.Write([]byte(tc.signatureBody))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
dir := t.TempDir()
finalPath := filepath.Join(dir, assetName)
if err := os.WriteFile(finalPath, oldContent, 0o600); err != nil {
t.Fatal(err)
}
asset := &version.Asset{
Name: assetName,
Size: len(newContent),
BrowserDownloadURL: server.URL + "/" + assetName,
}
err := downloadAssetWithSignature(nil, server.Client(), asset, nil, dir, minisign.PublicKey{})
if err == nil || !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("downloadAssetWithSignature error = %v, want %q", err, tc.wantError)
}
got, err := os.ReadFile(finalPath)
if err != nil {
t.Fatalf("previous artifact was removed: %v", err)
}
if !bytes.Equal(got, oldContent) {
t.Fatalf("previous artifact changed: got %q, want %q", got, oldContent)
}
tempPaths, err := filepath.Glob(filepath.Join(dir, assetName+".tmp-*"))
if err != nil {
t.Fatal(err)
}
if len(tempPaths) != 0 {
t.Fatalf("temporary downloads remain: %v", tempPaths)
}
})
}
}
func TestReplaceFileMissingSourcePreservesDestination(t *testing.T) {
dir := t.TempDir()
dst := filepath.Join(dir, "download.zip")
want := []byte("previous verified download")
if err := os.WriteFile(dst, want, 0o600); err != nil {
t.Fatal(err)
}
err := replaceFile(filepath.Join(dir, "missing.tmp"), dst)
if err == nil {
t.Fatal("replaceFile with a missing source succeeded")
}
got, err := os.ReadFile(dst)
if err != nil {
t.Fatalf("previous artifact was removed: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatalf("previous artifact changed: got %q, want %q", got, want)
}
}
func TestReplaceFileReplacesExistingDestination(t *testing.T) {
dir := t.TempDir()
src := filepath.Join(dir, "download.zip.tmp")
dst := filepath.Join(dir, "download.zip")
want := []byte("new verified download")
if err := os.WriteFile(src, want, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(dst, []byte("previous verified download"), 0o600); err != nil {
t.Fatal(err)
}
if err := replaceFile(src, dst); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(dst)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("replacement content = %q, want %q", got, want)
}
if _, err := os.Stat(src); !os.IsNotExist(err) {
t.Fatalf("source still exists after replacement: %v", err)
}
}
@@ -123,10 +123,11 @@ func webAddFile(web *clientpb.WebsiteAddContent, webpath string, contentType str
}
web.Contents[webpath] = &clientpb.WebContent{
Path: webpath,
ContentType: contentType,
OriginalFile: filepath.Base(contentPath),
Content: data,
Path: webpath,
ContentType: contentType,
OriginalFile: filepath.Base(contentPath),
Content: data,
ReplaceContent: true,
}
return nil
}
@@ -0,0 +1,42 @@
package websites
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
"google.golang.org/protobuf/proto"
)
func TestWebAddFileMarksContentReplacement(t *testing.T) {
for _, data := range [][]byte{nil, []byte("replacement")} {
name := "nonempty"
if len(data) == 0 {
name = "empty"
}
t.Run(name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "upload.txt")
if err := os.WriteFile(path, data, 0600); err != nil {
t.Fatal(err)
}
request := &clientpb.WebsiteAddContent{Contents: map[string]*clientpb.WebContent{}}
if err := webAddFile(request, "/upload.txt", "text/plain", path); err != nil {
t.Fatal(err)
}
encoded, err := proto.Marshal(request)
if err != nil {
t.Fatal(err)
}
decoded := &clientpb.WebsiteAddContent{}
if err := proto.Unmarshal(encoded, decoded); err != nil {
t.Fatal(err)
}
content := decoded.Contents["/upload.txt"]
if content == nil || !content.ReplaceContent || !bytes.Equal(content.Content, data) {
t.Fatalf("decoded upload = %v, want explicit replacement with %q", content, data)
}
})
}
}
+12 -1
View File
@@ -56,7 +56,7 @@ func WebsitesRmContent(cmd *cobra.Command, con *console.SliverClient, args []str
}
if recursive {
for contentPath := range website.Contents {
if strings.HasPrefix(contentPath, webPath) {
if isWebPathOrDescendant(contentPath, webPath) {
rmWebContent.Paths = append(rmWebContent.Paths, contentPath)
}
}
@@ -70,3 +70,14 @@ func WebsitesRmContent(cmd *cobra.Command, con *console.SliverClient, args []str
}
PrintWebsite(web, con)
}
func isWebPathOrDescendant(contentPath, webPath string) bool {
if webPath == "" {
return false
}
webPath = strings.TrimRight(webPath, "/")
if webPath == "" {
return strings.HasPrefix(contentPath, "/")
}
return contentPath == webPath || strings.HasPrefix(contentPath, webPath+"/")
}
@@ -46,3 +46,27 @@ func TestWebsiteRemoveSuccessMessage(t *testing.T) {
t.Fatalf("unexpected message: got %q want %q", got, want)
}
}
func TestRecursiveWebsiteContentPathSelection(t *testing.T) {
tests := []struct {
name string
webPath string
contentPath string
want bool
}{
{"exact file", "/public", "/public", true},
{"descendant", "/public", "/public/index.html", true},
{"sibling with shared prefix", "/public", "/publicity/index.html", false},
{"trailing slash", "/public/", "/public/index.html", true},
{"root", "/", "/public/index.html", true},
{"relative directory", "public", "public/index.html", true},
{"empty selection", "", "/public/index.html", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isWebPathOrDescendant(tt.contentPath, tt.webPath); got != tt.want {
t.Errorf("isWebPathOrDescendant(%q, %q) = %t, want %t", tt.contentPath, tt.webPath, got, tt.want)
}
})
}
}
@@ -49,6 +49,7 @@ func WebsitesUpdateContentCmd(cmd *cobra.Command, con *console.SliverClient, arg
Contents: map[string]*clientpb.WebContent{},
}
updateWeb.Contents[webPath] = &clientpb.WebContent{
Path: webPath,
ContentType: contentType,
}
+52 -13
View File
@@ -47,6 +47,7 @@ import (
"github.com/reeflective/console"
"github.com/reeflective/readline"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
uuid "uuid"
@@ -63,6 +64,10 @@ const (
DownN = "\033[%dB"
)
// isTerminal reports whether fd is a connected terminal.
// Variable rather than direct call so tests can override it.
var isTerminal = term.IsTerminal
// Observer - A function to call when the sessions changes.
type (
Observer func(*clientpb.Session, *clientpb.Beacon)
@@ -201,6 +206,7 @@ func NewConsole(isServer bool) *SliverClient {
// Global console settings
con.App.NewlineBefore = true
con.App.NewlineAfter = true
con.App.ExecuteMultiline = true
// Server menu.
server := con.App.Menu(consts.ServerMenu)
@@ -227,6 +233,7 @@ func NewConsole(isServer bool) *SliverClient {
// If run is true, the console application is started, making this call blocking. Otherwise, commands and
// RPC connection are bound to the console (making the console ready to run), but the console does not start.
func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, grpcConn *grpc.ClientConn, details *ConnectionDetails, serverCmds, sliverCmds console.Commands, run bool, rcScript string) error {
interactive := run && isTerminal(int(os.Stdin.Fd()))
con.IsCLI = !run
con.serverCmds = serverCmds
con.sliverCmds = sliverCmds
@@ -235,7 +242,7 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, grpcConn *grpc.Cl
// when asynchronously printing logs (that is, when no command is running).
// If ran from a system shell, however, those queries will block because
// the system shell is in control of stdin. So just use the classic Printf.
if con.IsCLI {
if !interactive {
con.printf = fmt.Printf
} else {
con.printf = con.App.TransientPrintf
@@ -260,7 +267,7 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, grpcConn *grpc.Cl
// Ascii cast sessions (complete terminal interface) are only useful
// for the interactive console. In CLI mode they would clobber stdout.
if !con.IsCLI {
if interactive {
asciicastLog := getConsoleAsciicastFile()
defer asciicastLog.Close()
@@ -276,20 +283,36 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, grpcConn *grpc.Cl
if rcScript != "" {
originalPrintf := con.printf
con.printf = fmt.Printf
con.runRCScript(serverCmds, sliverCmds, rcScript)
err := con.runCommandScript(serverCmds, sliverCmds, strings.NewReader(rcScript), "rc")
con.printf = originalPrintf
// An rc script is often a startup convenience in an interactive
// console. Keep opening the prompt after reporting its line errors.
if err != nil && !interactive {
return err
}
}
if !con.IsCLI {
if !run {
return nil
}
if interactive {
return con.App.Start()
}
return nil
if rcScript != "" {
return nil
}
// Readline needs a terminal. A pipe or redirected file is instead executed
// as a sequence of commands, ending cleanly at EOF.
return con.runCommandScript(serverCmds, sliverCmds, os.Stdin, "stdin")
}
func (con *SliverClient) runRCScript(serverCmds, sliverCmds console.Commands, rcScript string) {
scanner := bufio.NewScanner(strings.NewReader(rcScript))
func (con *SliverClient) runCommandScript(serverCmds, sliverCmds console.Commands, input io.Reader, source string) error {
scanner := bufio.NewScanner(input)
scanner.Buffer(make([]byte, 4096), 4*1024*1024)
lineNumber := 0
var firstError error
errorCount := 0
for scanner.Scan() {
lineNumber++
@@ -297,17 +320,31 @@ func (con *SliverClient) runRCScript(serverCmds, sliverCmds console.Commands, rc
if line == "" {
continue
}
if err := con.runRCLine(serverCmds, sliverCmds, line); err != nil {
con.PrintErrorf("rc line %d error: %s", lineNumber, err)
if err := con.runCommandLine(serverCmds, sliverCmds, line); err != nil {
lineError := fmt.Errorf("%s line %d: %w", source, lineNumber, err)
con.PrintErrorf("%s\n", lineError)
if firstError == nil {
firstError = lineError
}
errorCount++
}
}
if err := scanner.Err(); err != nil {
con.PrintErrorf("rc script error: %s", err)
readError := fmt.Errorf("%s line %d: %w", source, lineNumber+1, err)
con.PrintErrorf("%s\n", readError)
if firstError == nil {
firstError = readError
}
errorCount++
}
if errorCount > 0 {
return fmt.Errorf("%s: %d error(s) (first: %w)", source, errorCount, firstError)
}
return nil
}
func (con *SliverClient) runRCLine(serverCmds, sliverCmds console.Commands, line string) error {
func (con *SliverClient) runCommandLine(serverCmds, sliverCmds console.Commands, line string) error {
args, err := shellquote.Split(line)
if err != nil {
return fmt.Errorf("parse error: %w", err)
@@ -789,7 +826,9 @@ func (con *SliverClient) CheckLastUpdate() {
day := 24 * time.Hour
if compiledAt.Add(30 * day).Before(now) {
if lastUpdate == nil || lastUpdate.Add(30*day).Before(now) {
con.Printf("%sCheck for updates with the 'update' command\n\n", Info)
// The startup banner runs before readline enables raw terminal mode.
// Print directly so the reminder cannot redraw the prompt or query the cursor.
fmt.Printf("%sCheck for updates with the 'update' command\n\n", Info)
}
}
}
+103
View File
@@ -0,0 +1,103 @@
package console
import (
"io"
"log/slog"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/version"
reefconsole "github.com/reeflective/console"
)
func TestCheckLastUpdateBeforeReadline(t *testing.T) {
now := time.Now()
old := now.Add(-31 * 24 * time.Hour)
recent := now.Add(-24 * time.Hour)
for _, test := range []struct {
name string
compiledAt time.Time
lastCheck *time.Time
wantReminder bool
}{
{name: "old build without update check", compiledAt: old, wantReminder: true},
{name: "old build with old update check", compiledAt: old, lastCheck: &old, wantReminder: true},
{name: "old build with recent update check", compiledAt: old, lastCheck: &recent},
{name: "recent build without update check", compiledAt: recent},
} {
t.Run(test.name, func(t *testing.T) {
root := t.TempDir()
t.Setenv("SLIVER_CLIENT_ROOT_DIR", root)
t.Setenv("READLINE_CURSOR_POS", "1")
if test.lastCheck != nil {
lastCheck := strconv.FormatInt(test.lastCheck.Unix(), 10)
if err := os.WriteFile(filepath.Join(root, consts.LastUpdateCheckFileName), []byte(lastCheck), 0600); err != nil {
t.Fatal(err)
}
}
previousCompiledAt := version.CompiledAt
version.CompiledAt = strconv.FormatInt(test.compiledAt.Unix(), 10)
t.Cleanup(func() { version.CompiledAt = previousCompiledAt })
// Interactive startup selects the transient printer before the logo
// and update reminder run, but Readline has not acquired stdin yet.
app := reefconsole.New("sliver")
con := &SliverClient{
App: app,
printf: app.TransientPrintf,
jsonHandler: slog.NewJSONHandler(io.Discard, nil),
}
output := captureStartupOutput(t, con.CheckLastUpdate)
if strings.Contains(output, "\x1b[6n") {
t.Fatalf("startup output queried the cursor before readline: %q", output)
}
const reminder = "Check for updates with the 'update' command"
if got := strings.Contains(output, reminder); got != test.wantReminder {
t.Fatalf("update reminder present = %t, want %t; output: %q", got, test.wantReminder, output)
}
})
}
}
func captureStartupOutput(t *testing.T, writeOutput func()) string {
t.Helper()
// An unexpected cursor query must fail the assertion rather than wait
// indefinitely for a terminal response while the test is running.
stdin, err := os.Open(os.DevNull)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = stdin.Close() })
previousStdin := os.Stdin
os.Stdin = stdin
t.Cleanup(func() { os.Stdin = previousStdin })
reader, writer, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = reader.Close() })
t.Cleanup(func() { _ = writer.Close() })
previousStdout := os.Stdout
os.Stdout = writer
t.Cleanup(func() { os.Stdout = previousStdout })
writeOutput()
os.Stdout = previousStdout
if err := writer.Close(); err != nil {
t.Fatal(err)
}
output, err := io.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
return string(output)
}
+60 -40
View File
@@ -3,57 +3,77 @@ package console
import (
"sync"
"testing"
"testing/synctest"
"time"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
func TestRemoveEventListenerUnblocksInFlightDispatchAndClosesReceiver(t *testing.T) {
con := &SliverClient{EventListeners: &sync.Map{}}
listenerID, events := con.CreateEventListener()
value, ok := con.EventListeners.Load(listenerID)
if !ok {
t.Fatal("created listener is not registered")
}
listener := value.(*consoleEventListener)
for index := 0; index < cap(listener.events); index++ {
listener.events <- &clientpb.Event{EventType: "buffered"}
}
dispatchDone := make(chan struct{})
go func() {
listener.send(&clientpb.Event{EventType: "in-flight"})
close(dispatchDone)
}()
con.RemoveEventListener(listenerID)
select {
case <-dispatchDone:
case <-time.After(time.Second):
t.Fatal("listener removal did not unblock in-flight dispatch")
}
if _, stillRegistered := con.EventListeners.Load(listenerID); stillRegistered {
t.Fatal("removed listener remains registered")
}
for event := range events {
_ = event
}
}
func TestConcurrentEventDispatchAndRemovalDoesNotPanic(_ *testing.T) {
for iteration := 0; iteration < 1_000; iteration++ {
synctest.Test(t, func(t *testing.T) {
con := &SliverClient{EventListeners: &sync.Map{}}
listenerID, events := con.CreateEventListener()
value, ok := con.EventListeners.Load(listenerID)
if !ok {
t.Fatal("created listener is not registered")
}
listener := value.(*consoleEventListener)
buffered := &clientpb.Event{EventType: "buffered"}
for index := 0; index < cap(listener.events); index++ {
listener.events <- buffered
}
dispatchDone := make(chan struct{})
go func() {
con.triggerEventListeners(&clientpb.Event{EventType: "race"})
event := &clientpb.Event{EventType: "in-flight"}
con.triggerEventListeners(event)
close(dispatchDone)
}()
con.RemoveEventListener(listenerID)
<-dispatchDone
for event := range events {
_ = event
// The full buffer and Wait establish that send is blocked before removal.
synctest.Wait()
select {
case <-dispatchDone:
t.Fatal("dispatch completed before listener removal")
default:
}
}
removeDone := make(chan struct{})
go func() {
con.RemoveEventListener(listenerID)
// Dispatch and repeated removal after closure must remain harmless.
con.triggerEventListeners(&clientpb.Event{EventType: "removed"})
listener.send(&clientpb.Event{EventType: "closed"})
con.RemoveEventListener(listenerID)
close(removeDone)
}()
for _, done := range []<-chan struct{}{dispatchDone, removeDone} {
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("listener removal and in-flight dispatch did not complete")
}
}
if _, stillRegistered := con.EventListeners.Load(listenerID); stillRegistered {
t.Fatal("removed listener remains registered")
}
deadline := time.After(time.Second)
for received := 0; ; received++ {
select {
case event, open := <-events:
if !open {
if received != cap(listener.events) {
t.Fatalf("received %d buffered events, want %d", received, cap(listener.events))
}
return
}
if event != buffered {
t.Fatalf("unexpected event after removal: %v", event)
}
case <-deadline:
t.Fatal("removed listener receiver did not close")
}
}
})
}
+156
View File
@@ -0,0 +1,156 @@
package console
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"reflect"
"strings"
"testing"
consts "github.com/bishopfox/sliver/client/constants"
reefconsole "github.com/reeflective/console"
"github.com/spf13/cobra"
)
func pipedTestCommands(got *[]string) reefconsole.Commands {
return func() *cobra.Command {
root := &cobra.Command{Use: "root", SilenceErrors: true, SilenceUsage: true}
root.AddCommand(&cobra.Command{
Use: "record value",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
*got = append(*got, args[0])
return nil
},
})
root.AddCommand(&cobra.Command{
Use: "fail",
RunE: func(_ *cobra.Command, _ []string) error {
return errors.New("command failed")
},
})
return root
}
}
func withPipedStdin(t *testing.T, input string) *os.File {
t.Helper()
reader, writer, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
if _, err := io.WriteString(writer, input); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = reader.Close() })
oldStdin := os.Stdin
os.Stdin = reader
t.Cleanup(func() { os.Stdin = oldStdin })
oldIsTerminal := isTerminal
isTerminal = func(int) bool { return false }
t.Cleanup(func() { isTerminal = oldIsTerminal })
return reader
}
func TestStartClientRunsPipedCommandsInOrder(t *testing.T) {
withPipedStdin(t, "\r\nrecord 'one two'\r\nrecord three\n record four ")
var got []string
con := NewConsole(false)
con.Settings.ConsoleLogs = false
cmds := pipedTestCommands(&got)
if err := StartClient(con, nil, nil, nil, cmds, cmds, true, ""); err != nil {
t.Fatalf("run piped commands: %v", err)
}
if want := []string{"one two", "three", "four"}; !reflect.DeepEqual(got, want) {
t.Fatalf("commands = %q, want %q", got, want)
}
if con.IsCLI {
t.Fatal("piped console should retain console mode for menu switching")
}
}
func TestStartClientPipedCommandsFollowMenuSwitch(t *testing.T) {
withPipedStdin(t, "use\nrecord implant\n")
var got []string
con := NewConsole(false)
con.Settings.ConsoleLogs = false
serverCmds := func() *cobra.Command {
root := &cobra.Command{Use: "root", SilenceErrors: true, SilenceUsage: true}
root.AddCommand(&cobra.Command{Use: "use", Run: func(_ *cobra.Command, _ []string) {
con.App.SwitchMenu(consts.ImplantMenu)
}})
return root
}
implantCmds := pipedTestCommands(&got)
if err := StartClient(con, nil, nil, nil, serverCmds, implantCmds, true, ""); err != nil {
t.Fatalf("run piped commands across menus: %v", err)
}
if want := []string{"implant"}; !reflect.DeepEqual(got, want) {
t.Fatalf("commands = %q, want %q", got, want)
}
}
func TestStartClientRCPrecedesPipedStdin(t *testing.T) {
reader := withPipedStdin(t, "record piped\n")
var got []string
con := NewConsole(false)
con.Settings.ConsoleLogs = false
cmds := pipedTestCommands(&got)
if err := StartClient(con, nil, nil, nil, cmds, cmds, true, "record rc\n"); err != nil {
t.Fatalf("run rc script: %v", err)
}
if want := []string{"rc"}; !reflect.DeepEqual(got, want) {
t.Fatalf("commands = %q, want %q", got, want)
}
remaining, err := io.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
if string(remaining) != "record piped\n" {
t.Fatalf("piped stdin was consumed after --rc: %q", remaining)
}
}
func TestStartClientEmptyPipeExitsAtEOF(t *testing.T) {
withPipedStdin(t, "")
con := NewConsole(false)
con.Settings.ConsoleLogs = false
var got []string
if err := StartClient(con, nil, nil, nil, pipedTestCommands(&got), nil, true, ""); err != nil {
t.Fatalf("empty stdin: %v", err)
}
}
func TestRunCommandScriptContinuesAndReportsErrors(t *testing.T) {
var got []string
con := NewConsole(false)
var output bytes.Buffer
con.printf = func(format string, args ...any) (int, error) {
return fmt.Fprintf(&output, format, args...)
}
cmds := pipedTestCommands(&got)
input := strings.NewReader("record first\nfail\nrecord second\nrecord 'unterminated\nrecord third\n")
err := con.runCommandScript(cmds, cmds, input, "stdin")
if err == nil || !strings.Contains(err.Error(), "2 error(s)") {
t.Fatalf("error = %v, want two failures", err)
}
if want := []string{"first", "second", "third"}; !reflect.DeepEqual(got, want) {
t.Fatalf("commands = %q, want %q", got, want)
}
for _, line := range []string{"stdin line 2", "stdin line 4"} {
if !strings.Contains(output.String(), line) {
t.Fatalf("missing %q in output %q", line, output.String())
}
}
}
+2
View File
@@ -116,6 +116,8 @@ const (
CrackFileUpdated = "crack-file-updated"
CrackStatusEvent = "crack-status"
CrackTaskStatus = "crack-task-status"
CrackTaskCancel = "crack-task-cancel"
CrackTaskCancelAck = "crack-task-cancel-ack"
CrackJobCreated = "crack-job-created"
CrackJobUpdated = "crack-job-updated"
CredentialCrackedEvent = "credential-cracked"
+6
View File
@@ -1598,6 +1598,8 @@ func TestSocksReceiveLoopOverloadedConnectionDoesNotBlockHealthyConnection(t *te
t.Fatal("healthy SOCKS terminal did not close after ordered data")
}
// Start local shutdown before releasing the fake receive stream.
proxy.beginStop()
stream.results <- socksReceiveTestResult{err: io.EOF}
if err := proxy.Stop(); err != nil {
t.Fatalf("stop SOCKS receive test proxy: %v", err)
@@ -1689,6 +1691,8 @@ func TestSocksReceiveLoopRejectsInvalidAcknowledgementAtExactTunnelScope(t *test
t.Fatal("invalid acknowledgement closed healthy sibling")
}
// Start local shutdown before releasing the fake receive stream.
proxy.beginStop()
stream.results <- socksReceiveTestResult{err: io.EOF}
if err := proxy.Stop(); err != nil {
t.Fatalf("stop acknowledgement test proxy: %v", err)
@@ -1754,6 +1758,8 @@ func TestSocksReceiveLoopAppliesValidAcknowledgementOutOfBand(t *testing.T) {
}
}
// Start local shutdown before releasing the fake receive stream.
proxy.beginStop()
stream.results <- socksReceiveTestResult{err: io.EOF}
if err := proxy.Stop(); err != nil {
t.Fatalf("stop valid acknowledgement test proxy: %v", err)
+95 -105
View File
@@ -16,113 +16,103 @@ func TestMultiplayerConnectModeDefaultsToAuto(t *testing.T) {
}
}
func TestSelectMultiplayerDialStrategyAutoWithoutWGConfigUsesDirectMTLS(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
strategy, err := selectMultiplayerDialStrategy(&assets.ClientConfig{})
if err != nil {
t.Fatalf("select dial strategy: %v", err)
}
if strategy != multiplayerDialDirect {
t.Fatalf("expected direct mTLS strategy, got %v", strategy)
}
}
func TestSelectMultiplayerDialStrategyAutoLegacyWGConfigUsesDirectMTLS(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(false))
if err != nil {
t.Fatalf("select dial strategy: %v", err)
}
if strategy != multiplayerDialDirect {
t.Fatalf("expected direct mTLS strategy, got %v", strategy)
}
}
func TestSelectMultiplayerDialStrategyAutoEnabledWGConfigRequiresCompleteConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
_, err := selectMultiplayerDialStrategy(&assets.ClientConfig{
WG: &assets.ClientWGConfig{
Enabled: true,
ServerPubKey: "server-pub",
ClientIP: "100.64.0.2",
func TestSelectMultiplayerDialStrategy(t *testing.T) {
for _, test := range []struct {
name string
mode MultiplayerConnectMode
config *assets.ClientConfig
want multiplayerDialStrategy
wantErr error
wantErrText string
}{
{
name: "auto without WireGuard config",
mode: MultiplayerConnectAuto,
config: &assets.ClientConfig{},
want: multiplayerDialDirect,
},
})
if !errors.Is(err, ErrIncompleteWireGuardConfig) {
t.Fatalf("expected incomplete WG config error, got %v", err)
}
}
func TestSelectMultiplayerDialStrategyAutoEnabledWGConfigUsesWireGuard(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(true))
if err != nil {
t.Fatalf("select dial strategy: %v", err)
}
if strategy != multiplayerDialWireGuard {
t.Fatalf("expected WireGuard strategy, got %v", strategy)
}
}
func TestSelectMultiplayerDialStrategyEnableWGRejectsMissingWGConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectEnableWG)
_, err := selectMultiplayerDialStrategy(&assets.ClientConfig{})
if !errors.Is(err, ErrMissingWireGuardConfig) {
t.Fatalf("expected missing WG config error, got %v", err)
}
}
func TestSelectMultiplayerDialStrategyEnableWGRejectsIncompleteWGConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectEnableWG)
_, err := selectMultiplayerDialStrategy(&assets.ClientConfig{
WG: &assets.ClientWGConfig{
ServerPubKey: "server-pub",
ClientIP: "100.64.0.2",
{
name: "auto with legacy WireGuard config",
mode: MultiplayerConnectAuto,
config: completeWGClientConfig(false),
want: multiplayerDialDirect,
},
})
if !errors.Is(err, ErrIncompleteWireGuardConfig) {
t.Fatalf("expected incomplete WG config error, got %v", err)
}
}
func TestSelectMultiplayerDialStrategyEnableWGUsesCompleteLegacyConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectEnableWG)
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(false))
if err != nil {
t.Fatalf("select dial strategy: %v", err)
}
if strategy != multiplayerDialWireGuard {
t.Fatalf("expected WireGuard strategy, got %v", strategy)
}
}
func TestSelectMultiplayerDialStrategyDisableWGOverridesEnabledConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectDisableWG)
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(true))
if err != nil {
t.Fatalf("select dial strategy: %v", err)
}
if strategy != multiplayerDialDirect {
t.Fatalf("expected direct strategy, got %v", strategy)
}
}
func TestSelectMultiplayerDialStrategyRejectsInvalidMode(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectMode(255))
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(true))
if strategy != multiplayerDialDirect {
t.Fatalf("expected invalid mode to fall back to direct strategy, got %v", strategy)
}
if err == nil || err.Error() != "invalid multiplayer connect mode 255" {
t.Fatalf("expected invalid mode error, got %v", err)
{
name: "auto with incomplete enabled config",
mode: MultiplayerConnectAuto,
config: &assets.ClientConfig{WG: &assets.ClientWGConfig{
Enabled: true, ServerPubKey: "server-pub", ClientIP: "100.64.0.2",
}},
want: multiplayerDialDirect,
wantErr: ErrIncompleteWireGuardConfig,
},
{
name: "auto with enabled WireGuard config",
mode: MultiplayerConnectAuto,
config: completeWGClientConfig(true),
want: multiplayerDialWireGuard,
},
{
name: "enable with missing WireGuard config",
mode: MultiplayerConnectEnableWG,
config: &assets.ClientConfig{},
want: multiplayerDialDirect,
wantErr: ErrMissingWireGuardConfig,
},
{
name: "enable with incomplete WireGuard config",
mode: MultiplayerConnectEnableWG,
config: &assets.ClientConfig{WG: &assets.ClientWGConfig{
ServerPubKey: "server-pub", ClientIP: "100.64.0.2",
}},
want: multiplayerDialDirect,
wantErr: ErrIncompleteWireGuardConfig,
},
{
name: "enable with complete legacy config",
mode: MultiplayerConnectEnableWG,
config: completeWGClientConfig(false),
want: multiplayerDialWireGuard,
},
{
name: "disable overrides enabled config",
mode: MultiplayerConnectDisableWG,
config: completeWGClientConfig(true),
want: multiplayerDialDirect,
},
{
name: "invalid mode",
mode: MultiplayerConnectMode(255),
config: completeWGClientConfig(true),
want: multiplayerDialDirect,
wantErrText: "invalid multiplayer connect mode 255",
},
{
name: "nil config",
mode: MultiplayerConnectAuto,
want: multiplayerDialDirect,
wantErrText: "client config is required",
},
} {
t.Run(test.name, func(t *testing.T) {
setTestMultiplayerConnectMode(t, test.mode)
strategy, err := selectMultiplayerDialStrategy(test.config)
if strategy != test.want {
t.Errorf("dial strategy = %v, want %v", strategy, test.want)
}
switch {
case test.wantErr != nil:
if !errors.Is(err, test.wantErr) {
t.Fatalf("select dial strategy error = %v, want %v", err, test.wantErr)
}
case test.wantErrText != "":
if err == nil || err.Error() != test.wantErrText {
t.Fatalf("select dial strategy error = %v, want %q", err, test.wantErrText)
}
case err != nil:
t.Fatalf("select dial strategy: %v", err)
}
})
}
}
+4 -2
View File
@@ -1,5 +1,7 @@
# Sliver AI Agentic Loop
**Historical design note:** This document describes the initial agentic-loop port. The current tool surface includes operations that modify remote state and is not read-only.
## Goal
Port the example agentic loop from `examples/codex/codex-rs` into Sliver's AI chat so the server can:
@@ -141,7 +143,7 @@ If anything fails:
## Tool Surface
The first port exposes a safe read-only tool set:
The initial port exposed a read-only tool set:
- `list_sessions_and_beacons`
- `fs_ls`
@@ -221,7 +223,7 @@ Because UI-only items live in the database:
This port intentionally differs from the example in a few places.
- Sliver currently uses non-streaming Responses API requests, so reasoning items arrive as completed items rather than token deltas.
- Tool execution is currently a bounded read-only server-side function set, not the full Codex local tool surface.
- The initial port used a bounded read-only server-side function set, not the full Codex local tool surface.
- Sliver persists intermediate items in its conversation DB rather than only in a transient thread runtime.
The important invariant is still the same:
+4 -4
View File
@@ -5,18 +5,18 @@
- `components/` hosts reusable UI; `util/` stores loaders such as `Docs` and `Themes`.
- `public/` serves static assets and the generated `docs.json` consumed by search.
- `prebuild/` scripts run during `next build` to turn Markdown into JSON payloads.
- Tailwind config sits in `tailwind.config.ts`; shared CSS lives in `styles/`.
- Tailwind config sits in `tailwind.config.js`; shared CSS lives in `styles/`.
## Build, Test, and Development Commands
- `npm install` sets up dependencies.
- `npm run dev` starts the docs server with hot reload on `http://localhost:3000`.
- `npm run build` exports the static site and regenerates `public/docs.json` plus tutorials JSON.
- `npm run start` serves the last build for smoke tests.
- `npm run lint` runs the Next.js ESLint suite.
- `python3 -m http.server 8000 --directory out` serves the static build for smoke tests; `next start` does not support static exports.
- `npm run lint` runs ESLint.
- `npm run offline` builds, then zips `out/` for offline distribution.
## Coding Style & Naming Conventions
Use TypeScript with 2-space indentation and ES module imports. Keep React components and files in `components/` PascalCase, helpers camelCase, and Markdown files kebab-case so search slugs stay predictable. Favor function components, hooks, and Tailwind utility classes; let ESLint autofix formatting issues and keep shared constants in `util/`.
Use TypeScript with 2-space indentation and ES module imports. Keep React components PascalCase and helpers camelCase. Markdown filename stems are exact document names used by `?name=` links; follow existing title-case names and preserve filenames to avoid breaking links. Favor function components, hooks, and Tailwind utility classes; let ESLint autofix formatting issues and keep shared constants in `util/`.
## Testing Guidelines
No unit-test runner ships with this project, so treat `npm run lint` and `npm run build` as required gates. When adding interactive behavior, create targeted Playwright or Cypress checks under a `__tests__/` directory and describe how to run them in the PR. Always smoke-test the search panel locally after updating Markdown to confirm newly generated `docs.json` entries render.
+2 -2
View File
@@ -11,7 +11,7 @@ npm install
npm run dev
```
**NOTE:** The markdown is compiled into a static JSON object at build time. This means if you edit a `.md` file you will need to restart the dev server to see your changes.
Markdown is compiled into JSON for the site. `npm run dev` watches Markdown files and regenerates this content automatically.
### Offline Docs
@@ -27,7 +27,7 @@ This will produce a `www.zip` file that contains the static html and JavaScript
```bash
unzip -o www.zip
cd out/
python -m http.server 8000
python3 -m http.server 8000
```
Then open your browser to `http://localhost:8000/` to view the documentation.
+1 -1
View File
@@ -304,7 +304,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
a(anchorProps) {
const { href, children, className, ...rest } = anchorProps;
if (href?.startsWith("/")) {
if (href?.startsWith("/") || href?.startsWith("#")) {
return (
<a
{...rest}
+1051 -1036
View File
File diff suppressed because it is too large. Load diff
+30 -30
View File
@@ -13,48 +13,48 @@
"postinstall": "node ./prebuild/run-prebuild.js"
},
"dependencies": {
"@fontsource/fira-code": "^5.2.7",
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^3.3.1",
"@heroui/react": "^3.0.3",
"@heroui/styles": "^3.0.3",
"@fontsource/fira-code": "^5.3.0",
"@fortawesome/fontawesome-svg-core": "^7.3.1",
"@fortawesome/free-brands-svg-icons": "^7.3.1",
"@fortawesome/free-solid-svg-icons": "^7.3.1",
"@fortawesome/react-fontawesome": "^3.5.0",
"@heroui/react": "^3.2.6",
"@heroui/styles": "^3.2.6",
"@monaco-editor/react": "^4.7.0",
"@tailwindcss/postcss": "^4.2.4",
"@tanstack/react-query": "^5.100.6",
"asciinema-player": "^3.15.1",
"fuse.js": "^7.3.0",
"@tailwindcss/postcss": "^4.3.3",
"@tanstack/react-query": "^5.104.1",
"asciinema-player": "^3.17.0",
"fuse.js": "^7.5.0",
"lunr": "^2.3.9",
"mermaid": "^11.16.1",
"next": "^16.2.6",
"mermaid": "^11.17.2",
"next": "^16.3.8",
"next-themes": "^0.4.6",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.1",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/typography": "^0.5.20",
"@types/lunr": "^2.3.7",
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/node": "^25.9.9",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@types/react-syntax-highlighter": "^15.5.13",
"autoprefixer": "^10.5.0",
"concurrently": "^9.2.1",
"eslint": "^9.39.4",
"eslint-config-next": "^16.2.4",
"postcss": "^8.5.13",
"tailwindcss": "^4.2.4",
"autoprefixer": "^10.6.1",
"concurrently": "^9.2.4",
"eslint": "^9.39.5",
"eslint-config-next": "^16.3.8",
"postcss": "^8.5.28",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3"
},
"overrides": {
"dompurify": "^3.4.13",
"postcss": "^8.5.13",
"sharp": "^0.35.3",
"shell-quote": "^1.8.4",
"uuid": "^14.0.0"
"dompurify": "^3.4.16",
"postcss": "^8.5.28",
"sharp": "^0.35.5",
"shell-quote": "^1.12.0",
"uuid": "^14.0.2"
}
}
+5 -5
View File
@@ -121,7 +121,7 @@ const DocsIndexPage: NextPage = () => {
<Head>
<title>{name ? `${name} · Sliver Docs` : "Sliver Documentation"}</title>
</Head>
<div className="mx-auto w-full max-w-[90rem] px-4 pt-8 sm:px-6 lg:px-8 lg:pt-10">
<div className="w-full px-4 pt-8 sm:px-6 lg:px-8 lg:pt-10">
<div className="mb-8 lg:hidden">
<div className="flex items-center gap-3">
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
@@ -202,10 +202,10 @@ const DocsIndexPage: NextPage = () => {
<div
key={name || "documentation-overview"}
className="min-w-0 pb-16 lg:pr-4"
className="min-w-0 pb-16"
>
{name !== "" ? (
<article className="mx-auto w-full max-w-4xl">
<article className="w-full min-w-0">
<header className="mb-8 border-b border-separator/70 pb-8">
<p className="text-sm font-medium text-accent">Reference</p>
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
@@ -219,11 +219,11 @@ const DocsIndexPage: NextPage = () => {
/>
</article>
) : (
<div className="mx-auto w-full max-w-5xl pt-4 lg:pt-8">
<div className="w-full min-w-0 pt-4 lg:pt-8">
<h1 className="text-sm font-medium text-accent">
Sliver reference
</h1>
<p className="mt-4 max-w-2xl text-lg leading-8 text-muted">
<p className="mt-4 text-lg leading-8 text-muted">
Browse configuration, transports, extensions, payloads, and
troubleshooting guidance for the Sliver framework.
</p>
@@ -65,7 +65,7 @@ From an end-user perspective there's not much of a difference between the two, e
An alias is essentially just a thin wrapper around the existing `sideload` and `execute-assembly` commands, and aliases cannot have dependencies.
An extension is a shared library that is reflectively loaded into the Sliver implant process, and is passed several callbacks to return data to the implant. As such these extensions must implement the Sliver API. Extensions may also have dependencies, which are other extensions. For example, the COFF Loader is a DLL extension that loads and executes BOFs, in turn BOFs simply extensions that rely on the COFF Loader as a dependency. These types of extensions do not need to implement any Sliver-specific API, since the Sliver API is abstracted by their dependency.
An extension can be a native shared library or a BOF. Shared libraries are reflectively loaded into the Sliver implant process and must implement the Sliver extension API to return output. BOFs use Beacon callbacks and can execute through the built-in Reflektor executor. The COFF Loader DLL remains available as a dependency for legacy Windows BOF execution. For supported platforms and basic examples, see [Cross-platform BOFs](/docs?name=Cross-platform+BOFs).
## Aliases
@@ -170,6 +170,6 @@ As the alias support relies on Sliver side loading capabilities, please make sur
Native extensions are supported by Windows implants (`386`, `amd64`, and `arm64`), Linux implants (`386`, `amd64`, and `arm64`), and macOS implants (`amd64` and `arm64`). Each manifest must include a platform-native shared library for every target it supports: `.dll` on Windows, `.so` on Linux, and `.dylib` on macOS.
Extensions are similar in structure to an alias, but internally work differently. An extension is an artifact of native code that is reflectively loaded by the implant and passed certain callbacks. These callbacks allow the extension to return data to the C2 server. Extensions may also have dependencies (other extensions), which Sliver will load prior to the extension; circular dependencies are not allowed. For example, all BOF extensions (`.o` files) rely on the COFF Loader extension (a `.dll`).
Extensions are similar in structure to an alias, but internally work differently. An extension is an artifact of native code that is reflectively loaded by the implant and passed certain callbacks. These callbacks allow the extension to return data to the C2 server. Extensions may also have dependencies (other extensions), which Sliver will load prior to the extension; circular dependencies are not allowed. BOF extensions can use built-in Reflektor execution without a loader dependency, or retain a COFF Loader dependency for legacy Windows execution. See [BOF and COFF Support](/docs?name=BOF+and+COFF+Support) for executor selection and fallback behavior.
Native exports use the same Sliver extension ABI on every supported operating system. The export receives the argument buffer, its size, and a callback used to return output. The optional `init` export in the extension manifest is called once after the library is loaded and before any command export is invoked.
+2 -2
View File
@@ -1,6 +1,6 @@
The armory is the Sliver Alias and Extension package manager. It allows you to automatically install various third-party tools such as BOFs and .NET tooling.
The armory downloads packages from `github.com` and `api.github.com` so you'll need an internet connection in order for the command to work. The command does support proxies (see `--help`) and after an alias or extension is installed an internet connection is not required to execute the alias/extension.
The official armory downloads packages from `github.com` and `api.github.com` so you'll need an internet connection in order for the command to work. The command does support proxies (see `--help`) and after an alias or extension is installed an internet connection is not required to execute the alias/extension.
Aliases and extensions are installed on the "sliver client"-side, and thus are not shared among operators in [multiplayer mode](/docs?name=Multi-player+Mode).
@@ -50,4 +50,4 @@ If you also copied a package into the server-side AI store, remove that copy sep
## Private Armories
Sliver has experimental support for self-hosted private armories, but I have not gotten around to testing and writing the documentation for these, so you'll have to read through the source code to figure out how they work for now. I'll eventually update this documentation and release a reference implementation. If you do play around with this, just know the design is subject to change before becoming a real feature.
See [Private Armories](/docs?name=Private+Armories) for hosting options, signing and authentication, and client configuration using the standalone [Private Armory](https://github.com/sliverarmory/private-armory) service.
+6 -4
View File
@@ -1,10 +1,12 @@
Sliver keeps an audit log of every command and its arguments executed by the server (including commands executed by operators in multiplayer mode), as well as most events (such as a new session or beacon connecting to the server). The audit log's intended use is for after-action analysis; providing a detailed history of the entire engagement, including which commands were executed on which hosts when. This will include any commands executed by any operator on any session. Note some console commands only perform actions on the "client-side" and may not appear in the audit log, but will still appear in the client's command history. Additionally, interactive commands (e.g., `shell`) may not appear in the logs aside from the initial usage of the `shell` command.
Sliver's audit log records server-side unary gRPC requests, including multiplayer operator requests, plus session and beacon registrations. It supports after-action analysis, but request entries are written before execution and do not confirm success. Client-only actions and interactive command input (e.g., within `shell`) are not recorded in the audit log.
By default the audit log is located on the server at: `~/.sliver/logs/audit.json`. However, this can be changed by modifying the [`SLIVER_ROOT_DIR`](/docs?name=Environment+Variables) environment variable.
By default, logs rotate at 50 MB and retain up to 20 compressed backups for at most 90 days. These limits are configurable under `logs` in `server.yaml`.
#### Parsing Audit Logs
The audit log is stored in a newline delimited (one object per line) nested-JSON format designed to be primarily machine readable, an example entry is shown below:
The audit log uses a newline delimited (one object per line), nested-JSON format. A simplified RPC entry is shown below:
```
{"level":"info","msg":"{\"request\":\"{\\\"Port\\\":8888}\",\"method\":\"/rpcpb.SliverRPC/StartMTLSListener\"}","time":"2021-06-16T10:22:54-05:00"}
@@ -14,6 +16,6 @@ The audit log is stored in a newline delimited (one object per line) nested-JSON
The top level JSON should always contain:
- `level` - The level indicates the type of action performed. Currently, `info` indicates commands and `warn` indicates events.
- `msg` - A JSON object encoded as a string. This object should always contain a `request` and a `method`. The contents of `method` indicate which command was executed, `request` will contain parameters to that command. The contents of `request` will vary depending on the command, but it will be based on the corresponding gRPC/Protobuf message.
- `level` - Currently, `info` indicates RPC requests and `warn` indicates registration events.
- `msg` - A JSON object encoded as a string. RPC entries contain `request` (another JSON-encoded string), `method`, `user`, `remote_ip`, and optional `session` or `beacon` details. Registration events instead contain `Session` or `Beacon` and `Register`, without `request` or `method`.
- `time` - The server's timestamp of the log entry
@@ -1,5 +1,7 @@
Sliver supports the loading and execution of BOFs and COFFs, generally no code changes are needed to use BOFs with a Sliver implant. However, you may need to define a manifest file so that Sliver is aware of BOF arguments and their types.
For platform formats, supported architectures, and basic C examples for Windows, macOS, and Linux, see [Cross-platform BOFs](/docs?name=Cross-platform+BOFs).
### BOF Extensions
New Sliver implants execute BOFs with the built-in [Reflektor](https://github.com/sliverarmory/reflektor) executor by default. Armory packages can retain a [COFF Loader](https://github.com/sliverarmory/COFFLoader) dependency as a compatibility fallback for older clients, servers, and implants; Armory installs that dependency automatically.
@@ -23,6 +23,7 @@
- https://blog.ecapuano.com/p/so-you-want-to-be-a-soc-analyst-20
- https://wsummerhill.github.io/redteam/2023/07/25/Sliver-C2-Usage-for-Red-Teams.html
- https://rootsecdev.medium.com/hacking-active-directory-with-sliver-c2-19d7ceabbf13
- [HackITA — Sliver C2: Guide to Listeners, Beacons, Implants and Armory](https://en.hackita.it/articoli/silver/)
- https://dev.to/living_syn/sliver-and-cursed-chrome-for-post-exploitation-4gnk
### Opsec Blogs
@@ -1,8 +1,8 @@
You'll want to compile Sliver from a MacOS or Linux machine. Compiling from native Windows is possible in theory, but none of the scripts are designed to run on Windows. You can cross-compile the Windows server/client binaries from a better operating system like Linux or MacOS. If you only have a Windows machine, you should be able to use the Windows Subsystem for Linux (WSL).
Sliver can be compiled on Linux, macOS, and native Windows. Windows users can also build through the Windows Subsystem for Linux (WSL).
To compile from source you'll need:
- Go v1.27.1 or later
- Go v1.27.3 or later
- `make` (on MacOS you may need to install XCode and accompanying cli tools)
```asciinema
@@ -26,7 +26,7 @@ $ make
This will create `sliver-server` and `sliver-client` binaries.
Sliver embeds its own copy of the Go compiler and a few internal tools. The first time you run `make`, a bash script will download these assets to your local system. This means the first build will take longer than subsequent builds, especially if your internet connection is slow.
Sliver embeds its own copy of the Go compiler and a few internal tools. The first time you run `make`, a Go asset tool will download these assets to your local system. This means the first build will take longer than subsequent builds, especially if your internet connection is slow.
### Cross-compile to Specific Platforms
@@ -56,23 +56,25 @@ docker build --target production -t sliver .
{"src": "/asciinema/sliver-docker-production.cast", "cols": "132"}
```
The Docker build includes mingw and Metasploit, so it can take a while to build from scratch but Docker should cache the layers effectively. Sliver will also run its unit tests as part of the build, and that takes a few minutes too.
The production Docker image includes Metasploit, so it can take a while to build from scratch, but Docker caches the layers. Unit tests run only when building the separate `test` target.
### Windows Builds
If all you have is a Windows machine, the easiest way to build Sliver is using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) and following the Linux/cross-compile instructions above. To cross-compile a Windows binary use `make windows-amd64` and copy it to your Windows file system (i.e. `/mnt/c/Users/foo/Desktop`) and, you will need to run it using a terminal that supports ANSI sequences such as the [Windows Terminal](https://github.com/microsoft/terminal).
For a native Windows build, install Go, Git, and GNU Make, then run `make windows-amd64` from the repository root. Alternatively, use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) and follow the Linux/cross-compile instructions above. Run the resulting binaries in a terminal that supports ANSI sequences, such as [Windows Terminal](https://github.com/microsoft/terminal).
# Developers
If you want to modify any of the `.proto` files you'll need to set up a few additional tools to regenerate the `.pb.go` files.
- Protoc libprotoc v26.1 or later
- Protoc-gen-go v1.27.1
- Protoc-gen-go-grpc v1.2.0
The checked-in bindings record these tool versions:
- `protoc` v7.36.2 (as reported in the generated headers)
- `protoc-gen-go` v1.36.11
- `protoc-gen-go-grpc` v1.6.1
#### `protoc`
First install your platform's version of `protoc` v3.19.4 or later:
Install a compatible `protoc` release for your platform:
https://github.com/protocolbuffers/protobuf/releases/latest
@@ -83,8 +85,8 @@ Ensure that the correct `protoc` version is on your `$PATH`, you can check with
Assuming `$GOPATH/bin` is on your `$PATH` simply run the following commands to install the appropriate versions of `protoc-gen-go` and `protoc-gen-go-grpc`:
```
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.1
```
Ensure that these are both on your `$PATH` after running the commands; if not, you probably need to add `$GOPATH/bin` to your `$PATH`. To regenerate the Protobuf and gRPC files run:
@@ -4,6 +4,8 @@ The Sliver server configuration file is located in the `configs` sub-directory o
If no configuration file exists, a default YAML configuration is generated and written to disk on startup. If a legacy `server.json` file exists, Sliver reads it, writes `server.yaml`, and renames the old file to `.server.json-old`.
AI configuration is stored separately in `configs/ai.yaml`. A legacy inline `ai` block is migrated only when that file does not already exist.
#### Default Server Config
```yaml
@@ -18,20 +20,14 @@ logs:
grpc_unary_payloads: false
grpc_stream_payloads: false
tls_key_logger: false
max_size_mb: 50
max_backups: 20
max_age_days: 90
compress: true
grpc:
keepalive:
min_time_seconds: 30
permit_without_stream: true
ai:
provider: ""
model: ""
thinking_level: ""
anthropic:
api_key: ""
base_url: ""
openai:
api_key: ""
base_url: ""
watch_tower: null
go_proxy: ""
http_default:
@@ -61,15 +57,11 @@ cxx: {}
- `grpc_unary_payloads` - Log gRPC unary payloads.
- `grpc_stream_payloads` - Log gRPC streaming payloads.
- `tls_key_logger` - Enable TLS key logging (for debugging/traffic analysis only; sensitive).
- `max_size_mb`, `max_backups`, `max_age_days` - Log rotation size in MB, backup count, and maximum backup age in days (defaults: `50`, `20`, `90`).
- `compress` - Compress rotated logs (default: `true`).
- `grpc.keepalive` - gRPC keepalive enforcement configuration.
- `min_time_seconds` - Minimum time (seconds) between client pings before sending GOAWAY (`too_many_pings`).
- `permit_without_stream` - Allow client pings when there are no active streams.
- `ai` - Optional server-side AI defaults and provider credentials.
- `provider` - Default AI provider (`openai` or `anthropic`).
- `model` - Optional default model identifier.
- `thinking_level` - Optional reasoning level (for example `low`, `medium`, `high`, or `disabled`).
- `anthropic.api_key` / `openai.api_key` - Provider API key.
- `anthropic.base_url` / `openai.base_url` - Optional provider API endpoint override.
- `watch_tower` - Optional API keys for Watchtower integrations.
- `vt_api_key` - VirusTotal API key.
- `xforce_api_key` - IBM X-Force API key.
@@ -0,0 +1,194 @@
Sliver's built-in [Reflektor](https://github.com/sliverarmory/reflektor/tree/v0.0.8) executor runs Beacon Object Files (BOFs) on Windows, macOS, and Linux. A BOF is a relocatable native object with an entry point such as `go(char *args, int length)` and Beacon callbacks for arguments and output. It executes inside the implant process.
This page covers writing and compiling basic BOFs. For Armory installation, argument manifests, and compatibility with older components, see [BOF and COFF Support](/docs?name=BOF+and+COFF+Support). Projects with CNA wrappers can also use [OPFOR](/docs?name=OPFOR).
## Platform support
The following targets support built-in BOF execution in Sliver:
| Implant OS | Implant architecture (`GOARCH`) | Object format | Typical compiler |
| --- | --- | --- | --- |
| Windows (`windows`) | `386`, `amd64`, `arm64` | COFF relocatable object | MinGW-w64 GCC or Clang targeting Windows |
| macOS (`darwin`) | `amd64`, `arm64` | Mach-O relocatable object (`MH_OBJECT`) | Apple Clang |
| Linux (`linux`) | `386`, `amd64`, `arm64` | ELF relocatable object (`ET_REL`) | GCC or Clang targeting Linux |
The object must match the **implant's** operating system and CPU architecture, regardless of the client or server platform. `amd64` means x86-64; `386` means x86; `arm64` means AArch64. The `.o` suffix alone does not identify the format. Compile a separate artifact for each OS/architecture pair; renaming a Windows COFF object does not turn it into a Linux or macOS BOF.
Reflektor also accepts legacy ELF BOFs on macOS when they match the host architecture and ABI. Use native Mach-O for new macOS BOFs. Reflektor's standalone library supports additional targets, but the table above describes Sliver's built-in integration.
Use compatible client, server, and implant versions with the `bof_v1` capability. Setting `"bof_executor": "reflektor"` explicitly selects built-in execution when that capability is available. The examples below require this support and do not need a COFF Loader dependency. See [BOF and COFF Support](/docs?name=BOF+and+COFF+Support) for the optional legacy fallback.
## Basic examples
These three examples take no arguments and emit one message through `BeaconOutput`. Save the source files in a directory named `hello-bof`. Run the compiler commands from that directory; each command produces an object with `-c` rather than linking an executable or shared library.
The declarations are included directly to keep the examples self-contained. Windows imports use `__declspec(dllimport)`; Unix platforms use normal external symbols. The supported targets use 32-bit `int` for the entry-point length and Beacon callback parameters.
### Windows: COFF
Save as `hello-windows.c`:
```c
__declspec(dllimport) void BeaconOutput(int type, char *data, int length);
void go(char *args, int length) {
static char message[] = "Hello from a Windows BOF!\n";
(void)args;
(void)length;
BeaconOutput(0, message, (int)(sizeof(message) - 1));
}
```
With a MinGW-w64 cross compiler, build the Windows x86-64 object:
```bash
x86_64-w64-mingw32-gcc -Os -c -fno-stack-protector \
-fno-unwind-tables -fno-asynchronous-unwind-tables \
hello-windows.c -o hello.windows.amd64.o
```
For a Windows x86 implant, use `i686-w64-mingw32-gcc` with the same flags and name the result `hello.windows.386.o`. For Windows ARM64, Clang can compile this header-free example:
```bash
clang --target=aarch64-w64-windows-gnu -Os -c -fno-stack-protector \
-fno-unwind-tables -fno-asynchronous-unwind-tables \
hello-windows.c -o hello.windows.arm64.o
```
Existing Windows BOFs may import system functions using names such as `KERNEL32$GetCurrentProcessId`. Those imports and their calling conventions remain Windows-specific; port the source and imports before compiling for another OS.
### macOS: Mach-O
Save as `hello-macos.c`:
```c
extern void BeaconOutput(int type, char *data, int length);
void go(char *args, int length) {
static char message[] = "Hello from a macOS BOF!\n";
(void)args;
(void)length;
BeaconOutput(0, message, (int)(sizeof(message) - 1));
}
```
On macOS, install the Xcode Command Line Tools (`xcode-select --install`) if Clang is unavailable. Build separate objects for Apple Silicon and Intel implants:
```bash
clang -arch arm64 -Os -c -fno-stack-protector \
-fno-unwind-tables -fno-asynchronous-unwind-tables \
hello-macos.c -o hello.darwin.arm64.o
clang -arch x86_64 -Os -c -fno-stack-protector \
-fno-unwind-tables -fno-asynchronous-unwind-tables \
hello-macos.c -o hello.darwin.amd64.o
```
Keep these as separate thin Mach-O objects. Reflektor does not accept a universal/fat BOF object. Clang prefixes the C symbols with `_` in Mach-O; the default `go` entry-point selection handles `_go` automatically.
**Apple Silicon:** Native Mach-O `arm64` BOFs must use `BeaconOutput` for output. Reflektor rejects `BeaconPrintf` and `BeaconFormatPrintf` imports for this format because Apple's variadic calling convention differs from the callback bridge. The example above works on both macOS architectures.
### Linux: ELF
Save as `hello-linux.c`:
```c
extern void BeaconOutput(int type, char *data, int length);
void go(char *args, int length) {
static char message[] = "Hello from a Linux BOF!\n";
(void)args;
(void)length;
BeaconOutput(0, message, (int)(sizeof(message) - 1));
}
```
On an x86-64 Linux build host, use GCC to produce an ELF object:
```bash
gcc -m64 -Os -c -fPIC -fno-stack-protector \
-fno-unwind-tables -fno-asynchronous-unwind-tables \
hello-linux.c -o hello.linux.amd64.o
```
For a Linux x86 implant, replace `-m64` with `-m32` and name the result `hello.linux.386.o`; this requires a compiler with 32-bit target support. For ARM64, use a Linux AArch64 cross compiler:
```bash
aarch64-linux-gnu-gcc -Os -c -fPIC -fno-stack-protector \
-fno-unwind-tables -fno-asynchronous-unwind-tables \
hello-linux.c -o hello.linux.arm64.o
```
On a native ARM64 Linux build host, `gcc` with the same flags as the ARM64 command produces the ARM64 object. Do not use a macOS host's default `clang` or `gcc` command to build a Linux BOF without selecting a Linux target.
## Load and run in Sliver
Save the following `extension.json` beside the compiled objects in `hello-bof`. The command uses Sliver's modern manifest format, with execution settings inside `commands`. Keep only the `files` entries for artifacts you have built; add `386` or Windows `arm64` entries if you built those variants.
```json
{
"name": "hello-bof",
"version": "1.0.0",
"extension_author": "Example Author",
"commands": [
{
"command_name": "hello-bof",
"help": "Print a greeting from a platform-native BOF",
"bof_executor": "reflektor",
"entrypoint": "go",
"files": [
{
"os": "windows",
"arch": "amd64",
"path": "hello.windows.amd64.o"
},
{
"os": "darwin",
"arch": "amd64",
"path": "hello.darwin.amd64.o"
},
{
"os": "darwin",
"arch": "arm64",
"path": "hello.darwin.arm64.o"
},
{
"os": "linux",
"arch": "amd64",
"path": "hello.linux.amd64.o"
},
{
"os": "linux",
"arch": "arm64",
"path": "hello.linux.arm64.o"
}
],
"arguments": []
}
]
}
```
Copy the directory to your operator machine if you compiled on another host, then load it into the Sliver client:
```text
sliver > extensions load /absolute/path/to/hello-bof
sliver > use <session-or-beacon-id>
sliver (selected-target) > hello-bof
```
Sliver selects the artifact using the active session or beacon's exact `os`/`arch` pair. A macOS ARM64 target prints `Hello from a macOS BOF!`; Windows and Linux targets print the corresponding greeting. Sessions return output immediately; beacon output arrives with the asynchronous task result.
For BOFs with arguments, declare them in the manifest in the same order and types consumed by `BeaconDataParse`, `BeaconDataInt`, `BeaconDataShort`, and `BeaconDataExtract`. Sliver packs the arguments into the length-prefixed Beacon format. See the argument type table and conversion example in [BOF and COFF Support](/docs?name=BOF+and+COFF+Support).
## Compatibility and troubleshooting
Reflektor implements the Beacon data, format, and output callbacks, including `BeaconOutput` and supported `BeaconPrintf` calls. It preserves output channel values such as normal output (`0`) and errors (`13`). It does not implement every Beacon integration API: token manipulation, temporary-process, and process-injection callbacks require host integration and fail explicitly when unavailable. Check the [Reflektor BOF documentation](https://github.com/sliverarmory/reflektor/blob/v0.0.8/README.md#beacon-object-files) before importing additional Beacon APIs.
- **No artifact for the target:** Add the exact `files` entry with Go OS/architecture names, such as `darwin`/`arm64`, and the matching object. Paths are relative to `extension.json`; parent-directory traversal is not allowed.
- **Missing built-in support:** Upgrade the client and server and regenerate the implant with BOF support. An older running implant does not gain the capability when only the client is upgraded.
- **Wrong format or machine:** Inspect the object with `file`, `llvm-readobj`, or `objdump`. Check both the OS format and architecture, then rebuild for the implant.
- **Unresolved import:** Use the correct platform ABI and symbol spelling. Windows DLL imports do not carry over to Unix. The examples avoid standard-library and system-library dependencies.
- **Entry point not found:** Export a non-static `go` function with the signature shown above. C++ sources must use `extern "C"` to avoid name mangling. Custom entry points use the exact object symbol name, including Mach-O's leading underscore.
BOFs run native code in the implant process. Keep the entry point short and return normally; crashes or process-exit calls can terminate the implant. Worker threads using Beacon callbacks must finish before the entry point returns because output capture and object lifetime are tied to that execution.
@@ -1,4 +1,4 @@
This script installs the latest version of Sliver as a systemd service, installs Windows cross-compiler dependencies (mingw), and sets up multiplayer for all local users. After running the script, connect locally by running `sliver`.
This script installs the latest version of Sliver as a systemd service and sets up multiplayer for local users. After running the script, connect locally by running `sliver`.
https://sliver.sh/install
@@ -13,7 +13,6 @@ curl https://sliver.sh/install|sudo bash
```
- Installs server binary to `/root/sliver-server`
- Installs mingw
- Runs the server in daemon mode using systemd
- Installs client to `/usr/local/bin/sliver`
- Generates multiplayer configurations for all users with a `/home` directory
@@ -134,7 +134,7 @@ services:
secret_key: "$AWS_SECRET_ACCESS_KEY"
region: "us-east-1"
receivers:
- "https://sns.us-east-1.amazonaws.com/123456789012/my-topic"
- "arn:aws:sns:us-east-1:123456789012:my-topic"
```
##### Bark
@@ -239,13 +239,7 @@ services:
##### Line Notify
```yaml
services:
line_notify:
enabled: true
receivers:
- "$LINE_NOTIFY_TOKEN"
```
LINE Notify [ended service on March 31, 2025](https://developers.line.biz/en/news/2025/04/01/line-notify/); its APIs and the `line_notify` integration are no longer usable.
##### Mail (SMTP)
@@ -304,6 +298,8 @@ services:
##### Microsoft Teams
Use a Teams Workflows webhook. Legacy Office 365 connector webhooks [were retired in May 2026](https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/).
```yaml
services:
msteams:
@@ -0,0 +1,56 @@
[Private Armory](https://github.com/sliverarmory/private-armory) is a standalone service for hosting your own Sliver aliases, extensions, and bundles. Each Sliver client connects to it independently; installed packages are still local to that client, as described in [Armory](/docs?name=Armory).
The upstream wiki sometimes calls the project `external-armory`. The current repository is `private-armory`, and its executable is `armory-server`.
## Hosting
The service can run as a standalone executable or in a container. Follow the upstream [Setup](https://github.com/sliverarmory/private-armory/wiki/Setup) or [Docker](https://github.com/sliverarmory/private-armory/wiki/Docker) guide for deployment details.
- Configuration is stored in `<armory-root>/config.json`; restart the service after editing it.
- The supplied Compose configuration publishes port `8888` and persists `./armory-data` at `/data/armory-data`.
- Review TLS and authentication settings before deployment. The supplied `armory.env` disables TLS; HTTPS requires a certificate and private key. Verify the effective authentication settings for the version you deploy rather than assuming the example enables access control.
- Initial setup displays client connection information and a separate administration token. Give clients the client token, never the administration token or a signing private key.
See [Environment Variables](https://github.com/sliverarmory/private-armory/wiki/Environment-Variables) for configuration options. Older wiki JSON examples may differ from the current service configuration.
## Storage and Signing
The [storage providers](https://github.com/sliverarmory/private-armory/wiki/Storage-Providers) support a writable local filesystem or an S3 general-purpose bucket. Preserve the configured storage across restarts and container replacements.
Package and index signatures use Minisign. The [signing key providers](https://github.com/sliverarmory/private-armory/wiki/Package-Signing-Key-Providers) are:
| Provider | Key location |
| --- | --- |
| `local` | A private key file in the armory root. |
| `aws` | AWS Secrets Manager. |
| `vault` | HashiCorp Vault. |
| `external` | A separate signing process; the serving application holds the public key. |
With `external`, signing happens outside the service and package administration through its API is disabled. See [Standalone Signing](https://github.com/sliverarmory/private-armory/wiki/Standalone-Signing) and [Package Administration](https://github.com/sliverarmory/private-armory/wiki/Package-Administration) for the respective workflows.
Obtain the Minisign public key through a trusted channel. It establishes trust in the index and package signing metadata; it is separate from the TLS certificate. A valid signature does not establish that a package is safe to use.
## Client Configuration
Obtain the index URL, Minisign public key, and client authorization token from your armory administrator. The service's index URL ends in `/armory/index`; use the configured hostname, port, and HTTPS endpoint.
```text
sliver > armory add private --url https://armory.example.com:8888/armory/index --pubkey <MINISIGN_PUBLIC_KEY> --auth <CLIENT_TOKEN>
sliver > armory info
```
Replace the placeholders with your armory's values. `--auth` supplies the complete `Authorization` header value; Sliver does not add a `Bearer` prefix. Adding a source validates its index before saving it.
Configuration is saved to `~/.sliver-client/armories.json` by default, or under `SLIVER_CLIENT_ROOT_DIR` when set. Authorization values are stored in plaintext, and detailed `armory info <name>` output can display them; protect the file and redact credentials before sharing output.
## Managing Sources
| Command | Effect |
| --- | --- |
| `armory info` | List configured sources. |
| `armory refresh` | Reload saved source configuration and refresh cached metadata. |
| `armory disable private` / `armory enable private` | Disable or enable the private source. |
| `armory rm private` | Remove its source registration. |
| `armory save` | Save the current source configuration. |
Add and modify operations save by default. Changes made with `--no-save` are temporary and are lost on refresh or restart unless saved first. Disabling or removing a source does not uninstall packages already installed from it.
+36 -27
View File
@@ -69,7 +69,7 @@ export default function Home() {
</div>
<Card className="h-full bg-surface/80 shadow-surface backdrop-blur-2xl lg:col-span-5">
<Card.Header className="pb-3">
<Card.Header>
<div className="min-w-0">
<p className="text-sm font-medium text-accent">
Operator documentation
@@ -80,7 +80,7 @@ export default function Home() {
</div>
</Card.Header>
<Card.Content className="pt-2">
<Card.Content>
<p className="text-pretty text-base leading-7 text-muted">
Sliver is a cross-platform command and control framework for
professional red teams. Operate over mTLS, WireGuard, HTTP(S),
@@ -94,39 +94,48 @@ export default function Home() {
</Card.Content>
<Card.Footer className="mt-auto flex-col items-stretch gap-3 pt-5">
<Button
fullWidth
variant="primary"
onPress={() =>
router.push({
pathname: "/docs",
query: { name: "Getting Started" },
})
}
>
Getting started
</Button>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Button
fullWidth
variant="primary"
onPress={() =>
router.push({
pathname: "/docs",
query: { name: "Getting Started" },
})
}
>
Get started
</Button>
<Button
className="[--button-bg-hover:var(--color-purple-700)] [--button-bg-pressed:var(--color-purple-700)] [--button-bg:var(--color-purple-600)] [--button-fg:var(--color-white)]"
fullWidth
variant="tertiary"
onPress={() => {
window.open(
"https://github.com/BishopFox/sliver/releases/latest",
"_blank",
"noopener,noreferrer",
);
}}
<a
className={buttonVariants({
fullWidth: true,
variant: "secondary",
})}
href="https://github.com/BishopFox/sliver/releases/latest"
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={faDownload} />
Latest release
</Button>
</a>
<a
className={buttonVariants({
fullWidth: true,
variant: "secondary",
})}
href="https://github.com/sliverarmory/sliver-gui/releases/latest"
target="_blank"
rel="noopener noreferrer"
>
<SliversIcon height={16} width={16} />
Sliver GUI
</a>
</div>
<a
className={buttonVariants({
fullWidth: true,
variant: "secondary",
variant: "ghost",
})}
href="https://github.com/sliverarmory"
target="_blank"
@@ -1,6 +1,6 @@
Sliver implants support two types of connections, sessions and beacons.
Sessions use long-polling connections, which means they constantly maintain a single TCP connection open while communicating with the server. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.
Sessions provide interactive communication with the server; connection and polling behavior depend on the selected transport. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.
Typically during an engagement you will want to deploy a beacon on the target system and switch to a session while doing more active enumeration.
@@ -53,7 +53,7 @@ As you can see Sliver ran the Seatbelt assembly and provided us with the output
## Bof’s
Beacon object files are loaded using trustedsec’s coffloader. When you run a bof command the loader will first be loaded into memory and is used to run whichever bof you choose. From an operator’s perspective bof’s are similar to basic sliver commands.
The transcript below shows the legacy COFF Loader path. Current Sliver supports built-in Reflektor execution, with legacy loader compatibility depending on the package and component versions. See [BOF and COFF Support](/docs?name=BOF+and+COFF+Support) for current behavior.
```bash
[server] sliver (UNABLE_PRIDE) > sa-whoami
+1 -1
View File
@@ -1 +1 @@
{"tutorials":[{"name":"1 - Getting Started","content":"`sliver-server` is the binary you want to use to run the Sliver C2 server, `sliver-client` is solely a client to connect to a Sliver C2 server. Sliver server also acts as a client on its own, so you don’t necessarily run sliver server and client separately.\n\nFirst time running Sliver will take a couple seconds as it's retrieving its dependencies. Consecutive executions will be much faster. Go ahead and launch the `sliver-server`.\n\n```asciinema\n{\"src\": \"/asciinema/startup.cast\", \"cols\": \"132\", \"rows\": \"28\", \"idleTimeLimit\": 8}\n```\n\nLet's take a couple minutes to discuss what Sliver actually is and how it's set up.\n\n![Alt text](/images/Architecture.png)\n\nNow that Sliver is running, lets generate and execute your first implant to try out some of the basic features of Sliver, for now we’re going to run everything on the local host.\n\nHere's what we're going to do: \n* Generate your implant using the `generate` command as shown below.\n* Start HTTP listener on port 80\n* Execute implant in a separate terminal\n\n```asciinema\n{\"src\": \"/asciinema/first-implant.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNow let’s select our implant and run our first command using the `use` command.\n\n```bash\n[server] sliver > use\n? Select a session or beacon: \nSESSION 1884a365 RELATED_EARDRUM [::1]:49153 test.local tester darwin/amd64\n[*] Active session RELATED_EARDRUM (1884a365-085f-4506-b28e-80c481730fd0)\n\n[server] sliver (RELATED_EARDRUM) > pwd\n\n[*] /Users/tester/tools\n```\n\nOnce you have reached this point, go ahead and explore some of the commands listed below. In each case, first check out the command's help using the **`-h`** flag then try it out!\n\n```bash\nExploring and interacting with the filesystem\n\nFilesystem\n cat Dump file to stdout\n cd Change directory\n cp Copy a file\n download Download a file\n grep Search for strings that match a regex within a file or directory\n head Grab the first number of bytes or lines from a file\n ls List current directory\n mkdir Make a directory\n mv Move or rename a file\n pwd Print working directory\n rm Remove a file or directory\n tail Grab the last number of bytes or lines from a file\n upload Upload a file\n```\n\n```asciinema\n{\"src\": \"/asciinema/filesystem.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nGetting some environmental information\n```bash\nInfo\n env List environment variables\n getgid Get session process GID\n getpid Get session pid\n getuid Get session process UID\n info Get session info\n ping Send round trip message to implant (does not use ICMP)\n whoami Get session user execution context\n```\nExecute a binary\n\n```asciinema\n{\"src\": \"/asciinema/execute.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nRunning an interactive shell\n\n```asciinema\n{\"src\": \"/asciinema/shell.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNot all commands are supported for all operating systems or meant to be used on all operating systems, here are a few examples:\n\n```\nmount Get information on mounted filesystems (only for Windows and Linux)\nscreenshot Take a screenshot (only for Windows and Linux)\nmemfiles List current memfiles (Linux only)\n```\n\nThis is an open-source project if you see a bug please file an issue! Pull requests are always welcome."},{"name":"2 - Beacons vs Sessions","content":"Sliver implants support two types of connections, sessions and beacons.\n\nSessions use long-polling connections, which means they constantly maintain a single TCP connection open while communicating with the server. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.\n\nTypically during an engagement you will want to deploy a beacon on the target system and switch to a session while doing more active enumeration.\n\nLet’s start with generating and deploying a beacon using `http`.\n\n```asciinema\n{\"src\": \"/asciinema/beacon_generation.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nYou can see the beacon callback times either in the `info` command or using `beacons watch`.\n\n```bash\n[server] sliver > beacons watch\n\n ID Name Transport Username Operating System Last Check-In Next Check-In\n========== =============== =========== ================= ==========Line truncated
{"tutorials":[{"name":"1 - Getting Started","content":"`sliver-server` is the binary you want to use to run the Sliver C2 server, `sliver-client` is solely a client to connect to a Sliver C2 server. Sliver server also acts as a client on its own, so you don’t necessarily run sliver server and client separately.\n\nFirst time running Sliver will take a couple seconds as it's retrieving its dependencies. Consecutive executions will be much faster. Go ahead and launch the `sliver-server`.\n\n```asciinema\n{\"src\": \"/asciinema/startup.cast\", \"cols\": \"132\", \"rows\": \"28\", \"idleTimeLimit\": 8}\n```\n\nLet's take a couple minutes to discuss what Sliver actually is and how it's set up.\n\n![Alt text](/images/Architecture.png)\n\nNow that Sliver is running, lets generate and execute your first implant to try out some of the basic features of Sliver, for now we’re going to run everything on the local host.\n\nHere's what we're going to do: \n* Generate your implant using the `generate` command as shown below.\n* Start HTTP listener on port 80\n* Execute implant in a separate terminal\n\n```asciinema\n{\"src\": \"/asciinema/first-implant.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNow let’s select our implant and run our first command using the `use` command.\n\n```bash\n[server] sliver > use\n? Select a session or beacon: \nSESSION 1884a365 RELATED_EARDRUM [::1]:49153 test.local tester darwin/amd64\n[*] Active session RELATED_EARDRUM (1884a365-085f-4506-b28e-80c481730fd0)\n\n[server] sliver (RELATED_EARDRUM) > pwd\n\n[*] /Users/tester/tools\n```\n\nOnce you have reached this point, go ahead and explore some of the commands listed below. In each case, first check out the command's help using the **`-h`** flag then try it out!\n\n```bash\nExploring and interacting with the filesystem\n\nFilesystem\n cat Dump file to stdout\n cd Change directory\n cp Copy a file\n download Download a file\n grep Search for strings that match a regex within a file or directory\n head Grab the first number of bytes or lines from a file\n ls List current directory\n mkdir Make a directory\n mv Move or rename a file\n pwd Print working directory\n rm Remove a file or directory\n tail Grab the last number of bytes or lines from a file\n upload Upload a file\n```\n\n```asciinema\n{\"src\": \"/asciinema/filesystem.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nGetting some environmental information\n```bash\nInfo\n env List environment variables\n getgid Get session process GID\n getpid Get session pid\n getuid Get session process UID\n info Get session info\n ping Send round trip message to implant (does not use ICMP)\n whoami Get session user execution context\n```\nExecute a binary\n\n```asciinema\n{\"src\": \"/asciinema/execute.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nRunning an interactive shell\n\n```asciinema\n{\"src\": \"/asciinema/shell.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNot all commands are supported for all operating systems or meant to be used on all operating systems, here are a few examples:\n\n```\nmount Get information on mounted filesystems (only for Windows and Linux)\nscreenshot Take a screenshot (only for Windows and Linux)\nmemfiles List current memfiles (Linux only)\n```\n\nThis is an open-source project if you see a bug please file an issue! Pull requests are always welcome."},{"name":"2 - Beacons vs Sessions","content":"Sliver implants support two types of connections, sessions and beacons.\n\nSessions provide interactive communication with the server; connection and polling behavior depend on the selected transport. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.\n\nTypically during an engagement you will want to deploy a beacon on the target system and switch to a session while doing more active enumeration.\n\nLet’s start with generating and deploying a beacon using `http`.\n\n```asciinema\n{\"src\": \"/asciinema/beacon_generation.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nYou can see the beacon callback times either in the `info` command or using `beacons watch`.\n\n```bash\n[server] sliver > beacons watch\n\n ID Name Transport Username Operating System Last Check-In Next Check-In\n========== =============== =========== ================= ================== =======Line truncated
+46 -5
View File
@@ -131,7 +131,7 @@ function Test-SkipPackage {
}
function Get-TestDirectories {
Get-ChildItem -Path @("client", "implant", "server", "util") -Recurse -Filter "*_test.go" -File |
Get-ChildItem -Path @("client", "docs", "implant", "protobuf", "server", "test", "util") -Recurse -Filter "*_test.go" -File |
ForEach-Object {
[string](Resolve-Path -LiteralPath $_.DirectoryName -Relative).Replace("\", "/")
} |
@@ -273,12 +273,13 @@ try {
$clientTestPkgs = @()
$implantTestPkgs = @()
$serverUtilTestPkgs = @()
$untaggedTestPkgs = @()
$e2eSupportTestPkgs = @()
foreach ($testDir in $testDirs) {
$pkg = if ($testDir.StartsWith(".")) { $testDir } else { "./$testDir" }
switch -Wildcard ($pkg) {
"./server/c2" { continue }
"./server/generate" { continue }
"./client/*" {
$clientTestPkgs += $pkg
continue
@@ -295,6 +296,25 @@ try {
$serverUtilTestPkgs += $pkg
continue
}
"./docs" {
$untaggedTestPkgs += $pkg
continue
}
"./protobuf/*" {
$untaggedTestPkgs += $pkg
continue
}
"./test/e2e" {
$e2eSupportTestPkgs += $pkg
continue
}
"./test/e2e/*" {
$e2eSupportTestPkgs += $pkg
continue
}
default {
throw "No test runner configured for discovered package: $pkg"
}
}
}
@@ -308,6 +328,16 @@ try {
}
}
foreach ($pkg in $e2eSupportTestPkgs) {
if (Test-SkipPackage -Package $pkg -Tags "client,$tags") {
continue
}
Invoke-TestStep -Name $pkg -Action {
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", "-tags=client,$tags", $pkg)
}
}
foreach ($pkg in $implantTestPkgs) {
if (Test-SkipPackage -Package $pkg) {
continue
@@ -318,6 +348,17 @@ try {
}
}
foreach ($pkg in $untaggedTestPkgs) {
if (Test-SkipPackage -Package $pkg) {
continue
}
Invoke-TestStep -Name $pkg -Action {
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", $pkg)
}
}
# Includes generation helpers without the sliver_generate compilation tests.
foreach ($pkg in $serverUtilTestPkgs) {
if (Test-SkipPackage -Package $pkg -Tags "server,$tags") {
continue
@@ -380,7 +421,7 @@ try {
Invoke-TestStep -Name "./server/generate" -Action {
Invoke-ExternalCommand -FilePath "go" `
-Arguments @("test", "-timeout", "6h", "-p", $generateGoP, "-parallel", $generateTestParallel, "-tags=server,$tags", "./server/generate") `
-Arguments @("test", "-timeout", "6h", "-p", $generateGoP, "-parallel", $generateTestParallel, "-tags=server,$tags,sliver_generate", "./server/generate") `
-Environment @{
GOMAXPROCS = $generateGoMaxProcs
GOPROXY = "off"
@@ -389,9 +430,9 @@ try {
} else {
Write-Host ""
if ($UnitOnly) {
Write-Host "Skipping ./server/generate tests (--unit-only)"
Write-Host "Skipping ./server/generate compilation tests (--unit-only)"
} else {
Write-Host "Skipping ./server/generate tests (--skip-generate)"
Write-Host "Skipping ./server/generate compilation tests (--skip-generate)"
}
}
} finally {
+109 -24
View File
@@ -27,16 +27,70 @@ fi
SKIP_GENERATE=0
UNIT_ONLY=0
for arg in "$@"; do
if [ "$arg" = "--skip-generate" ]; then
GENERATE_ONLY=0
GENERATE_RUN=""
usage() {
cat <<'EOF'
Usage: ./go-tests.sh [--skip-generate | --unit-only | --generate-only] [--run REGEX]
--skip-generate Skip implant compilation tests.
--unit-only Skip implant compilation and C2 end-to-end tests.
--generate-only Run only the server/generate package tests.
--run REGEX Select generation tests by Go test name regex; requires --generate-only.
--help Show this help.
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--skip-generate)
SKIP_GENERATE=1
fi
if [ "$arg" = "--unit-only" ]; then
;;
--unit-only)
UNIT_ONLY=1
SKIP_GENERATE=1
fi
;;
--generate-only)
GENERATE_ONLY=1
;;
--run)
if [ "$#" -lt 2 ] || [ -z "$2" ] || [[ "$2" == --* ]]; then
echo "--run requires a non-empty regex" >&2
exit 2
fi
GENERATE_RUN="$2"
shift
;;
--run=*)
GENERATE_RUN="${1#--run=}"
if [ -z "$GENERATE_RUN" ]; then
echo "--run requires a non-empty regex" >&2
exit 2
fi
;;
--help | -h)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
shift
done
if [ "$GENERATE_ONLY" -eq 1 ] && [ "$SKIP_GENERATE" -eq 1 ]; then
echo "--generate-only cannot be combined with --skip-generate or --unit-only" >&2
exit 2
fi
if [ -n "$GENERATE_RUN" ] && [ "$GENERATE_ONLY" -eq 0 ]; then
echo "--run requires --generate-only" >&2
exit 2
fi
echo "----------------------------------------------------------------"
echo "WARNING: Running unit tests on slow systems can take a LONG time"
echo " Recommended to only run on 16+ CPU cores and 32Gb+ RAM"
@@ -161,7 +215,7 @@ collect_test_dirs() {
if command -v rg >/dev/null 2>&1; then
rg --files -g '*_test.go'
else
find client implant server test util -type f -name '*_test.go' -print
find client docs implant protobuf server test util -type f -name '*_test.go' -print
fi
}
@@ -175,14 +229,42 @@ unpack_server_assets() {
"$HOST_GO" run -tags=server,go_sqlite ./server unpack --force
}
run_generate_tests() {
# Reclaim the host build cache before generation so long CI runs do not
# exhaust the runner's disk while retaining the implant compiler cache.
run_test_cmd "clean Go build cache" "$HOST_GO" clean -cache || return 1
export GOPROXY=off
# Generate tests are memory intensive (garble/c-shared cross-builds). Keep
# parallelism conservative by default to avoid OOM kills on smaller systems.
local generate_gomaxprocs="${SLIVER_GENERATE_GOMAXPROCS:-2}"
local generate_go_p="${SLIVER_GENERATE_GO_P:-1}"
local generate_test_parallel="${SLIVER_GENERATE_TEST_PARALLEL:-1}"
local generate_test_cmd=(
"$HOST_GO" test -timeout 6h -p "$generate_go_p" -parallel "$generate_test_parallel" -tags="server,$TAGS,sliver_generate"
)
if [ -n "$GENERATE_RUN" ]; then
# Keep test selection out of GOFLAGS, which implant compiler children inherit.
generate_test_cmd+=(-run "$GENERATE_RUN" -count=1 -v)
fi
generate_test_cmd+=(./server/generate)
run_test_cmd "./server/generate" \
env GOMAXPROCS="$generate_gomaxprocs" "${generate_test_cmd[@]}"
}
run_test_cmd "unpack server assets" unpack_server_assets || exit 1
if [ "$GENERATE_ONLY" -eq 1 ]; then
run_generate_tests || exit 1
exit 0
fi
TEST_DIRS=()
while IFS= read -r test_dir; do
TEST_DIRS+=("$test_dir")
done < <(collect_test_dirs | xargs -n1 dirname | sort -u)
CLIENT_TEST_PKGS=()
UNTAGGED_TEST_PKGS=()
IMPLANT_TEST_PKGS=()
SERVER_UTIL_TEST_PKGS=()
E2E_SUPPORT_TEST_PKGS=()
@@ -190,12 +272,15 @@ E2E_SUPPORT_TEST_PKGS=()
for test_dir in "${TEST_DIRS[@]}"; do
pkg="./$test_dir"
case "$pkg" in
./server/c2 | ./server/generate)
./server/c2)
# handled separately below
;;
./client/*)
CLIENT_TEST_PKGS+=("$pkg")
;;
./docs | ./protobuf/*)
UNTAGGED_TEST_PKGS+=("$pkg")
;;
./implant/*)
IMPLANT_TEST_PKGS+=("$pkg")
;;
@@ -205,9 +290,21 @@ for test_dir in "${TEST_DIRS[@]}"; do
./test/e2e | ./test/e2e/*)
E2E_SUPPORT_TEST_PKGS+=("$pkg")
;;
*)
echo "No test runner configured for $pkg" >&2
exit 1
;;
esac
done
## Packages without client, implant, or server build tags.
for pkg in "${UNTAGGED_TEST_PKGS[@]}"; do
if should_skip_package "$pkg"; then
continue
fi
run_test_cmd "$pkg" "$HOST_GO" test "$pkg" || exit 1
done
## Client
for pkg in "${CLIENT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "client,$TAGS"; then
@@ -233,7 +330,7 @@ for pkg in "${IMPLANT_TEST_PKGS[@]}"; do
run_test_cmd "$pkg" "$HOST_GO" test "$pkg" || exit 1
done
## Server + Util
## Server + Util, including generation helpers without compilation tests.
for pkg in "${SERVER_UTIL_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "server,$TAGS"; then
continue
@@ -261,26 +358,14 @@ else
run_test_cmd "./server/c2 (e2e dns)" "$HOST_GO" test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
fi
## Server generate
## Implant compilation tests opt in with sliver_generate; helper tests run above.
if [ "$SKIP_GENERATE" -eq 0 ]; then
# The package tests above no longer need their Go build cache. Reclaim it
# before the cross-platform generation matrix starts so long CI runs do not
# exhaust the runner's disk while retaining the implant compiler cache.
run_test_cmd "clean Go build cache" "$HOST_GO" clean -cache || exit 1
export GOPROXY=off
# Generate tests are memory intensive (garble/c-shared cross-builds). Keep
# parallelism conservative by default to avoid OOM kills on smaller systems.
GENERATE_GOMAXPROCS="${SLIVER_GENERATE_GOMAXPROCS:-2}"
GENERATE_GO_P="${SLIVER_GENERATE_GO_P:-1}"
GENERATE_TEST_PARALLEL="${SLIVER_GENERATE_TEST_PARALLEL:-1}"
run_test_cmd "./server/generate" \
env GOMAXPROCS="$GENERATE_GOMAXPROCS" \
"$HOST_GO" test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
run_generate_tests || exit 1
else
echo
if [ "$UNIT_ONLY" -eq 1 ]; then
echo "Skipping ./server/generate tests (--unit-only)"
echo "Skipping ./server/generate compilation tests (--unit-only)"
else
echo "Skipping ./server/generate tests (--skip-generate)"
echo "Skipping ./server/generate compilation tests (--skip-generate)"
fi
fi
+77 -82
View File
@@ -1,16 +1,16 @@
module github.com/bishopfox/sliver
go 1.27.1
go 1.27.3
replace github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.46.3-0.20231214181515-27e49f3c3b69
replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20261002031128-9c7d5fba75f5
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.2
charm.land/bubbletea/v2 v2.1.0
charm.land/glamour/v2 v2.0.1
charm.land/huh/v2 v2.0.3
charm.land/lipgloss/v2 v2.0.6
@@ -22,35 +22,37 @@ require (
github.com/alecthomas/chroma v0.10.0
github.com/alecthomas/chroma/v2 v2.27.0
github.com/atotto/clipboard v0.1.4
github.com/carapace-sh/carapace v1.11.0
github.com/carapace-sh/carapace v1.15.3
github.com/charmbracelet/colorprofile v0.4.3
github.com/charmbracelet/x/ansi v0.11.8
github.com/charmbracelet/x/ansi v0.11.9
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
github.com/cheggaaa/pb/v3 v3.1.7
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.1
github.com/ebitengine/purego v0.10.1
github.com/ebitengine/purego v0.11.0
github.com/glebarez/sqlite v1.11.0
github.com/go-sql-driver/mysql v1.9.3
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang/protobuf v1.5.4
github.com/gorilla/mux v1.8.1
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/hashicorp/yamux v0.1.2
github.com/jackc/pgx/v5 v5.10.0
github.com/jedib0t/go-pretty/v6 v6.8.3
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
github.com/klauspost/compress v1.18.4
github.com/klauspost/compress v1.18.7
github.com/lesnuages/go-winio v0.4.19
github.com/lesnuages/snitch v0.6.0
github.com/lucasb-eyer/go-colorful v1.4.1
github.com/mark3labs/mcp-go v0.45.0
github.com/mattn/go-sqlite3 v1.14.32
github.com/mattn/go-sqlite3 v1.14.52
github.com/miekg/dns v1.1.70
github.com/moloch--/asciicast v0.1.1
github.com/moloch--/go-keystone v0.0.2
github.com/moloch--/sgn v0.1.2
github.com/ncruces/go-sqlite3 v0.30.4
github.com/nikoksr/notify v1.5.0
github.com/nikoksr/notify v1.6.0
github.com/openai/openai-go/v2 v2.7.1
github.com/reeflective/console v0.1.25
github.com/reeflective/readline v1.1.4
@@ -63,7 +65,7 @@ require (
github.com/sliverarmory/wasm-donut v0.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.12.1
github.com/tetratelabs/wazero v1.12.0
github.com/things-go/go-socks5 v0.1.0
github.com/ulikunitz/xz v0.5.15
@@ -71,15 +73,15 @@ require (
github.com/yiya1989/sshkrb5 v0.0.1
github.com/zeebo/xxh3 v1.1.0
golang.org/x/crypto v0.55.0
golang.org/x/mod v0.38.0
golang.org/x/net v0.57.0
golang.org/x/sys v0.47.0
golang.org/x/mod v0.40.0
golang.org/x/net v0.58.0
golang.org/x/sys v0.49.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
google.golang.org/api v0.269.0
google.golang.org/grpc v1.83.1
google.golang.org/api v0.295.0
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.12
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
@@ -88,7 +90,7 @@ require (
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.2
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c
maunium.net/go/mautrix v0.26.0
maunium.net/go/mautrix v0.30.0
modernc.org/sqlite v1.39.0
tailscale.com v1.92.5
)
@@ -96,21 +98,21 @@ require (
require (
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth v0.23.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/firestore v1.21.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/longrunning v0.8.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cloud.google.com/go/storage v1.60.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
cloud.google.com/go/firestore v1.24.0 // indirect
cloud.google.com/go/iam v1.12.0 // indirect
cloud.google.com/go/longrunning v1.2.0 // indirect
cloud.google.com/go/monitoring v1.30.0 // indirect
cloud.google.com/go/storage v1.62.2 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
filippo.io/hpke v0.4.0 // indirect
firebase.google.com/go/v4 v4.18.0 // indirect
firebase.google.com/go/v4 v4.21.0 // indirect
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/PagerDuty/go-pagerduty v1.8.0 // indirect
@@ -119,25 +121,25 @@ require (
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
github.com/appleboy/go-fcm v1.2.6 // indirect
github.com/appleboy/go-fcm v1.2.11 // indirect
github.com/atc0005/go-teams-notify/v2 v2.14.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/ses v1.34.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.39.10 // indirect
github.com/aws/aws-sdk-go-v2 v1.45.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.33.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.20.1 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ses v1.39.1 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.44.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.35.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.40.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.47.1 // indirect
github.com/aws/smithy-go v1.28.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
@@ -150,7 +152,7 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
@@ -161,11 +163,10 @@ require (
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/coder/websocket v1.8.15 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/creachadair/msync v0.7.1 // indirect
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
github.com/demisto/goxforce v0.0.0-20160322194047-db8357535b1d // indirect
github.com/dghubble/oauth1 v0.7.3 // indirect
@@ -188,12 +189,11 @@ require (
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
github.com/go-lark/lark v1.16.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
@@ -208,8 +208,8 @@ require (
github.com/google/nftables v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect
github.com/googleapis/gax-go/v2 v2.24.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gregdel/pushover v1.4.0 // indirect
@@ -219,7 +219,6 @@ require (
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jezek/xgb v1.1.1 // indirect
@@ -228,48 +227,43 @@ require (
github.com/joho/godotenv v1.5.1 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/go-types v0.0.0-20240719050749-165e75e768f7 // indirect
github.com/kevinburke/rest v0.0.0-20250718180114-1a15e4f2364f // indirect
github.com/kevinburke/twilio-go v0.0.0-20250718182727-5fe7adc01f29 // indirect
github.com/kevinburke/twilio-go v0.0.0-20260522200016-74f727d55757 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/mailgun/errors v0.4.0 // indirect
github.com/mailgun/mailgun-go/v5 v5.8.1 // indirect
github.com/mailgun/mailgun-go/v5 v5.19.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.30 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mileusna/viber v1.0.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/oapi-codegen/runtime v1.1.2 // indirect
github.com/oapi-codegen/runtime v1.6.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/plivo/plivo-go/v7 v7.59.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/plivo/plivo-go/v7 v7.60.3 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/rs/zerolog v1.35.1 // indirect
github.com/rsteube/carapace-shlex v0.1.2 // indirect
github.com/safchain/ethtool v0.6.2 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
github.com/silenceper/wechat/v2 v2.1.11 // indirect
github.com/slack-go/slack v0.24.0 // indirect
github.com/silenceper/wechat/v2 v2.1.14 // indirect
github.com/slack-go/slack v0.29.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
@@ -281,9 +275,9 @@ require (
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/textmagic/textmagic-rest-go-v2/v3 v3.0.43885 // indirect
github.com/textmagic/textmagic-rest-go-v2/v3 v3.0.50044 // indirect
github.com/thedevsaddam/gojsonq/v2 v2.5.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/gjson v1.19.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
@@ -292,33 +286,34 @@ require (
github.com/utahta/go-linenotify v0.5.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xo/terminfo v1.0.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark v1.8.5 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
go.mau.fi/util v0.9.3 // indirect
go.mau.fi/util v0.10.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.48.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.49.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect
+160 -179
View File
@@ -6,8 +6,8 @@ cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/bubbletea/v2 v2.1.0 h1:A9fj9h5LFrvQhQHFNggp8K8rvY6CUfgn9dMbb6p2HIw=
charm.land/bubbletea/v2 v2.1.0/go.mod h1:xWHeyxIX9/wiFU2kgPW04ojOZyTzrWf+8lGjUCaftiU=
charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c=
charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k=
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
@@ -17,36 +17,36 @@ charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZ
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo=
cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM=
cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=
cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
cloud.google.com/go/firestore v1.24.0 h1:x0Z3hrgjYgo2wI9whuBRQcNc2hYwzZDQy/7pkUXbXcs=
cloud.google.com/go/firestore v1.24.0/go.mod h1:5aojyjN4olKUnBZDCRWwM+NsdrrCX3t1qfyERZGOonM=
cloud.google.com/go/iam v1.12.0 h1:Aki3bX9aHUDKPHfnRJfDcTdVedvy6quGBQcTqx3DRXk=
cloud.google.com/go/iam v1.12.0/go.mod h1:FEZ4lXpADAC2AIpQY7LANNjjwyQ2jK439CI2VaD+sLY=
cloud.google.com/go/logging v1.19.0 h1:NCqhdVUg3wQ8Cobdf16FDSuTGi3+6+hdSBHrY5TsR6Q=
cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA=
cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
cloud.google.com/go/monitoring v1.30.0 h1:r/d+JUbyKmJ8b07iznuKfzVzrIXTWxHQ3lBRm3x2LlY=
cloud.google.com/go/monitoring v1.30.0/go.mod h1:htlUR0QWVMrjFzZmN4LGnMAve9xB/eduwjmINxVZ8RM=
cloud.google.com/go/storage v1.62.2 h1:WgR4U9n7bIzXkkVnwPKKE8bkaKUNsHG+0MAAlh9DGU4=
cloud.google.com/go/storage v1.62.2/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA=
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0=
filippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw=
firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=
firebase.google.com/go/v4 v4.21.0 h1:HBZV4jrLtFYj8EwWyqEZOuRLfkfkV2bpnfyyXHOhPxY=
firebase.google.com/go/v4 v4.21.0/go.mod h1:CDumIdA5oTiyDpLNVcQoW8ZrB5CTgyE2D45DuENIABg=
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb h1:c1Bb1n1wr2xgTHkNVxkUHBshff33m+IXgY6lE6oYttg=
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb/go.mod h1:fSQ9asCJK/lIGXUnoRwJNMXcnzel53AsZMCtlVBIn3k=
github.com/Binject/debug v0.0.0-20210312092933-6277045c2fdf/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
@@ -59,12 +59,12 @@ github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/k
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 h1:O2sXMyJh8b7devAGdE+163xtRurt0RVpB6DIzX5vGfg=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0/go.mod h1:hEpiGU18xf70qb3jbTcIggWAiEfX/cOIVc2OTe4OegA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0 h1:ZIT85vKP7LBS84XJ0WdJ3dPOX3iz4j3c0+lpajGQMyo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0/go.mod h1:rqP9UEhOXv9WhQ7Gjz+G5y/pf8+BJZW5/Ts0AhE0PwE=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 h1:0YP0+/ixwu+Uqeu/FGiBZNQ19huiUxxiPXIc9WsLKuQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0/go.mod h1:6ZZMQhZKDvUvkJw2rc+oDP90tMMzuU/J+5HG1ZmPOmE=
github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo=
github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@@ -105,46 +105,46 @@ github.com/alicebob/miniredis/v2 v2.30.0 h1:uA3uhDbCxfO9+DI/DuGeAMr9qI+noVWwGPNT
github.com/alicebob/miniredis/v2 v2.30.0/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/appleboy/go-fcm v1.2.6 h1:TU5/+2QnmTNjWkHLe9hUB9EPJ5Bv+CegzTJI0qK0QgA=
github.com/appleboy/go-fcm v1.2.6/go.mod h1:nvi8DgoMax8o6nwQYgO8pIXSX6iaQY7yDYvtwIGa6aI=
github.com/appleboy/go-fcm v1.2.11 h1:JK0kQOyM7xYGxykKKwEoNyNnWkrbIX3HswKDug/AcX4=
github.com/appleboy/go-fcm v1.2.11/go.mod h1:TVKSknWBz2N63uZjigxi/dudL+jZ7KQ9tfQwF3dkyyU=
github.com/atc0005/go-teams-notify/v2 v2.14.0 h1:7N+xw+COnYANLREaAveQ65rsNQ12nIZJED9nMLyscCo=
github.com/atc0005/go-teams-notify/v2 v2.14.0/go.mod h1:EECsWM2b0Hvoz7O+QdlsvyN2KCUOFQCGj8bUBXv3A3Q=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/ses v1.34.17 h1:XR7CtY988tck2Bhuy1JP4FsV8z0OAwjuh+gb7nAy8/M=
github.com/aws/aws-sdk-go-v2/service/ses v1.34.17/go.mod h1:2CspeTVldnJdRixX36SzTZuoIpjyKlfeXyB7/JB5KGk=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sns v1.39.10 h1:wqErrLzV3iERQ7dbZbKQS0gOM6ngxZtmPwKyRGn+Krc=
github.com/aws/aws-sdk-go-v2/service/sns v1.39.10/go.mod h1:OiwBtRz6QlQyt69WLBMvSiyfgI7cOd6xSJ9ThTMjI5M=
github.com/aws/aws-sdk-go-v2 v1.45.1 h1:iIoG3NaLhV6UZpPXyPXlDj2I9oS8tV/nMcMnITCC6Ks=
github.com/aws/aws-sdk-go-v2 v1.45.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
github.com/aws/aws-sdk-go-v2/config v1.33.1 h1:bq9jze1hQ5YTCLoVxNnbp0T7rglrlOE7N9YsHqjGkEw=
github.com/aws/aws-sdk-go-v2/config v1.33.1/go.mod h1:2A3HQwG4zaL5Tm80rc6RZj8LmWWv4WYT5v8raSz/L7A=
github.com/aws/aws-sdk-go-v2/credentials v1.20.1 h1:Z8GRNEx0u9sDkZOq4PUnN8mjGwbUQGRzMSXpvt3d8xQ=
github.com/aws/aws-sdk-go-v2/credentials v1.20.1/go.mod h1:uBIK00kFo95dnemqfFMTWx0X8YRqsh6ecIoCjjOkZqM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 h1:YIEBqcqRnpi4Pfv0YHImtgi6czGCwKHANC7SwmUAVD0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1/go.mod h1:imEf0oufgAo8KAkCHhrOdqGEC0YWx1PPBQH82shSxGw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 h1:pc138gM1CW+XPc60rEwUlwwuwWFQK16CI1T7v1F9Oec=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1/go.mod h1:1+koxpPIbfBdfzP6vojm5/zTpTQ/micYwlxIiNB3TxI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 h1:K0JsbZQj+1h208Ro1zHeA4l7bMp0NvRffHQ91q8Ol1s=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1/go.mod h1:W3/vL6EtCIatICGy9ab29QhMuae+cOKPWcMxv02CO+Q=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 h1:yhw5KD1phVyP9vijxOUzDfEtJx+bt+L63k+VfuiYFAA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1/go.mod h1:ZW2e0d7DYlRxlS9hEiMXE47gTdX5KRN4byUiNbUpG+Q=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 h1:RmmWQPREQdk9U+PfqeHW3MqZaBaNK7TpV9W3RY+b+7g=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1/go.mod h1:0A3W4F+68ZnNk5XcNL/e9HFMwnP8RlEicFfy6eOEDyw=
github.com/aws/aws-sdk-go-v2/service/ses v1.39.1 h1:B6jArw3eQI7qj9y4glbzZJMd8Cv9+0ON8CzATYOXzbw=
github.com/aws/aws-sdk-go-v2/service/ses v1.39.1/go.mod h1:c32UEWTkWkzfGTqCErAhYxfU+NUdj1wssOQFFGwC7JU=
github.com/aws/aws-sdk-go-v2/service/signin v1.7.1 h1:mdMtSVKdQ3+mzBh+l0ogrFYZVQUCg6pJZOirA2ARsYE=
github.com/aws/aws-sdk-go-v2/service/signin v1.7.1/go.mod h1:9IqUlsJDbUPcg6cgx3WEzXdjrbWzLDQrak0aaSqlTcI=
github.com/aws/aws-sdk-go-v2/service/sns v1.44.1 h1:dMIcbUQ8fPJPbX9jZV19JtL2lCAgEh1LUNlpe6sdgqE=
github.com/aws/aws-sdk-go-v2/service/sns v1.44.1/go.mod h1:ucBUMGW8avqGmbyQoXyoC6Cgt+WsNBrhL9DA4Bb+jN4=
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1 h1:TFg6XiS7EsHN0/jpV3eVNczZi/sPIVP5jxIs+euIESQ=
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1/go.mod h1:OIezd9K0sM/64DDP4kXx/i0NdgXu6R5KE6SCsIPJsjc=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/aws-sdk-go-v2/service/sso v1.35.1 h1:B6WFn91tobD6gG4724ONHaqrpKsoETGnv98LHe/yIGM=
github.com/aws/aws-sdk-go-v2/service/sso v1.35.1/go.mod h1:tWuiVBUtPBr8/rgRiYS8Uf85sHcAN+G7XS3D3CEoUh8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.40.1 h1:6yeYCWFvgbI2TI3K6jr9LtBNhXgJ7g4xqD+DEiaDDmM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.40.1/go.mod h1:naFe83jSMuYkH+QjQPX8n1MLhBkeCFM5Lsnh5m5wz3c=
github.com/aws/aws-sdk-go-v2/service/sts v1.47.1 h1:Sv2xPnRHlThSUtVujYuUBPI/Il8si6UPHXL8DMiB/F0=
github.com/aws/aws-sdk-go-v2/service/sts v1.47.1/go.mod h1:mKo/CzaCz8qytGW70NG4vIIGAx1HXTlb5lHNkC5k3lk=
github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ=
github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@@ -163,8 +163,8 @@ github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+Eg
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/caarlos0/go-reddit/v3 v3.0.1 h1:w8ugvsrHhaE/m4ez0BO/sTBOBWI9WZTjG7VTecHnql4=
github.com/caarlos0/go-reddit/v3 v3.0.1/go.mod h1:QlwgmG5SAqxMeQvg/A2dD1x9cIZCO56BMnMdjXLoisI=
github.com/carapace-sh/carapace v1.11.0 h1:dTUFXvIrUTLd9BGLsfDt4wCgEyPl8bJeZOFDmN7fIfo=
github.com/carapace-sh/carapace v1.11.0/go.mod h1:NVkuv1s8wyFvu7d98rdgYwqGKMB6GO1MEUZp5RcWri8=
github.com/carapace-sh/carapace v1.15.3 h1:vA8vrQ99xW0rcESc4eyn+Tgc7MCKgxU+czHR8fQ54Rk=
github.com/carapace-sh/carapace v1.15.3/go.mod h1:5MUSHyLN9GGb5/NY/j9VI68/TcZV4ApRCAHGg4WeU0s=
github.com/carapace-sh/carapace-shlex v1.1.1 h1:ccmNeetAYZOk4IcV36youFDsXusT9uCNW2Njkw+QS+Q=
github.com/carapace-sh/carapace-shlex v1.1.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
@@ -179,10 +179,10 @@ github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 h1:hISLzUbwcvJbGoNR6A3L/TpguRRnACBmZOee8LcHLJw=
github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4/go.mod h1:aJDVk+WOVM1K04rdc/71PMPoUGfjxjhvXfP3Pjz1Zio=
github.com/charmbracelet/x/ansi v0.11.9 h1:kIrqLBxtTVbLyyHO2Evoi93EjQo1mylWmpNEsfPxXAM=
github.com/charmbracelet/x/ansi v0.11.9/go.mod h1:DG/hpOnaI2QJ/KMMqLL2h0KVhluuJLazf8973P+Vy5w=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
@@ -226,11 +226,10 @@ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJ
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho=
@@ -243,8 +242,6 @@ github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb h1:7X9nrm
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb/go.mod h1:RfQ9wji3fjcSEsQ+uFCtIh3+BXgcZum8Kt3JxvzYzlk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
@@ -269,8 +266,8 @@ github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 h1:uh1GSejOhVPR
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7/go.mod h1:ncTaGuXc5v7AuiVekeJ0Nwh8Bf4cudukoj0qM/15UZE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.11.0 h1:jhp/D+Nyv7UUW8HAcmcjt2N2rYrYi9m3SL21k0Ua/NI=
github.com/ebitengine/purego v0.11.0/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -308,8 +305,8 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
@@ -319,8 +316,8 @@ github.com/go-lark/lark v1.16.0 h1:U6BwkLM9wrZedSM7cIiMofganr8PCvJN+M75w2lf2Gg=
github.com/go-lark/lark v1.16.0/go.mod h1:6ltbSztPZRT6IaO9ZIQyVaY5pVp/KeMizDYtfZkU+vM=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
@@ -351,7 +348,6 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
@@ -397,7 +393,6 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
@@ -410,10 +405,10 @@ github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0
github.com/google/uuid v0.0.0-20171113160352-8c31c18f31ed/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/googleapis/enterprise-certificate-proxy v0.3.20 h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk=
github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
github.com/googleapis/gax-go/v2 v2.24.0 h1:myMaPYyF9MecEmvQqMqomIwn9t/4KCZN9qnwsS76wlg=
github.com/googleapis/gax-go/v2 v2.24.0/go.mod h1:IaTHBDd7NHxSCiu0vEs8pQZu4dGZrWwuSoxCnk16OFM=
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 h1:4EZlYQIiyecYJlUbVkFXCXHz1QPhVXcHnQKAzBTPfQo=
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4/go.mod h1:lEO7XoHJ/xNRBCxrn4h/CEB67h0kW1B0t4ooP2yrjUA=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -477,8 +472,6 @@ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90=
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4=
@@ -487,12 +480,12 @@ github.com/kevinburke/go-types v0.0.0-20240719050749-165e75e768f7 h1:36PMhfw/I1Y
github.com/kevinburke/go-types v0.0.0-20240719050749-165e75e768f7/go.mod h1:8tQOif9eUJLpDnvfDcGtesfv6VpL2UvDbW4l8kXnSDE=
github.com/kevinburke/rest v0.0.0-20250718180114-1a15e4f2364f h1:y+inhBsY0ewgXFXCXlxodNxkXdYeU9YuneCYQEnRkmw=
github.com/kevinburke/rest v0.0.0-20250718180114-1a15e4f2364f/go.mod h1:3cBF15uOiTj025Ll5QHLw317EB+e06+AEwyt7oHUubI=
github.com/kevinburke/twilio-go v0.0.0-20250718182727-5fe7adc01f29 h1:AZ5D/KUEro9jOlMv94YJoufveb3vwM5WBwQ0Pfsh+XI=
github.com/kevinburke/twilio-go v0.0.0-20250718182727-5fe7adc01f29/go.mod h1:Z7bqFOTtIqCCIO+uPtod7Fp3gPMR8aTCcp5Pe8D+3fE=
github.com/kevinburke/twilio-go v0.0.0-20260522200016-74f727d55757 h1:dQyNAHiOY8hT4lnjYEp2lypELs6bLsqd3QJ4KJfiAY0=
github.com/kevinburke/twilio-go v0.0.0-20260522200016-74f727d55757/go.mod h1:Z7bqFOTtIqCCIO+uPtod7Fp3gPMR8aTCcp5Pe8D+3fE=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -522,28 +515,23 @@ github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBql
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
github.com/mailgun/errors v0.4.0/go.mod h1:xGBaaKdEdQT0/FhwvoXv4oBaqqmVZz9P1XEnvD/onc0=
github.com/mailgun/mailgun-go/v5 v5.8.1 h1:QoRa2oBqLq+8GTYgM4id/cKW/WySQRka3PtnW8IK6KQ=
github.com/mailgun/mailgun-go/v5 v5.8.1/go.mod h1:qNTXXuJi9/myqpDLI8Mbn54WCXdto1kEHm6I2/WWYQQ=
github.com/mailgun/mailgun-go/v5 v5.19.3 h1:82A9Jcj8fUM6P6nCL5agO+mKwAibecZZf4RBHioXR8s=
github.com/mailgun/mailgun-go/v5 v5.19.3/go.mod h1:ZgRr4pcGJDQjc8/HTuV6i04GKK3UvrAe1qTTd/pvixA=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc=
github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM=
@@ -562,15 +550,10 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/moloch--/asciicast v0.1.1 h1:btwwniCK9br3ys33XZ71IG/M3ZujSSCcBzsH/sieiXg=
github.com/moloch--/asciicast v0.1.1/go.mod h1:OckO16UDLgxVLclrCnbocL1ix15Br/8Xv/caBoYq98o=
github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701 h1:6caCgDZpovjvv+eclRytFDf0GpVEnjDmoJRHpfkb42E=
github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701/go.mod h1:UdqyqqCSnPu3Zc8Q3kpDszrq1hcyBHsG17hlWlRJmWI=
github.com/moloch--/console v0.0.0-20261002031128-9c7d5fba75f5 h1:BwamAdS8GG5tsxKisLphOOC5RVmTcy8Ixf+oGyhh/Jc=
github.com/moloch--/console v0.0.0-20261002031128-9c7d5fba75f5/go.mod h1:UdqyqqCSnPu3Zc8Q3kpDszrq1hcyBHsG17hlWlRJmWI=
github.com/moloch--/go-keystone v0.0.2 h1:xThbYs45CSzoDUz5kBZxAyTPIpK3t5M7NB1x+NTTRL8=
github.com/moloch--/go-keystone v0.0.2/go.mod h1:kpGRJpw9b7Eq3MluHrba+GPKZhSyDrvhLULSwH5nGGM=
github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a h1:Q69DxLWF+Z8oIjuNbeKC6Xygk8Z4heWzzO79Io1cEYA=
@@ -590,13 +573,13 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nikoksr/notify v1.5.0 h1:mzkCw8eb0P+qHwgmGQyPPGqz4GH+07FJDr44Bs16T9k=
github.com/nikoksr/notify v1.5.0/go.mod h1:CEV9Bw9Y59K5oj7d8h83Xl32ATeL43ZEg9qTQsfwcCc=
github.com/nikoksr/notify v1.6.0 h1:H9pyvNyo4/47vn7uPTBbb+ld6Y/KmpXo9Ghf8cGqAlU=
github.com/nikoksr/notify v1.6.0/go.mod h1:GBrx8S2GI0ZtXdobxNmXJjPJN4P6qXbjPW78zlLSg6s=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU=
github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -630,11 +613,9 @@ github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/plivo/plivo-go/v7 v7.59.2 h1:S3CGORMXXYOnuKXBQ1Sk9DtSbeXa+bRA4S3TypfpOQY=
github.com/plivo/plivo-go/v7 v7.59.2/go.mod h1:ceCFoYEzQrtrJjLcU7HR/r6Vz2kAVSaylrL7SjGPymc=
github.com/plivo/plivo-go/v7 v7.60.3 h1:pv6OPdt0z9KxgyuT7nLEtD+f6xcRxox+lcnxWvGFoVA=
github.com/plivo/plivo-go/v7 v7.60.3/go.mod h1:ceCFoYEzQrtrJjLcU7HR/r6Vz2kAVSaylrL7SjGPymc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -648,9 +629,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/rsteube/carapace v0.50.2 h1:9nPOBmIKPySFQvtBRGghFafRCR09Cf9K4/BEo1M+VFE=
github.com/rsteube/carapace v0.50.2/go.mod h1:syVOvI8e2rEEK/9aMZxfWuHvcnQK/EcnTV4roClEnLE=
github.com/rsteube/carapace-shlex v0.1.2 h1:ZKjhIfXoCkEnzensMglTaLbkNOaLkmM8SCRshpJKx6s=
@@ -662,15 +642,15 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1GjJohAA0p6hVEaDtHWWs=
github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/silenceper/wechat/v2 v2.1.11 h1:KA0iuhEpwMl9L3R0Kg8KSE23CEszMbnhjBf/L2EJnSw=
github.com/silenceper/wechat/v2 v2.1.11/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s=
github.com/silenceper/wechat/v2 v2.1.14 h1:ORpbzDUENOMO/mXzZ2uajXPbuNTPFWxxO5vQ3Nsh9Sk=
github.com/silenceper/wechat/v2 v2.1.14/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/slack-go/slack v0.24.0 h1:oMz5WcCBVTkxpBxeA8QVxxK70R+XV/a0qNka6TRNGHQ=
github.com/slack-go/slack v0.24.0/go.mod h1:H0yR/YBuRJ39RkE+JpV/d/oEsbanzTRowR82bCN0cEs=
github.com/slack-go/slack v0.29.0 h1:ohhMNgp9DmPKiLhH/pNZV4NxhOXKgNy0SH8FzVHNerI=
github.com/slack-go/slack v0.29.0/go.mod h1:UEe+jmo9WLlwHB04qsOrTDvqM7Aa4rQL3O5wF3n0hx4=
github.com/sliverarmory/beignet v0.0.3 h1:ssR6AK9lirVulHZhQMyMmVAi0FcFwWxlh9LwKCJQ4X0=
github.com/sliverarmory/beignet v0.0.3/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/malasada v0.0.3 h1:b2KG2NhQVfBIIrbP8rGJwmdh7o8LH6NKYKH1DF4No1U=
@@ -713,8 +693,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
@@ -743,16 +723,16 @@ github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQ
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/textmagic/textmagic-rest-go-v2/v3 v3.0.43885 h1:Ve/YElmvgqg7yUQewiPfTV3PgCp1Tlu2PcPI0f8rAgg=
github.com/textmagic/textmagic-rest-go-v2/v3 v3.0.43885/go.mod h1:76AjUSm7GRxmgio8I/XbwaO5Vvw40HdSD/W3Z3M+OWo=
github.com/textmagic/textmagic-rest-go-v2/v3 v3.0.50044 h1:RZgDQrwyLHpPrmTEx7d9AsXfUt5a6cZYmGGCIax0n8Y=
github.com/textmagic/textmagic-rest-go-v2/v3 v3.0.50044/go.mod h1:76AjUSm7GRxmgio8I/XbwaO5Vvw40HdSD/W3Z3M+OWo=
github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0=
github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs=
github.com/things-go/go-socks5 v0.1.0 h1:4f5dz0iMQ6cA4wseFmyLmCHmg3SWJTW92ndrKS6oERg=
github.com/things-go/go-socks5 v0.1.0/go.mod h1:Riabiyu52kLsla0YmJqunt1c1JEl6iXSr4bRd7swFEA=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
@@ -781,8 +761,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yiya1989/sshkrb5 v0.0.1 h1:oAKpXLBeZExjYDV9eTL2KypmXU+5oPrJtqDuR5JbWic=
github.com/yiya1989/sshkrb5 v0.0.1/go.mod h1:r1bHcNa22x8rrg/Kj/cDF5bX6UtZrHiQgRFvj3Mnpl8=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
@@ -791,8 +771,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
@@ -801,35 +781,37 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.mau.fi/util v0.9.3 h1:aqNF8KDIN8bFpFbybSk+mEBil7IHeBwlujfyTnvP0uU=
go.mau.fi/util v0.9.3/go.mod h1:krWWfBM1jWTb5f8NCa2TLqWMQuM81X7TGQjhMjBeXmQ=
go.mau.fi/util v0.10.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk=
go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
@@ -848,8 +830,8 @@ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
@@ -865,8 +847,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -888,8 +870,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
@@ -936,7 +918,6 @@ golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -945,8 +926,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -973,8 +954,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -989,8 +970,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1005,8 +986,8 @@ golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg=
google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE=
google.golang.org/api v0.295.0 h1:SSqFeEVjnK5SKo6t7D0E0M7EfX8SP7K0+OJd2Ly5FzU=
google.golang.org/api v0.295.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
@@ -1014,19 +995,19 @@ google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 h1:RxhCsti413yL0IjU9dVvuTbCISo8gs3RW1jPMStck+4=
google.golang.org/genproto v0.0.0-20260226221140-a57be14db171/go.mod h1:uhvzakVEqAuXU3TC2JCsxIRe5f77l+JySE3EqPoMyqM=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms=
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU=
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d h1:QwnJwPte4XXAkhPu26LTDIahnsMSUV0kK8HkxbC+Pc4=
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d/go.mod h1:WRrQ7/7N19PypuT0fxLOL5Lq0waoiRri4FbtHDEKrGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1089,8 +1070,8 @@ honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0 h1:5SXjd4ET5dYijLa
honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0/go.mod h1:EPDDhEZqVHhWuPI5zPAsjU0U7v9xNIWjoOVyZ5ZcniQ=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
maunium.net/go/mautrix v0.26.0 h1:valc2VmZF+oIY4bMq4Cd5H9cEKMRe8eP4FM7iiaYLxI=
maunium.net/go/mautrix v0.26.0/go.mod h1:NWMv+243NX/gDrLofJ2nNXJPrG8vzoM+WUCWph85S6Q=
maunium.net/go/mautrix v0.30.0 h1:bad+q7w5tLqiHpr+oUxVI+8m8ePbV3AvoFKg2jQzPyo=
maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/bishopfox/sliver
go 1.27.1
go 1.27.3
require (
filippo.io/age v1.1.1
+10 -3
View File
@@ -25,10 +25,17 @@ var (
SliverName = `{{.Name}}`
)
// Message - Fake message for embedding canaries
// Message carries canaries in struct tags, which remain plaintext under Garble.
type Message struct {
Command string `c2:"[[GenerateCanary]]"`
// {{if .Config.IncludeMTLS}}
MTLS string `c2:"[[GenerateCanary]]"`
// {{end}}
// {{if .Config.IncludeWG}}
WireGuard string `c2:"[[GenerateCanary]]"`
// {{end}}
}
// never obfuscate the Message type
var _ = reflect.TypeOf(Message{})
// Access a field at runtime to retain the type metadata and its canary tags.
// A discarded TypeOf call alone is optimized away, removing the canaries.
var _ = reflect.TypeOf(Message{}).Field(0)
+62 -29
View File
@@ -19,6 +19,8 @@ package extension
*/
import (
"bytes"
"io"
"io/fs"
"testing"
)
@@ -33,7 +35,11 @@ func TestBasicMemFSOpenFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stat, _ := fi.Stat(); stat.Name() != "memfs/test.1" {
stat, err := fi.Stat()
if err != nil {
t.Fatal(err)
}
if stat.Name() != "memfs/test.1" {
t.Fatalf("expected memfs/test.1, got %s", stat.Name())
}
defer fi.Close()
@@ -58,20 +64,24 @@ func TestMemFSOpenRoot(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(entries) != 3 {
for entry := range entries {
t.Logf("entry: %s", entries[entry].Name())
wantEntries := map[string]bool{"memfs/test.1": false, "memfs/test.2": false, "memfs/a": true}
if len(entries) != len(wantEntries) {
t.Fatalf("expected %d entries, got %d", len(wantEntries), len(entries))
}
for _, entry := range entries {
wantDir, ok := wantEntries[entry.Name()]
if !ok || entry.IsDir() != wantDir {
t.Fatalf("unexpected entry %q (directory=%v)", entry.Name(), entry.IsDir())
}
t.Fatalf("expected 3 entries, got %d", len(entries))
delete(wantEntries, entry.Name())
}
di, err := dirNode.Stat()
if err != nil {
t.Fatal(err)
}
t.Logf("name: %s, is dir: %v", di.Name(), di.IsDir())
for _, entry := range entries {
t.Logf("entry: %s", entry.Name())
if di.Name() != "memfs" || !di.IsDir() {
t.Fatalf("expected memfs root directory, got %q (directory=%v)", di.Name(), di.IsDir())
}
}
@@ -80,7 +90,7 @@ func TestBasicMemFSOpenDir(t *testing.T) {
"test/foo.1": {0x00},
"test/foo": {0x00},
"testing/foo": {0x00},
"a/b/c/d/e/f/test/a.txt": {0x00},
"a/b/c/d/e/f/test/a.txt": {0x31, 0x00, 0x7f},
}}
// Test Open File
@@ -91,7 +101,11 @@ func TestBasicMemFSOpenDir(t *testing.T) {
if f == nil {
t.Fatal("expected file, got nil and no error")
}
if stat, _ := f.Stat(); stat.Name() != "memfs/test" || !stat.IsDir() {
stat, err := f.Stat()
if err != nil {
t.Fatal(err)
}
if stat.Name() != "memfs/test" || !stat.IsDir() {
t.Fatalf("expected 'memfs/test' dir, got '%s'", stat.Name())
}
defer f.Close()
@@ -100,7 +114,16 @@ func TestBasicMemFSOpenDir(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stat, _ := dir.Stat(); stat.Name() != "memfs/a/b/c/d/e/f/test" || !stat.IsDir() {
defer func() {
if err := dir.Close(); err != nil {
t.Errorf("close directory: %v", err)
}
}()
stat, err = dir.Stat()
if err != nil {
t.Fatal(err)
}
if stat.Name() != "memfs/a/b/c/d/e/f/test" || !stat.IsDir() {
t.Fatalf("expected 'memfs/a/b/c/d/e/f/test' dir, got '%s'", stat.Name())
}
@@ -108,19 +131,17 @@ func TestBasicMemFSOpenDir(t *testing.T) {
if err != nil {
t.Fatal(err)
}
data := make([]byte, 1)
_, err = f.Read(data)
if err != nil {
t.Fatal(err)
}
if data[0] != 0x00 {
t.Fatalf("expected 0x00, got %x", data[0])
}
defer func() {
if err := f.Close(); err != nil {
t.Errorf("close file: %v", err)
}
}()
assertMemFSFileContents(t, f, []byte{0x31, 0x00, 0x7f})
}
func TestMemFSReadFileData(t *testing.T) {
wasmFS := WasmMemoryFS{memFS: map[string][]byte{
"test/foo.1": {0x00},
"test/foo.1": {0x41, 0x00, 0xff, 0x42},
}}
// Test Open File
@@ -128,19 +149,31 @@ func TestMemFSReadFileData(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stat, _ := fi.Stat(); stat.Name() != "memfs/test/foo.1" {
stat, err := fi.Stat()
if err != nil {
t.Fatal(err)
}
if stat.Name() != "memfs/test/foo.1" {
t.Fatalf("expected memfs/test/foo.1, got %s", stat.Name())
}
defer fi.Close()
// Read file
data := make([]byte, 1)
_, err = fi.Read(data)
if err != nil {
t.Fatal(err)
assertMemFSFileContents(t, fi, []byte{0x41, 0x00, 0xff, 0x42})
}
func assertMemFSFileContents(t *testing.T, file fs.File, want []byte) {
t.Helper()
// Read in chunks to verify byte counts and advancement of the file offset.
for offset := 0; offset < len(want); {
end := min(offset+2, len(want))
data := make([]byte, end-offset)
n, err := file.Read(data)
if err != nil || n != len(data) || !bytes.Equal(data, want[offset:end]) {
t.Fatalf("Read at %d = (%x, %d, %v), want (%x, %d, nil)", offset, data, n, err, want[offset:end], len(data))
}
offset = end
}
if data[0] != 0x00 {
t.Fatalf("expected 0x00, got %x", data[0])
if n, err := file.Read(make([]byte, 1)); n != 0 || err != io.EOF {
t.Fatalf("Read after file contents = (%d, %v), want (0, EOF)", n, err)
}
}
Loaded 100 of 1734 files, more files were not shown because too many files have changed in this diff. Show more