mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-09 09:59:43 -10:00
Compare commits
104
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ccfd6a257 | ||
|
|
4c2e8d4bda | ||
|
|
9f8f1d11be | ||
|
|
6cf3ad78bd | ||
|
|
56e140826f | ||
|
|
ad6ba0a61b | ||
|
|
9e1c1e7c67 | ||
|
|
a36b0b1d1d | ||
|
|
4c1350d8cb | ||
|
|
3e409bdc91 | ||
|
|
46eb1c22a4 | ||
|
|
f857903c83 | ||
|
|
ee53c01313 | ||
|
|
14a43e6510 | ||
|
|
931e3f9046 | ||
|
|
a6d5e97d45 | ||
|
|
f0ef60e25f | ||
|
|
9fdc66e4a8 | ||
|
|
ab960fd4ed | ||
|
|
34fc1c5ad6 | ||
|
|
ac65aa3708 | ||
|
|
73bf782656 | ||
|
|
a0a303ae89 | ||
|
|
5383bd67b5 | ||
|
|
73ea63fd44 | ||
|
|
3ef04241a1 | ||
|
|
402b5459e6 | ||
|
|
7c56553372 | ||
|
|
a40754eaac | ||
|
|
042bb6bccc | ||
|
|
8bc7273cad | ||
|
|
8e5d2f9dbe | ||
|
|
52864c481f | ||
|
|
2f6fca1eac | ||
|
|
481b5b0534 | ||
|
|
65efd3f2fd | ||
|
|
58c5f33cf5 | ||
|
|
3e20660ab0 | ||
|
|
acb6cd2d77 | ||
|
|
7f82085467 | ||
|
|
1ac72de74d | ||
|
|
94df7a3e58 | ||
|
|
329e44dfd9 | ||
|
|
5d303fca79 | ||
|
|
90f759dac8 | ||
|
|
1fc9e256c0 | ||
|
|
13935b2e68 | ||
|
|
1db7b99685 | ||
|
|
b48e3fc537 | ||
|
|
10ec67d274 | ||
|
|
52f5517c2a | ||
|
|
c4ed103182 | ||
|
|
c04b0884a5 | ||
|
|
893fe4ccda | ||
|
|
b43ef0fabe | ||
|
|
25d2bffb5a | ||
|
|
534763f087 | ||
|
|
ec733974de | ||
|
|
d9811baa7e | ||
|
|
d8b360ecea | ||
|
|
12c55be611 | ||
|
|
3a147f1e01 | ||
|
|
5b03320a32 | ||
|
|
1cc6d35dc6 | ||
|
|
1c5d8ab5a9 | ||
|
|
04a71e6d7d | ||
|
|
bb8b90be56 | ||
|
|
7a2b58ade8 | ||
|
|
854143b593 | ||
|
|
d3068394bf | ||
|
|
d80db8a932 | ||
|
|
179ff728dd | ||
|
|
422044af0d | ||
|
|
7dbdd1931c | ||
|
|
8b7e1b63ab | ||
|
|
ed5c89253c | ||
|
|
06b5a56e74 | ||
|
|
704912d25a | ||
|
|
ea1fe95408 | ||
|
|
8fe54f243e | ||
|
|
4fa6fe86fe | ||
|
|
23c5a0b164 | ||
|
|
7b8dafcf55 | ||
|
|
4e6531fa98 | ||
|
|
6e9b2a9820 | ||
|
|
b11f85d4d7 | ||
|
|
23f1341684 | ||
|
|
e68e163456 | ||
|
|
f8430cecf7 | ||
|
|
276a30fbaa | ||
|
|
3aa8a9f93c | ||
|
|
fc3fb7386a | ||
|
|
6edea618e9 | ||
|
|
e920f2f8ea | ||
|
|
5b4a03c9c3 | ||
|
|
e93fabecd6 | ||
|
|
b2cae511e6 | ||
|
|
2e8e53789f | ||
|
|
6a08fb2f7c | ||
|
|
76d38d4677 | ||
|
|
3f87964397 | ||
|
|
bb19ba0665 | ||
|
|
262851373d | ||
|
|
40df2701d1 |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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/...
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@ run:
|
||||
- osusergo
|
||||
- netgo
|
||||
- go_sqlite
|
||||
- sliver_generate
|
||||
# Raw implant entry points are Go templates; lint their supporting packages.
|
||||
- sliver_lint
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml) [](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
|
||||
|
||||
### Features
|
||||
|
||||
- Dynamic code generation
|
||||
- Compile-time obfuscation
|
||||
- Multiplayer-mode
|
||||
- Staged and Stageless payloads
|
||||
- [Procedurally generated C2](https://sliver.sh/docs?name=HTTPS+C2) over HTTP(S)
|
||||
- [DNS canary](https://sliver.sh/docs?name=DNS+C2) blue team detection
|
||||
- [Secure C2](https://sliver.sh/docs?name=Transport+Encryption) over mTLS, WireGuard, HTTP(S), and DNS
|
||||
- Fully scriptable using [Python](https://github.com/moloch--/sliver-py)
|
||||
- Windows process migration, process injection, user token manipulation, etc.
|
||||
- Let's Encrypt integration
|
||||
- In-memory .NET assembly execution
|
||||
- COFF/BOF in-memory loader
|
||||
- TCP and named pipe pivots
|
||||
- Much more!
|
||||
|
||||
### Getting Started
|
||||
|
||||
Download the latest [release](https://github.com/BishopFox/sliver/releases) and see the Sliver [wiki](https://sliver.sh/docs?name=Getting+Started) for a quick tutorial on basic setup and usage. To get the very latest and greatest compile from source.
|
||||
|
||||
+30
-3
@@ -19,6 +19,7 @@ package cli
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -34,6 +35,8 @@ const (
|
||||
logFileName = "sliver-client.log"
|
||||
)
|
||||
|
||||
var clientLogFile *os.File
|
||||
|
||||
// Initialize logging.
|
||||
func initLogging(appDir string) *os.File {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
@@ -47,11 +50,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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
)
|
||||
|
||||
func TestPackageLoggerRemainsOpenAfterInit(t *testing.T) {
|
||||
logFile, ok := log.Writer().(*os.File)
|
||||
if !ok {
|
||||
t.Fatalf("standard logger writer = %T, want *os.File", log.Writer())
|
||||
}
|
||||
if want := filepath.Join(assets.GetRootAppDir(), logFileName); filepath.Clean(logFile.Name()) != want {
|
||||
t.Fatalf("standard logger file = %q, want %q", logFile.Name(), want)
|
||||
}
|
||||
if _, err := logFile.Stat(); err != nil {
|
||||
t.Fatalf("standard logger file is closed after package initialization: %v", err)
|
||||
}
|
||||
}
|
||||
+19
-7
@@ -20,24 +20,37 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
)
|
||||
|
||||
func selectConfig() (string, *assets.ClientConfig) {
|
||||
configs := assets.GetConfigs()
|
||||
func selectConfig(configPath string, stdinTTY bool) (string, *assets.ClientConfig, error) {
|
||||
if configPath != "" {
|
||||
config, err := loadMCPClientConfig(configPath)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("load client config: %w", err)
|
||||
}
|
||||
return filepath.Base(configPath), config, nil
|
||||
}
|
||||
return selectConfigFrom(assets.GetConfigs(), stdinTTY)
|
||||
}
|
||||
|
||||
func selectConfigFrom(configs map[string]*assets.ClientConfig, stdinTTY bool) (string, *assets.ClientConfig, error) {
|
||||
if len(configs) == 0 {
|
||||
return "", nil
|
||||
return "", nil, fmt.Errorf("no config files found at %s", assets.GetConfigDir())
|
||||
}
|
||||
|
||||
if len(configs) == 1 {
|
||||
for key, config := range configs {
|
||||
return key, config
|
||||
return key, config, nil
|
||||
}
|
||||
}
|
||||
if !stdinTTY {
|
||||
return "", nil, fmt.Errorf("multiple configs found; use --config to select one")
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(configs))
|
||||
for key := range configs {
|
||||
@@ -48,9 +61,8 @@ func selectConfig() (string, *assets.ClientConfig) {
|
||||
selection := keys[0]
|
||||
err := forms.Select("Select a server:", keys, &selection)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return "", nil
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return selection, configs[selection]
|
||||
return selection, configs[selection], nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigFlagRoutesBeforeAndAfterConsoleAndMCP(t *testing.T) {
|
||||
flag := rootCmd.PersistentFlags().Lookup(ConsoleConfigFlagName)
|
||||
if flag == nil {
|
||||
t.Fatal("root --config flag is missing")
|
||||
}
|
||||
oldValue, oldChanged := flag.Value.String(), flag.Changed
|
||||
t.Cleanup(func() {
|
||||
_ = flag.Value.Set(oldValue)
|
||||
flag.Changed = oldChanged
|
||||
})
|
||||
|
||||
for _, command := range []string{"console", "mcp"} {
|
||||
for _, before := range []bool{true, false} {
|
||||
name := command + " after --config"
|
||||
args := []string{"--config", "selected.cfg", command}
|
||||
if !before {
|
||||
name = command + " before --config"
|
||||
args = []string{command, "--config", "selected.cfg"}
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_ = flag.Value.Set("")
|
||||
flag.Changed = false
|
||||
cmd, remaining, err := rootCmd.Traverse(args)
|
||||
if err != nil {
|
||||
t.Fatalf("traverse %q: %v", args, err)
|
||||
}
|
||||
if cmd.Name() != command {
|
||||
t.Fatalf("command = %q, want %q", cmd.Name(), command)
|
||||
}
|
||||
if err := cmd.ParseFlags(remaining); err != nil {
|
||||
t.Fatalf("parse %q: %v", remaining, err)
|
||||
}
|
||||
got, err := cmd.Flags().GetString(ConsoleConfigFlagName)
|
||||
if err != nil || got != "selected.cfg" {
|
||||
t.Fatalf("--config = %q, error = %v", got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
)
|
||||
|
||||
func TestSelectConfigFromNonTTYRequiresExplicitConfigWhenMultiple(t *testing.T) {
|
||||
configs := map[string]*assets.ClientConfig{
|
||||
"first": {Operator: "first"},
|
||||
"second": {Operator: "second"},
|
||||
}
|
||||
key, config, err := selectConfigFrom(configs, false)
|
||||
if err == nil || !strings.Contains(err.Error(), "--config") {
|
||||
t.Fatalf("error = %v, want --config guidance", err)
|
||||
}
|
||||
if key != "" || config != nil {
|
||||
t.Fatalf("selected %q (%v) without an explicit config", key, config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectConfigFromNonTTYUsesSoleConfig(t *testing.T) {
|
||||
want := &assets.ClientConfig{Operator: "only"}
|
||||
key, config, err := selectConfigFrom(map[string]*assets.ClientConfig{"only": want}, false)
|
||||
if err != nil || key != "only" || config != want {
|
||||
t.Fatalf("selection = %q (%v), error = %v", key, config, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectConfigWithExplicitPathOnNonTTY(t *testing.T) {
|
||||
want := &assets.ClientConfig{Operator: "selected", LHost: "127.0.0.1"}
|
||||
data, err := json.Marshal(want)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "selected.cfg")
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key, config, err := selectConfig(path, false)
|
||||
if err != nil {
|
||||
t.Fatalf("select explicit config: %v", err)
|
||||
}
|
||||
if key != "selected.cfg" || config.Operator != want.Operator || config.LHost != want.LHost {
|
||||
t.Fatalf("selection = %q (%v)", key, config)
|
||||
}
|
||||
}
|
||||
+25
-17
@@ -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
|
||||
|
||||
@@ -57,7 +57,9 @@ func makeRunners(implantCmd *cobra.Command, con *console.SliverClient) (pre, pos
|
||||
// The pre-run function connects to the server and sets up a "fake" console,
|
||||
// so we can have access to active sessions/beacons, and other stuff needed.
|
||||
pre = func(_ *cobra.Command, args []string) error {
|
||||
startConsole(implantCmd, args)
|
||||
if err := startConsole(implantCmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the active target.
|
||||
target, _ := implantCmd.Flags().GetString("use")
|
||||
|
||||
+21
-13
@@ -20,7 +20,6 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/rsteube/carapace"
|
||||
@@ -32,19 +31,12 @@ func importCmd() *cobra.Command {
|
||||
Use: "import",
|
||||
Short: "Import a client configuration file",
|
||||
Long: `import [config files]`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if 0 < len(args) {
|
||||
for _, arg := range args {
|
||||
conf, err := assets.ReadConfig(arg)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] %s\n", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
assets.SaveConfig(conf)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Missing config file path, see --help")
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
_, _ = fmt.Fprint(cmd.OutOrStdout(), "Missing config file path, see --help")
|
||||
return nil
|
||||
}
|
||||
return importConfigs(args)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{}, cobra.ShellCompDirectiveDefault
|
||||
@@ -55,3 +47,19 @@ func importCmd() *cobra.Command {
|
||||
|
||||
return cmdImport
|
||||
}
|
||||
|
||||
func importConfigs(paths []string) error {
|
||||
for _, path := range paths {
|
||||
conf, err := assets.ReadConfig(path)
|
||||
if err != nil {
|
||||
return &commandExitError{
|
||||
code: 3,
|
||||
err: fmt.Errorf("read import %q: %w", path, err),
|
||||
}
|
||||
}
|
||||
if err := assets.SaveConfig(conf); err != nil {
|
||||
return fmt.Errorf("save import %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeImportConfig(t *testing.T, name, contents string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), name)
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestImportCommandReportsInvalidConfig(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
path := writeImportConfig(t, "missing-operator.cfg", `{"lhost":"server"}`)
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want error identifying %q", err, path)
|
||||
}
|
||||
if got := commandExitCode(err); got != 1 {
|
||||
t.Fatalf("invalid config exit code = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandReportsConfigWriteFailure(t *testing.T) {
|
||||
clientRoot := t.TempDir()
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
|
||||
path := writeImportConfig(t, "alice.cfg", `{"operator":"alice","lhost":"server"}`)
|
||||
|
||||
configDir := filepath.Join(clientRoot, "configs")
|
||||
if err := os.MkdirAll(filepath.Join(configDir, "alice_server.cfg"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want write error identifying %q", err, path)
|
||||
}
|
||||
if got := commandExitCode(err); got != 1 {
|
||||
t.Fatalf("config write failure exit code = %d, want 1", got)
|
||||
}
|
||||
if _, ok := errors.AsType[*os.PathError](err); !ok {
|
||||
t.Fatalf("config write failure does not wrap the filesystem error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandReadFailureExitCode(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
path := filepath.Join(t.TempDir(), "missing.cfg")
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want read error identifying %q", err, path)
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("import error does not wrap the missing file error: %v", err)
|
||||
}
|
||||
if got := commandExitCode(fmt.Errorf("command failed: %w", err)); got != 3 {
|
||||
t.Fatalf("wrapped read failure exit code = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandParseFailureExitCode(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
path := writeImportConfig(t, "malformed.cfg", `{"operator":`)
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want parse error identifying %q", err, path)
|
||||
}
|
||||
if _, ok := errors.AsType[*json.SyntaxError](err); !ok {
|
||||
t.Fatalf("import error does not wrap the JSON syntax error: %v", err)
|
||||
}
|
||||
if got := commandExitCode(err); got != 3 {
|
||||
t.Fatalf("parse failure exit code = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandStopsAfterFirstFailure(t *testing.T) {
|
||||
clientRoot := t.TempDir()
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
|
||||
invalidPath := writeImportConfig(t, "invalid.cfg", `{"lhost":"server"}`)
|
||||
validPath := writeImportConfig(t, "valid.cfg", `{"operator":"alice","lhost":"server"}`)
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{invalidPath, validPath})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(invalidPath)) {
|
||||
t.Fatalf("import error = %v, want first file %q", err, invalidPath)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(clientRoot, "configs", "alice_server.cfg")); !os.IsNotExist(err) {
|
||||
t.Fatalf("later config was imported after first failure: stat error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandMissingPathPreservesSuccess(t *testing.T) {
|
||||
cmd := importCmd()
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
err := cmd.RunE(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("import without a path failed: %v", err)
|
||||
}
|
||||
if got := commandExitCode(err); got != 0 {
|
||||
t.Fatalf("missing path exit code = %d, want 0", got)
|
||||
}
|
||||
if got, want := output.String(), "Missing config file path, see --help"; got != want {
|
||||
t.Fatalf("missing path output = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -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 {
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
package armory
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
cracktop "github.com/bishopfox/sliver/client/command/crack/top"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CrackTopCmd launches the full-screen real-time crack cluster monitor.
|
||||
// The implementation lives in the top subpackage; this facade preserves the
|
||||
// existing command-handler API for callers of client/command/crack.
|
||||
//
|
||||
//nolint:revive // Preserve the established exported command-handler API.
|
||||
func CrackTopCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
cracktop.CrackTopCmd(cmd, con, args)
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
const crackTopActionTestJobID = "dddddddd-4444-4444-8444-444444444444"
|
||||
|
||||
func newCrackTopJobActionTestModel(
|
||||
status clientpb.CrackJobStatus,
|
||||
actions map[crackTopJobAction]crackTopJobActionFunc,
|
||||
) *crackTopModel {
|
||||
snapshot := &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{
|
||||
ID: crackTopActionTestJobID,
|
||||
Status: status,
|
||||
UpdatedAt: 100,
|
||||
Tasks: []*clientpb.CrackTask{{
|
||||
ID: "action-task",
|
||||
HostUUID: "action-station",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
LastHeartbeatAt: 1_700_000_000,
|
||||
}},
|
||||
}},
|
||||
Stations: []*clientpb.Crackstation{{
|
||||
ID: "action-station",
|
||||
HostUUID: "action-station",
|
||||
Status: &clientpb.CrackstationStatus{
|
||||
HostUUID: "action-station",
|
||||
State: clientpb.States_CRACKING,
|
||||
CurrentCrackJobID: crackTopActionTestJobID,
|
||||
},
|
||||
}},
|
||||
RefreshedAt: time.Unix(1_700_000_000, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{
|
||||
"action-task": {status: &crackStatusView{Devices: []crackDeviceView{{Speed: "100"}}}},
|
||||
},
|
||||
}
|
||||
model := newCrackTopModel(context.Background(), nil, nil, time.Second)
|
||||
model.jobActions = actions
|
||||
model.snapshot = snapshot
|
||||
model.dashboard = buildCrackTopDashboard(snapshot)
|
||||
model.refreshing = false
|
||||
model.width = 100
|
||||
model.height = 28
|
||||
model.normalizeJobSelection()
|
||||
return model
|
||||
}
|
||||
|
||||
func singleCrackTopAction(action crackTopJobAction, actionJob crackTopJobActionFunc) map[crackTopJobAction]crackTopJobActionFunc {
|
||||
return map[crackTopJobAction]crackTopJobActionFunc{action: actionJob}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteConfirmsAndRemovesTerminalJob(t *testing.T) {
|
||||
deletedID := ""
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
deletedID = jobID
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, openCommand := model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
if openCommand != nil || model.pendingJobAction != (crackTopPendingJobAction{action: crackTopJobActionDelete, jobID: crackTopActionTestJobID}) {
|
||||
t.Fatalf("delete prompt = command:%v prompt:%#v", openCommand != nil, model.pendingJobAction)
|
||||
}
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"Delete crack job?", crackTopActionTestJobID, "This cannot be undone", "y delete"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Fatalf("delete confirmation is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
if got := lipgloss.Width(model.View().Content); got > model.width {
|
||||
t.Fatalf("delete modal width = %d, want <= %d", got, model.width)
|
||||
}
|
||||
|
||||
_, deleteCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if deleteCommand == nil || model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != crackTopActionTestJobID {
|
||||
t.Fatalf("confirmed delete = command:%v pending:%#v running:%#v", deleteCommand != nil, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
rawMessage := deleteCommand()
|
||||
message, ok := rawMessage.(crackTopJobActionCompletedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("delete command returned %T, want crackTopJobActionCompletedMsg", rawMessage)
|
||||
}
|
||||
if deletedID != crackTopActionTestJobID || message.action != crackTopJobActionDelete || message.jobID != crackTopActionTestJobID || message.err != nil {
|
||||
t.Fatalf("delete call/message = called:%q message:%#v", deletedID, message)
|
||||
}
|
||||
|
||||
_, refreshCommand := model.Update(message)
|
||||
if refreshCommand == nil || !model.refreshing {
|
||||
t.Fatalf("successful delete refresh = command:%v refreshing:%v", refreshCommand != nil, model.refreshing)
|
||||
}
|
||||
if len(model.snapshot.Jobs) != 0 || len(model.dashboard.Jobs) != 0 || model.selectedJobID != "" {
|
||||
t.Fatalf("deleted job remains in dashboard: snapshot=%#v dashboard=%#v selected=%q", model.snapshot.Jobs, model.dashboard.Jobs, model.selectedJobID)
|
||||
}
|
||||
if _, ok := model.snapshot.taskTelemetry["action-task"]; ok {
|
||||
t.Fatal("deleted job telemetry remains cached")
|
||||
}
|
||||
if model.toastLevel != "success" || !strings.Contains(model.toast, "Deleted crack job dddddddd") {
|
||||
t.Fatalf("success toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteRejectsInProgressJob(t *testing.T) {
|
||||
deleteCalls := 0
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
deleteCalls++
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, command := model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
if command == nil {
|
||||
t.Fatal("in-progress delete did not schedule a warning")
|
||||
}
|
||||
if model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != "" || deleteCalls != 0 {
|
||||
t.Fatalf("in-progress delete changed state: pending=%#v running=%#v calls=%d", model.pendingJobAction, model.runningJobAction, deleteCalls)
|
||||
}
|
||||
if model.toastLevel != "warning" || !strings.Contains(model.toast, "Only completed, failed, or cancelled crack jobs can be deleted") {
|
||||
t.Fatalf("in-progress warning = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteFailureRetainsJobAndEscapeCancels(t *testing.T) {
|
||||
wantErr := errors.New("database is busy\ntry later")
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_FAILED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
return nil, wantErr
|
||||
}),
|
||||
)
|
||||
|
||||
_, _ = model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
_, cancelCommand := model.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
|
||||
if cancelCommand != nil || model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != "" {
|
||||
t.Fatalf("escaped delete prompt = command:%v pending=%#v running=%#v", cancelCommand != nil, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
|
||||
_, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyDelete})
|
||||
_, enterCommand := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
|
||||
if enterCommand != nil || model.pendingJobAction.jobID != crackTopActionTestJobID || model.runningJobAction.jobID != "" {
|
||||
t.Fatalf("bare enter confirmed destructive action: command=%v pending=%#v running=%#v", enterCommand != nil, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
_, deleteCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if deleteCommand == nil {
|
||||
t.Fatal("y did not confirm deletion")
|
||||
}
|
||||
message := deleteCommand().(crackTopJobActionCompletedMsg)
|
||||
_, toastCommand := model.Update(message)
|
||||
if toastCommand == nil || model.refreshing {
|
||||
t.Fatalf("failed delete = toast command:%v refreshing:%v", toastCommand != nil, model.refreshing)
|
||||
}
|
||||
if len(model.snapshot.Jobs) != 1 || model.snapshot.Jobs[0].GetID() != crackTopActionTestJobID {
|
||||
t.Fatalf("failed delete removed job: %#v", model.snapshot.Jobs)
|
||||
}
|
||||
if model.toastLevel != "error" || !strings.Contains(model.toast, "database is busy try later") || strings.Contains(model.toast, "\n") {
|
||||
t.Fatalf("failure toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopResizeBelowMinimumCancelsHiddenConfirmation(t *testing.T) {
|
||||
deleteCalls := 0
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
deleteCalls++
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, _ = model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
if model.pendingJobAction.jobID == "" {
|
||||
t.Fatal("delete confirmation did not open")
|
||||
}
|
||||
_, resizeCommand := model.Update(tea.WindowSizeMsg{Width: crackTopMinWidth - 1, Height: crackTopMinHeight})
|
||||
if resizeCommand != nil || model.pendingJobAction.jobID != "" {
|
||||
t.Fatalf("small resize left hidden confirmation actionable: command=%v pending=%#v", resizeCommand != nil, model.pendingJobAction)
|
||||
}
|
||||
_, confirmCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if confirmCommand != nil || model.runningJobAction.jobID != "" || deleteCalls != 0 {
|
||||
t.Fatalf("hidden confirmation executed: command=%v running=%#v calls=%d", confirmCommand != nil, model.runningJobAction, deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopBelowMinimumCannotOpenHiddenConfirmation(t *testing.T) {
|
||||
deleteCalls := 0
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
deleteCalls++
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
_, _ = model.Update(tea.WindowSizeMsg{Width: crackTopMinWidth, Height: crackTopMinHeight - 1})
|
||||
|
||||
_, openCommand := model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
_, confirmCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if openCommand != nil || confirmCommand != nil || model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != "" || deleteCalls != 0 {
|
||||
t.Fatalf("undersized dashboard accepted hidden delete: open=%v confirm=%v pending=%#v running=%#v calls=%d",
|
||||
openCommand != nil, confirmCommand != nil, model.pendingJobAction, model.runningJobAction, deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteAllowsEveryTerminalStatus(t *testing.T) {
|
||||
for _, status := range []clientpb.CrackJobStatus{
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
clientpb.CrackJobStatus_FAILED,
|
||||
clientpb.CrackJobStatus_CANCELLED,
|
||||
} {
|
||||
t.Run(status.String(), func(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
status,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) { return nil, nil }),
|
||||
)
|
||||
_, command := model.Update(tea.KeyPressMsg{Text: "d", Code: 'd'})
|
||||
if command != nil || model.pendingJobAction.jobID != crackTopActionTestJobID {
|
||||
t.Fatalf("terminal status %s did not open delete confirmation", status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopDeleteSuppressesStaleRefreshUntilServerOmitsJob(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_COMPLETED,
|
||||
singleCrackTopAction(crackTopJobActionDelete, func(context.Context, string) (*clientpb.CrackJob, error) { return nil, nil }),
|
||||
)
|
||||
model.removeJob(crackTopActionTestJobID)
|
||||
|
||||
stale := &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{ID: crackTopActionTestJobID, Status: clientpb.CrackJobStatus_COMPLETED, Tasks: []*clientpb.CrackTask{{ID: "stale-task"}}}},
|
||||
RefreshedAt: time.Unix(1_700_000_001, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{"stale-task": {}},
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: stale})
|
||||
if len(model.snapshot.Jobs) != 0 || len(model.dashboard.Jobs) != 0 {
|
||||
t.Fatalf("stale refresh restored deleted job: snapshot=%#v dashboard=%#v", model.snapshot.Jobs, model.dashboard.Jobs)
|
||||
}
|
||||
if _, ok := model.snapshot.taskTelemetry["stale-task"]; ok {
|
||||
t.Fatal("stale refresh retained deleted job telemetry")
|
||||
}
|
||||
if _, ok := model.deletedJobIDs[crackTopActionTestJobID]; !ok {
|
||||
t.Fatal("stale refresh cleared the deletion tombstone")
|
||||
}
|
||||
|
||||
fresh := &crackTopSnapshot{RefreshedAt: time.Unix(1_700_000_002, 0)}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: fresh})
|
||||
if _, ok := model.deletedJobIDs[crackTopActionTestJobID]; ok {
|
||||
t.Fatal("server-confirmed refresh did not clear the deletion tombstone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopPauseConfirmsAppliesAuthoritativeStatusAndRefreshes(t *testing.T) {
|
||||
pausedID := ""
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
singleCrackTopAction(crackTopJobActionPause, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
pausedID = jobID
|
||||
return &clientpb.CrackJob{ID: jobID, Status: clientpb.CrackJobStatus_PAUSED, UpdatedAt: 200}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, openCommand := model.Update(tea.KeyPressMsg{Text: "p", Code: 'p'})
|
||||
if openCommand != nil || model.pendingJobAction != (crackTopPendingJobAction{action: crackTopJobActionPause, jobID: crackTopActionTestJobID}) {
|
||||
t.Fatalf("pause prompt = command:%v prompt:%#v", openCommand != nil, model.pendingJobAction)
|
||||
}
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"Pause crack job?", "release its current crackstation work", "may restart", "y pause"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Fatalf("pause confirmation is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
_, enterCommand := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
|
||||
if enterCommand != nil || model.runningJobAction.jobID != "" {
|
||||
t.Fatal("enter confirmed pause without an explicit y")
|
||||
}
|
||||
|
||||
_, pauseCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
if pauseCommand == nil || model.runningJobAction.action != crackTopJobActionPause {
|
||||
t.Fatalf("pause confirmation = command:%v running:%#v", pauseCommand != nil, model.runningJobAction)
|
||||
}
|
||||
message := pauseCommand().(crackTopJobActionCompletedMsg)
|
||||
if pausedID != crackTopActionTestJobID || message.status != clientpb.CrackJobStatus_PAUSED || !message.hasStatus {
|
||||
t.Fatalf("pause call/message = called:%q message:%#v", pausedID, message)
|
||||
}
|
||||
_, refreshCommand := model.Update(message)
|
||||
if refreshCommand == nil || !model.refreshing {
|
||||
t.Fatalf("successful pause refresh = command:%v refreshing:%v", refreshCommand != nil, model.refreshing)
|
||||
}
|
||||
if got := model.snapshot.Jobs[0].GetStatus(); got != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("local pause status = %s, want PAUSED", got)
|
||||
}
|
||||
if model.dashboard.Jobs[0].RateKnown || model.dashboard.ActiveJobs != 0 {
|
||||
t.Fatalf("paused job retained active throughput: row=%#v active=%d", model.dashboard.Jobs[0], model.dashboard.ActiveJobs)
|
||||
}
|
||||
if model.toastLevel != "success" || !strings.Contains(model.toast, "Paused crack job dddddddd") {
|
||||
t.Fatalf("pause toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopCancelConfirmsActiveAndPausedJobs(t *testing.T) {
|
||||
for _, status := range []clientpb.CrackJobStatus{clientpb.CrackJobStatus_IN_PROGRESS, clientpb.CrackJobStatus_PAUSED} {
|
||||
t.Run(status.String(), func(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
status,
|
||||
singleCrackTopAction(crackTopJobActionCancel, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
return &clientpb.CrackJob{ID: jobID, Status: clientpb.CrackJobStatus_CANCELLED, UpdatedAt: 200}, nil
|
||||
}),
|
||||
)
|
||||
_, openCommand := model.Update(tea.KeyPressMsg{Text: "c", Code: 'c'})
|
||||
if openCommand != nil || model.pendingJobAction.action != crackTopJobActionCancel {
|
||||
t.Fatalf("cancel prompt = command:%v prompt:%#v", openCommand != nil, model.pendingJobAction)
|
||||
}
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"Cancel crack job?", "Cancelled jobs cannot be resumed", "y cancel"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Fatalf("cancel confirmation is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
_, cancelCommand := model.Update(tea.KeyPressMsg{Text: "y", Code: 'y'})
|
||||
message := cancelCommand().(crackTopJobActionCompletedMsg)
|
||||
_, refreshCommand := model.Update(message)
|
||||
if refreshCommand == nil || model.snapshot.Jobs[0].GetStatus() != clientpb.CrackJobStatus_CANCELLED {
|
||||
t.Fatalf("cancel result = command:%v job:%#v", refreshCommand != nil, model.snapshot.Jobs[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopResumePausedJobWithoutDestructiveConfirmation(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_PAUSED,
|
||||
singleCrackTopAction(crackTopJobActionResume, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
return &clientpb.CrackJob{ID: jobID, Status: clientpb.CrackJobStatus_IN_PROGRESS, UpdatedAt: 300}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, resumeCommand := model.Update(tea.KeyPressMsg{Text: "u", Code: 'u'})
|
||||
if resumeCommand == nil || model.pendingJobAction.jobID != "" || model.runningJobAction.action != crackTopJobActionResume {
|
||||
t.Fatalf("resume start = command:%v pending:%#v running:%#v", resumeCommand != nil, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
plain := strings.ToLower(ansi.Strip(model.View().Content))
|
||||
if !strings.Contains(plain, "resuming crack job dddddddd") {
|
||||
t.Fatalf("resume progress footer missing:\n%s", plain)
|
||||
}
|
||||
message := resumeCommand().(crackTopJobActionCompletedMsg)
|
||||
_, refreshCommand := model.Update(message)
|
||||
if refreshCommand == nil || model.snapshot.Jobs[0].GetStatus() != clientpb.CrackJobStatus_IN_PROGRESS {
|
||||
t.Fatalf("resume result = command:%v job:%#v", refreshCommand != nil, model.snapshot.Jobs[0])
|
||||
}
|
||||
if model.toastLevel != "success" || !strings.Contains(model.toast, "Resumed crack job dddddddd") {
|
||||
t.Fatalf("resume toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleActionsRejectIneligibleStatuses(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key rune
|
||||
action crackTopJobAction
|
||||
status clientpb.CrackJobStatus
|
||||
want string
|
||||
}{
|
||||
{name: "pause paused", key: 'p', action: crackTopJobActionPause, status: clientpb.CrackJobStatus_PAUSED, want: "Only in-progress crack jobs can be paused"},
|
||||
{name: "resume active", key: 'u', action: crackTopJobActionResume, status: clientpb.CrackJobStatus_IN_PROGRESS, want: "Only paused crack jobs can be resumed"},
|
||||
{name: "cancel complete", key: 'c', action: crackTopJobActionCancel, status: clientpb.CrackJobStatus_COMPLETED, want: "Only in-progress or paused crack jobs can be cancelled"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
calls := 0
|
||||
model := newCrackTopJobActionTestModel(
|
||||
test.status,
|
||||
singleCrackTopAction(test.action, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
calls++
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
_, command := model.Update(tea.KeyPressMsg{Text: string(test.key), Code: test.key})
|
||||
if command == nil || calls != 0 || model.pendingJobAction.jobID != "" || model.runningJobAction.jobID != "" {
|
||||
t.Fatalf("ineligible action = command:%v calls:%d pending:%#v running:%#v", command != nil, calls, model.pendingJobAction, model.runningJobAction)
|
||||
}
|
||||
if model.toastLevel != "warning" || !strings.Contains(model.toast, test.want) {
|
||||
t.Fatalf("ineligible toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleRejectsNonAuthoritativeSuccessResponse(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_PAUSED,
|
||||
singleCrackTopAction(crackTopJobActionResume, func(context.Context, string) (*clientpb.CrackJob, error) {
|
||||
return nil, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, resumeCommand := model.Update(tea.KeyPressMsg{Text: "u", Code: 'u'})
|
||||
message := resumeCommand().(crackTopJobActionCompletedMsg)
|
||||
if message.err == nil || !strings.Contains(message.err.Error(), "no job state") {
|
||||
t.Fatalf("empty resume response error = %v", message.err)
|
||||
}
|
||||
_, toastCommand := model.Update(message)
|
||||
if toastCommand == nil || model.refreshing || model.snapshot.Jobs[0].GetStatus() != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("empty resume response mutated state: command=%v refreshing=%v job=%#v", toastCommand != nil, model.refreshing, model.snapshot.Jobs[0])
|
||||
}
|
||||
if model.toastLevel != "error" || !strings.Contains(model.toast, "server returned no job state") {
|
||||
t.Fatalf("empty resume toast = level:%q text:%q", model.toastLevel, model.toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleRejectsIDOnlyResumeResponse(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(
|
||||
clientpb.CrackJobStatus_PAUSED,
|
||||
singleCrackTopAction(crackTopJobActionResume, func(_ context.Context, jobID string) (*clientpb.CrackJob, error) {
|
||||
return &clientpb.CrackJob{ID: jobID}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, resumeCommand := model.Update(tea.KeyPressMsg{Text: "u", Code: 'u'})
|
||||
message := resumeCommand().(crackTopJobActionCompletedMsg)
|
||||
if message.err == nil || !strings.Contains(message.err.Error(), "without an update timestamp") {
|
||||
t.Fatalf("ID-only resume response error = %v", message.err)
|
||||
}
|
||||
_, toastCommand := model.Update(message)
|
||||
if toastCommand == nil || model.refreshing || model.snapshot.Jobs[0].GetStatus() != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("ID-only resume response mutated state: command=%v refreshing=%v job=%#v", toastCommand != nil, model.refreshing, model.snapshot.Jobs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleStatusOverrideSuppressesStaleRefresh(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(clientpb.CrackJobStatus_IN_PROGRESS, nil)
|
||||
model.overrideJobStatus(crackTopActionTestJobID, clientpb.CrackJobStatus_PAUSED, 200)
|
||||
|
||||
stale := &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{ID: crackTopActionTestJobID, Status: clientpb.CrackJobStatus_IN_PROGRESS, UpdatedAt: 100}},
|
||||
RefreshedAt: time.Unix(1_700_000_001, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{},
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: stale})
|
||||
if got := model.snapshot.Jobs[0].GetStatus(); got != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("stale refresh status = %s, want PAUSED", got)
|
||||
}
|
||||
if _, ok := model.jobStatusOverrides[crackTopActionTestJobID]; !ok {
|
||||
t.Fatal("stale refresh cleared authoritative status override")
|
||||
}
|
||||
|
||||
observed := &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{ID: crackTopActionTestJobID, Status: clientpb.CrackJobStatus_PAUSED, UpdatedAt: 200}},
|
||||
RefreshedAt: time.Unix(1_700_000_002, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{},
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: observed})
|
||||
if _, ok := model.jobStatusOverrides[crackTopActionTestJobID]; ok {
|
||||
t.Fatal("server-confirmed refresh did not clear status override")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopLifecycleStatusOverrideYieldsToRepeatedSameSecondConflict(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(clientpb.CrackJobStatus_IN_PROGRESS, nil)
|
||||
model.overrideJobStatus(crackTopActionTestJobID, clientpb.CrackJobStatus_PAUSED, 200)
|
||||
|
||||
conflictingSnapshot := func(refreshedAt int64) *crackTopSnapshot {
|
||||
return &crackTopSnapshot{
|
||||
Jobs: []*clientpb.CrackJob{{
|
||||
ID: crackTopActionTestJobID,
|
||||
Status: clientpb.CrackJobStatus_CANCELLED,
|
||||
UpdatedAt: 200,
|
||||
}},
|
||||
RefreshedAt: time.Unix(refreshedAt, 0),
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{},
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: conflictingSnapshot(1_700_000_001)})
|
||||
if got := model.snapshot.Jobs[0].GetStatus(); got != clientpb.CrackJobStatus_PAUSED {
|
||||
t.Fatalf("first same-second conflict status = %s, want PAUSED stale-snapshot mask", got)
|
||||
}
|
||||
override, ok := model.jobStatusOverrides[crackTopActionTestJobID]
|
||||
if !ok || !override.equalVersionDisagreementSeen {
|
||||
t.Fatalf("first same-second conflict override = %#v, present=%v", override, ok)
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: conflictingSnapshot(1_700_000_002)})
|
||||
if got := model.snapshot.Jobs[0].GetStatus(); got != clientpb.CrackJobStatus_CANCELLED {
|
||||
t.Fatalf("repeated same-second conflict status = %s, want CANCELLED", got)
|
||||
}
|
||||
if _, ok := model.jobStatusOverrides[crackTopActionTestJobID]; ok {
|
||||
t.Fatal("repeated same-second conflict did not release status override")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopActiveFilterRetainsPausedJob(t *testing.T) {
|
||||
model := newCrackTopJobActionTestModel(clientpb.CrackJobStatus_PAUSED, nil)
|
||||
model.filter = crackTopFilterActive
|
||||
model.normalizeJobSelection()
|
||||
|
||||
jobs := model.filteredJobs()
|
||||
if len(jobs) != 1 || jobs[0].ID != crackTopActionTestJobID || model.selectedJobID != crackTopActionTestJobID {
|
||||
t.Fatalf("active filter lost paused job: jobs=%#v selected=%q", jobs, model.selectedJobID)
|
||||
}
|
||||
if model.dashboard.ActiveJobs != 0 {
|
||||
t.Fatalf("paused job counted as active throughput work: %d", model.dashboard.ActiveJobs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package top
|
||||
|
||||
import "time"
|
||||
|
||||
const crackTopHistoryLimit = 120
|
||||
|
||||
// crackTopHistorySample captures the real telemetry available at one refresh
|
||||
// observation. Known fields distinguish an observed zero from telemetry that
|
||||
// was unavailable, including when the refresh itself failed.
|
||||
type crackTopHistorySample struct {
|
||||
At time.Time
|
||||
|
||||
ClusterRate uint64
|
||||
ClusterRateKnown bool
|
||||
ClusterComplete bool
|
||||
Progress float64
|
||||
ProgressKnown bool
|
||||
Utilization float64
|
||||
UtilizationKnown bool
|
||||
Temperature float64
|
||||
TemperatureKnown bool
|
||||
CountsKnown bool
|
||||
|
||||
ActiveJobs int
|
||||
OnlineWorkers int
|
||||
TotalWorkers int
|
||||
Recovered uint64
|
||||
}
|
||||
|
||||
func crackTopHistorySampleFromDashboard(dashboard crackTopDashboard, at time.Time) crackTopHistorySample {
|
||||
sample := crackTopHistorySample{
|
||||
At: at,
|
||||
ActiveJobs: dashboard.ActiveJobs,
|
||||
OnlineWorkers: dashboard.OnlineWorkers,
|
||||
TotalWorkers: dashboard.OnlineWorkers + dashboard.Unavailable,
|
||||
Recovered: dashboard.Recovered,
|
||||
ClusterRateKnown: dashboard.ClusterRateKnown,
|
||||
ClusterComplete: dashboard.ClusterComplete,
|
||||
ProgressKnown: dashboard.ProgressKnown,
|
||||
CountsKnown: true,
|
||||
}
|
||||
if sample.ClusterRateKnown {
|
||||
sample.ClusterRate = dashboard.ClusterRate
|
||||
}
|
||||
if sample.ProgressKnown {
|
||||
sample.Progress = dashboard.Progress
|
||||
}
|
||||
|
||||
var utilizationTotal float64
|
||||
utilizationSamples := 0
|
||||
for _, worker := range dashboard.Workers {
|
||||
if !worker.TelemetryLive {
|
||||
continue
|
||||
}
|
||||
if worker.HasUtil {
|
||||
utilizationTotal += worker.Utilization
|
||||
utilizationSamples++
|
||||
}
|
||||
if worker.HasTemp && (!sample.TemperatureKnown || worker.Temperature > sample.Temperature) {
|
||||
sample.Temperature = worker.Temperature
|
||||
sample.TemperatureKnown = true
|
||||
}
|
||||
}
|
||||
if utilizationSamples > 0 {
|
||||
sample.Utilization = utilizationTotal / float64(utilizationSamples)
|
||||
sample.UtilizationKnown = true
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func appendCrackTopHistory(history []crackTopHistorySample, sample crackTopHistorySample) []crackTopHistorySample {
|
||||
if len(history) >= crackTopHistoryLimit {
|
||||
start := len(history) - (crackTopHistoryLimit - 1)
|
||||
retained := copy(history, history[start:])
|
||||
history = history[:retained]
|
||||
}
|
||||
return append(history, sample)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
//nolint:gocyclo // This test validates every derived history field in one complete sample.
|
||||
func TestCrackTopHistorySampleFromDashboardDerivesLiveTelemetry(t *testing.T) {
|
||||
at := time.Unix(1_700_000_000, 123)
|
||||
dashboard := crackTopDashboard{
|
||||
ActiveJobs: 2,
|
||||
OnlineWorkers: 3,
|
||||
Unavailable: 2,
|
||||
Recovered: 9,
|
||||
ClusterRate: 12_345,
|
||||
ClusterRateKnown: true,
|
||||
ClusterComplete: true,
|
||||
Progress: 0.625,
|
||||
ProgressKnown: true,
|
||||
Workers: []crackTopWorkerRow{
|
||||
{TelemetryLive: true, Utilization: 20, HasUtil: true, Temperature: 60, HasTemp: true},
|
||||
{TelemetryLive: true, Utilization: 80, HasUtil: true, Temperature: 75, HasTemp: true},
|
||||
{TelemetryLive: true},
|
||||
{TelemetryLive: false, Utilization: 100, HasUtil: true, Temperature: 99, HasTemp: true},
|
||||
},
|
||||
}
|
||||
|
||||
sample := crackTopHistorySampleFromDashboard(dashboard, at)
|
||||
if !sample.At.Equal(at) {
|
||||
t.Fatalf("sample time = %s, want %s", sample.At, at)
|
||||
}
|
||||
if !sample.ClusterRateKnown || sample.ClusterRate != 12_345 {
|
||||
t.Fatalf("cluster rate = %d known %v, want 12345 known", sample.ClusterRate, sample.ClusterRateKnown)
|
||||
}
|
||||
if !sample.ClusterComplete {
|
||||
t.Fatal("complete cluster snapshot was recorded as incomplete")
|
||||
}
|
||||
if !sample.ProgressKnown || sample.Progress != 0.625 {
|
||||
t.Fatalf("progress = %v known %v, want 0.625 known", sample.Progress, sample.ProgressKnown)
|
||||
}
|
||||
if !sample.UtilizationKnown || sample.Utilization != 50 {
|
||||
t.Fatalf("utilization = %v known %v, want live-worker mean 50", sample.Utilization, sample.UtilizationKnown)
|
||||
}
|
||||
if !sample.TemperatureKnown || sample.Temperature != 75 {
|
||||
t.Fatalf("temperature = %v known %v, want live-worker maximum 75", sample.Temperature, sample.TemperatureKnown)
|
||||
}
|
||||
if sample.ActiveJobs != 2 || sample.OnlineWorkers != 3 || sample.TotalWorkers != 5 || sample.Recovered != 9 {
|
||||
t.Fatalf("sample counts = active:%d online:%d total:%d recovered:%d", sample.ActiveJobs, sample.OnlineWorkers, sample.TotalWorkers, sample.Recovered)
|
||||
}
|
||||
if !sample.CountsKnown {
|
||||
t.Fatal("successful dashboard sample marked counts unknown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopHistorySampleFromDashboardPreservesUnknowns(t *testing.T) {
|
||||
dashboard := crackTopDashboard{
|
||||
ClusterRate: 999,
|
||||
Progress: 0.9,
|
||||
Workers: []crackTopWorkerRow{
|
||||
{TelemetryLive: false, Utilization: 88, HasUtil: true, Temperature: 70, HasTemp: true},
|
||||
{TelemetryLive: true},
|
||||
},
|
||||
}
|
||||
|
||||
sample := crackTopHistorySampleFromDashboard(dashboard, time.Time{})
|
||||
if sample.ClusterRateKnown || sample.ClusterRate != 0 {
|
||||
t.Fatalf("unknown cluster rate = %d known %v, want zero unknown", sample.ClusterRate, sample.ClusterRateKnown)
|
||||
}
|
||||
if sample.ClusterComplete {
|
||||
t.Fatal("incomplete cluster snapshot was recorded as complete")
|
||||
}
|
||||
if sample.ProgressKnown || sample.Progress != 0 {
|
||||
t.Fatalf("unknown progress = %v known %v, want zero unknown", sample.Progress, sample.ProgressKnown)
|
||||
}
|
||||
if sample.UtilizationKnown || sample.Utilization != 0 {
|
||||
t.Fatalf("unknown utilization = %v known %v, want zero unknown", sample.Utilization, sample.UtilizationKnown)
|
||||
}
|
||||
if sample.TemperatureKnown || sample.Temperature != 0 {
|
||||
t.Fatalf("unknown temperature = %v known %v, want zero unknown", sample.Temperature, sample.TemperatureKnown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendCrackTopHistoryCapsAndRetainsNewestSamples(t *testing.T) {
|
||||
history := make([]crackTopHistorySample, 0, crackTopHistoryLimit)
|
||||
for index := 0; index < crackTopHistoryLimit+5; index++ {
|
||||
history = appendCrackTopHistory(history, crackTopHistorySample{ActiveJobs: index})
|
||||
}
|
||||
|
||||
if len(history) != crackTopHistoryLimit {
|
||||
t.Fatalf("history length = %d, want %d", len(history), crackTopHistoryLimit)
|
||||
}
|
||||
if history[0].ActiveJobs != 5 {
|
||||
t.Fatalf("oldest retained sample = %d, want 5", history[0].ActiveJobs)
|
||||
}
|
||||
if history[len(history)-1].ActiveJobs != crackTopHistoryLimit+4 {
|
||||
t.Fatalf("newest retained sample = %d, want %d", history[len(history)-1].ActiveJobs, crackTopHistoryLimit+4)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package crack
|
||||
package top
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package crack
|
||||
package top
|
||||
|
||||
import (
|
||||
"context"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,598 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
//nolint:gocyclo // This test validates all history fields across success, gap, failure, and recovery snapshots.
|
||||
func TestCrackTopAcceptedSnapshotsAppendRealOverviewHistory(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
first := crackTopLiveTestSnapshot()
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: first})
|
||||
if len(model.history) != 1 {
|
||||
t.Fatalf("history length after first accepted snapshot = %d, want 1", len(model.history))
|
||||
}
|
||||
if sample := model.history[0]; !sample.At.Equal(first.RefreshedAt) ||
|
||||
!sample.ClusterRateKnown || sample.ClusterRate != 2_502_000 ||
|
||||
!sample.ProgressKnown || sample.Progress != 0.3125 ||
|
||||
!sample.UtilizationKnown || sample.Utilization != 98.5 ||
|
||||
!sample.TemperatureKnown || sample.Temperature != 70 {
|
||||
t.Fatalf("first history sample does not reflect live fixture telemetry: %#v", sample)
|
||||
}
|
||||
|
||||
gapTime := first.RefreshedAt.Add(time.Second)
|
||||
unknownGap := &crackTopSnapshot{
|
||||
Stations: []*clientpb.Crackstation{{
|
||||
HostUUID: "worker-without-status",
|
||||
Name: "Awaiting Status",
|
||||
}},
|
||||
RefreshedAt: gapTime,
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: unknownGap})
|
||||
if len(model.history) != 2 {
|
||||
t.Fatalf("history length after second accepted snapshot = %d, want 2", len(model.history))
|
||||
}
|
||||
gap := model.history[1]
|
||||
if !gap.At.Equal(gapTime) {
|
||||
t.Fatalf("unknown-gap time = %s, want %s", gap.At, gapTime)
|
||||
}
|
||||
if gap.ClusterRateKnown || gap.ProgressKnown || gap.UtilizationKnown || gap.TemperatureKnown {
|
||||
t.Fatalf("unreported telemetry was recorded as known instead of an unknown gap: %#v", gap)
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{err: errors.New("refresh failed")})
|
||||
if len(model.history) != 3 {
|
||||
t.Fatalf("failed refresh history length = %d, want an explicit third gap sample", len(model.history))
|
||||
}
|
||||
failure := model.history[2]
|
||||
if failure.ClusterRateKnown || failure.ProgressKnown || failure.UtilizationKnown || failure.TemperatureKnown || failure.CountsKnown {
|
||||
t.Fatalf("failed refresh did not produce an unknown chart gap: %#v", failure)
|
||||
}
|
||||
|
||||
recovered := crackTopLiveTestSnapshot()
|
||||
recovered.RefreshedAt = gapTime.Add(time.Second)
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: recovered})
|
||||
if len(model.history) != 4 || !model.history[3].ClusterRateKnown {
|
||||
t.Fatalf("recovered refresh did not resume live history after the gap: %#v", model.history)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopWideOverviewRendersRealDataCardsChartsAndJournal(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
model.refreshing = false
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: crackTopLiveTestSnapshot()})
|
||||
|
||||
view := model.View()
|
||||
if !view.AltScreen {
|
||||
t.Fatal("wide crack top overview did not request the alternate screen")
|
||||
}
|
||||
plain := ansi.Strip(view.Content)
|
||||
for _, expected := range []string{
|
||||
"CLUSTER / STATE",
|
||||
"THROUGHPUT",
|
||||
"SAMPLE AVG",
|
||||
"DIAGNOSTICS",
|
||||
"KEYSPACE / PROGRESS",
|
||||
"OBSERVED WORKERS",
|
||||
"DEVICE LOAD",
|
||||
"rate",
|
||||
"progress",
|
||||
"worker max util",
|
||||
"RECENT JOURNAL",
|
||||
"ACTIVE 1",
|
||||
"JOBS 1 loaded",
|
||||
"2/2 online",
|
||||
"2.50 MH/s",
|
||||
"RECOVERED 7 loaded",
|
||||
"31.2%",
|
||||
"98% avg worker max",
|
||||
"70°C max",
|
||||
"QUEUE 2 run 0 leased 0 waiting",
|
||||
"Alpha Rig",
|
||||
"Beta Rig",
|
||||
"snapshot ready",
|
||||
} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Errorf("wide overview does not contain %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
if got := ansi.StringWidth(strings.Split(view.Content, "\n")[0]); got > model.width {
|
||||
t.Fatalf("wide overview first-line width = %d, want <= %d", got, model.width)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewDoesNotPresentDefaultsAsLiveBeforeSnapshot(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = crackTopOverviewWidth
|
||||
model.height = crackTopOverviewHeight
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"WAITING", "first live snapshot", "ACTIVE —", "RECOVERED —", "QUEUE —"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Errorf("pre-snapshot overview is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
for _, unsupported := range []string{"HEALTHY", "OK LIVE", "ACTIVE 0", "RECOVERED 0", "QUEUE 0"} {
|
||||
if strings.Contains(plain, unsupported) {
|
||||
t.Errorf("pre-snapshot overview presented %q as observed:\n%s", unsupported, plain)
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{err: errors.New("rpc unavailable")})
|
||||
failed := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"NO SNAPSHOT", "refresh failed", "ERROR", "no live snapshot"} {
|
||||
if !strings.Contains(failed, expected) {
|
||||
t.Errorf("initial-failure overview is missing %q:\n%s", expected, failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopCompactViewDoesNotPresentDefaultsAsLiveBeforeSnapshot(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 90
|
||||
model.height = 28
|
||||
model.refreshing = false
|
||||
|
||||
plain := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"waiting for data", "ACTIVE —", "JOBS —", "WORKERS —", "RECOVERED —", "awaiting first snapshot"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Errorf("compact pre-snapshot view is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
for _, unsupported := range []string{"ACTIVE 0", "JOBS 0 loaded", "WORKERS 0/0", "RECOVERED 0", "no active jobs"} {
|
||||
if strings.Contains(plain, unsupported) {
|
||||
t.Errorf("compact pre-snapshot view presented %q as observed:\n%s", unsupported, plain)
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = model.Update(crackTopSnapshotMsg{err: errors.New("rpc unavailable")})
|
||||
failed := ansi.Strip(model.View().Content)
|
||||
for _, expected := range []string{"no snapshot", "ACTIVE —", "current data unavailable"} {
|
||||
if !strings.Contains(failed, expected) {
|
||||
t.Errorf("compact initial-failure view is missing %q:\n%s", expected, failed)
|
||||
}
|
||||
}
|
||||
for _, unsupported := range []string{"ACTIVE 0", "JOBS 0 loaded", "WORKERS 0/0", "RECOVERED 0", "no active jobs"} {
|
||||
if strings.Contains(failed, unsupported) {
|
||||
t.Errorf("compact initial-failure view presented %q as observed:\n%s", unsupported, failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewMarksRetainedSnapshotMetricsUnavailableAfterRefreshFailure(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: crackTopLiveTestSnapshot()})
|
||||
_, _ = model.Update(crackTopSnapshotMsg{err: errors.New("temporary outage")})
|
||||
|
||||
meters := ansi.Strip(model.renderOverviewMeters(150, crackTopOverviewMetersHeight))
|
||||
if count := strings.Count(meters, "current data unavailable"); count != 3 {
|
||||
t.Fatalf("failed refresh meters did not expose progress/worker gaps:\n%s", meters)
|
||||
}
|
||||
if strings.Contains(meters, "no active jobs") || strings.Contains(meters, "no observed workers") {
|
||||
t.Fatalf("failed refresh meters converted unknown counts to zero:\n%s", meters)
|
||||
}
|
||||
throughput := ansi.Strip(model.renderOverviewThroughputCard(55, crackTopOverviewSummaryHeight))
|
||||
if !strings.Contains(throughput, "CLUSTER last 2.50 MH/s") || !strings.Contains(throughput, "RECOVERED 7 last snapshot") || !strings.Contains(throughput, "VIEW —") {
|
||||
t.Fatalf("retained throughput was not labeled as stale:\n%s", throughput)
|
||||
}
|
||||
diagnostics := ansi.Strip(model.renderOverviewDiagnosticsCard(55, crackTopOverviewSummaryHeight))
|
||||
if !strings.Contains(diagnostics, "QUEUE last 2 run") {
|
||||
t.Fatalf("retained queue counts were presented as current:\n%s", diagnostics)
|
||||
}
|
||||
jobs := ansi.Strip(model.renderJobsPane(90, 8))
|
||||
workers := ansi.Strip(model.renderWorkersPane(59, 8))
|
||||
if !strings.Contains(jobs, "LAST SNAPSHOT") || !strings.Contains(workers, "LAST SNAPSHOT") {
|
||||
t.Fatalf("retained detail panes were not labeled stale:\njobs:\n%s\nworkers:\n%s", jobs, workers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewRefreshInFlightIsSilent(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: crackTopLiveTestSnapshot()})
|
||||
if command := model.startRefresh(); command == nil || !model.refreshing {
|
||||
t.Fatalf("refresh did not start: command=%v refreshing=%v", command != nil, model.refreshing)
|
||||
}
|
||||
|
||||
top := ansi.Strip(model.renderOverviewTopBar(model.width))
|
||||
diagnostics := ansi.Strip(model.renderOverviewDiagnosticsCard(50, crackTopOverviewSummaryHeight))
|
||||
footer := ansi.Strip(model.renderOverviewFooter(model.width))
|
||||
header := ansi.Strip(model.renderHeader(model.width))
|
||||
for name, rendered := range map[string]string{
|
||||
"top bar": top,
|
||||
"diagnostics": diagnostics,
|
||||
"footer": footer,
|
||||
"header": header,
|
||||
} {
|
||||
lower := strings.ToLower(rendered)
|
||||
if strings.Contains(lower, "refreshing") || strings.Contains(lower, "live snapshot in flight") {
|
||||
t.Errorf("%s exposed an in-flight refresh:\n%s", name, rendered)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(diagnostics, "OK LIVE DATA COMPLETE") {
|
||||
t.Errorf("silent refresh changed snapshot diagnostics:\n%s", diagnostics)
|
||||
}
|
||||
if !strings.Contains(footer, "JOBS focus") {
|
||||
t.Errorf("silent refresh displaced the normal footer:\n%s", footer)
|
||||
}
|
||||
if !strings.Contains(header, "updated ") {
|
||||
t.Errorf("silent refresh hid the current snapshot timestamp:\n%s", header)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewLabelsPartialRateHistoryAsLowerBounds(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
model.snapshot = &crackTopSnapshot{RefreshedAt: time.Now()}
|
||||
model.dashboard = crackTopDashboard{ClusterRate: 2_000, ClusterRateKnown: true, ClusterComplete: false}
|
||||
model.history = []crackTopHistorySample{
|
||||
{At: time.Now().Add(-time.Second), ClusterRate: 1_000, ClusterRateKnown: true, ClusterComplete: true},
|
||||
{At: time.Now(), ClusterRate: 2_000, ClusterRateKnown: true, ClusterComplete: false},
|
||||
}
|
||||
|
||||
average, peak, known, complete := crackTopRateStats(model.history)
|
||||
if !known || complete || average != 1_500 || peak != 2_000 {
|
||||
t.Fatalf("partial rate stats = avg:%d peak:%d known:%v complete:%v", average, peak, known, complete)
|
||||
}
|
||||
plain := ansi.Strip(model.renderOverviewThroughputCard(55, crackTopOverviewSummaryHeight))
|
||||
if !strings.Contains(plain, "CLUSTER ≥ 2.00 kH/s") || !strings.Contains(plain, "AVG ≥ 1.50 kH/s") || !strings.Contains(plain, "PEAK ≥ 2.00 kH/s") {
|
||||
t.Fatalf("partial rate card lost lower-bound semantics:\n%s", plain)
|
||||
}
|
||||
rateChart := ansi.Strip(model.renderOverviewCharts(150, 8))
|
||||
if !strings.Contains(rateChart, "rate ≥ 2.00 kH/s") {
|
||||
t.Fatalf("partial rate chart lost current lower-bound semantics:\n%s", rateChart)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewPausedOnlyWorkloadIsNotIdle(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.snapshot = &crackTopSnapshot{RefreshedAt: time.Now()}
|
||||
model.dashboard = crackTopDashboard{Jobs: []crackTopJobRow{{ID: "paused", Status: clientpb.CrackJobStatus_PAUSED}}}
|
||||
plain := ansi.Strip(model.renderOverviewStateCard(50, crackTopOverviewSummaryHeight))
|
||||
if !strings.Contains(plain, "PAUSED") || strings.Contains(plain, "IDLE") {
|
||||
t.Fatalf("paused-only workload state is misleading:\n%s", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopPlotPreservesUnknownGapAndBounds(t *testing.T) {
|
||||
const (
|
||||
width = 4
|
||||
height = 4
|
||||
)
|
||||
plot := crackTopRenderPlot([]crackTopChartPoint{
|
||||
{value: 100, known: true},
|
||||
{value: 100, known: true},
|
||||
{known: false},
|
||||
{value: 100, known: true},
|
||||
}, width, height, 100)
|
||||
|
||||
lines := strings.Split(plot, "\n")
|
||||
if len(lines) != height {
|
||||
t.Fatalf("plot height = %d, want %d:\n%s", len(lines), height, plot)
|
||||
}
|
||||
for index, line := range lines {
|
||||
if got := ansi.StringWidth(line); got > width {
|
||||
t.Fatalf("plot line %d width = %d, want <= %d: %q", index+1, got, width, line)
|
||||
}
|
||||
}
|
||||
top := []rune(lines[0])
|
||||
if len(top) != width {
|
||||
t.Fatalf("plot top row width = %d runes, want %d: %q", len(top), width, lines[0])
|
||||
}
|
||||
if top[0] != '╶' || top[1] != '╴' || top[2] != ' ' || top[3] != '●' {
|
||||
t.Fatalf("plot did not preserve the unknown sample as a visible gap: %q", lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopPlotConnectsVerticalTransitions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
points []crackTopChartPoint
|
||||
height int
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "rising",
|
||||
points: []crackTopChartPoint{
|
||||
{value: 0, known: true},
|
||||
{value: 50, known: true},
|
||||
{value: 100, known: true},
|
||||
},
|
||||
height: 3,
|
||||
want: " ╷\n·┌┘\n╶┘·",
|
||||
},
|
||||
{
|
||||
name: "falling",
|
||||
points: []crackTopChartPoint{
|
||||
{value: 100, known: true},
|
||||
{value: 50, known: true},
|
||||
{value: 0, known: true},
|
||||
},
|
||||
height: 3,
|
||||
want: "╶┐ \n·└┐\n··╵",
|
||||
},
|
||||
{
|
||||
name: "flat",
|
||||
points: []crackTopChartPoint{
|
||||
{value: 50, known: true},
|
||||
{value: 50, known: true},
|
||||
{value: 50, known: true},
|
||||
},
|
||||
height: 3,
|
||||
want: " \n╶─╴\n···",
|
||||
},
|
||||
{
|
||||
name: "full range rise",
|
||||
points: []crackTopChartPoint{
|
||||
{value: 0, known: true},
|
||||
{value: 100, known: true},
|
||||
},
|
||||
height: 4,
|
||||
want: " ╷\n │\n·│\n╶┘",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
got := crackTopRenderPlot(testCase.points, len(testCase.points), testCase.height, 100)
|
||||
if got != testCase.want {
|
||||
t.Fatalf("plot =\n%q\nwant\n%q", got, testCase.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopChartStatsDoesNotPresentStaleLatestAsCurrent(t *testing.T) {
|
||||
latest, average, peak, latestKnown, historyKnown, latestLowerBound, historyLowerBound := crackTopChartStats([]crackTopChartPoint{
|
||||
{value: 25, known: true},
|
||||
{value: 75, known: true},
|
||||
{known: false},
|
||||
})
|
||||
if latestKnown || !historyKnown {
|
||||
t.Fatalf("latest/history known = %v/%v, want false/true", latestKnown, historyKnown)
|
||||
}
|
||||
if latestLowerBound || historyLowerBound {
|
||||
t.Fatalf("unexpected lower-bound flags = latest:%v history:%v", latestLowerBound, historyLowerBound)
|
||||
}
|
||||
if latest != 0 || average != 50 || peak != 75 {
|
||||
t.Fatalf("latest/average/peak = %v/%v/%v, want 0/50/75", latest, average, peak)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopChartStatsUseOnlyVisiblePlotWindow(t *testing.T) {
|
||||
points := []crackTopChartPoint{
|
||||
{value: 1_000, known: true},
|
||||
{value: 10, known: true},
|
||||
{value: 20, known: true},
|
||||
{value: 30, known: true},
|
||||
}
|
||||
visible := crackTopVisibleChartPoints(points, 3)
|
||||
latest, average, peak, latestKnown, historyKnown, _, _ := crackTopChartStats(visible)
|
||||
if len(visible) != 3 || !latestKnown || !historyKnown || latest != 30 || average != 20 || peak != 30 {
|
||||
t.Fatalf("visible chart stats = len:%d latest:%v avg:%v peak:%v latest-known:%v history-known:%v", len(visible), latest, average, peak, latestKnown, historyKnown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopRecoveredDeltaUsesOnlyObservedCountEndpoints(t *testing.T) {
|
||||
history := []crackTopHistorySample{
|
||||
{Recovered: 0, CountsKnown: false},
|
||||
{Recovered: 10, CountsKnown: true},
|
||||
{Recovered: 0, CountsKnown: false},
|
||||
{Recovered: 13, CountsKnown: true},
|
||||
}
|
||||
if delta, known := crackTopRecoveredDelta(history); !known || delta != 3 {
|
||||
t.Fatalf("recovered delta across boundary gaps = %d known:%v, want 3 known", delta, known)
|
||||
}
|
||||
if delta, known := crackTopRecoveredDelta([]crackTopHistorySample{{Recovered: 13}, {Recovered: 14}}); known || delta != 0 {
|
||||
t.Fatalf("unobserved recovered endpoints = %d known:%v, want unknown", delta, known)
|
||||
}
|
||||
if delta, known := crackTopRecoveredDelta([]crackTopHistorySample{{Recovered: 13, CountsKnown: true}}); known || delta != 0 {
|
||||
t.Fatalf("single recovered endpoint = %d known:%v, want unknown", delta, known)
|
||||
}
|
||||
if delta, known := crackTopRecoveredDelta([]crackTopHistorySample{{Recovered: 13, CountsKnown: true}, {Recovered: 12, CountsKnown: true}}); known || delta != 0 {
|
||||
t.Fatalf("decreasing recovered counter = %d known:%v, want unknown", delta, known)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopChartSpanDisclosesSampleIndexedHistory(t *testing.T) {
|
||||
points := []crackTopChartPoint{
|
||||
{at: time.Unix(1_700_000_000, 0)},
|
||||
{at: time.Unix(1_700_000_001, 0)},
|
||||
{at: time.Unix(1_700_000_061, 0)},
|
||||
}
|
||||
if got := crackTopChartSpan(points); got != "1m1s/3 samples" {
|
||||
t.Fatalf("chart span = %q, want elapsed time and sample count", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopPlotDistinguishesUnknownFromObservedZero(t *testing.T) {
|
||||
plot := crackTopRenderPlot([]crackTopChartPoint{
|
||||
{value: 0, known: true},
|
||||
{known: false},
|
||||
{value: 0, known: true},
|
||||
}, 3, 3, 100)
|
||||
bottom := strings.Split(plot, "\n")[2]
|
||||
if bottom != "●·●" {
|
||||
t.Fatalf("zero/unknown/zero baseline = %q, want observed points around a grid gap", bottom)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopAuthoritativeJobActionIsJournaled(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.runningJobAction = crackTopPendingJobAction{action: crackTopJobActionPause, jobID: crackTopTestJobID}
|
||||
_, _ = model.Update(crackTopJobActionCompletedMsg{
|
||||
action: crackTopJobActionPause,
|
||||
jobID: crackTopTestJobID,
|
||||
status: clientpb.CrackJobStatus_PAUSED,
|
||||
updatedAt: 1_700_000_100,
|
||||
hasStatus: true,
|
||||
})
|
||||
if len(model.journal) != 1 || model.journal[0].label != "JOB" || model.journal[0].message != "aaaaaaaa PAUSED" {
|
||||
t.Fatalf("authoritative action journal = %#v", model.journal)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test keeps the complete action-to-authoritative-snapshot freshness transition together.
|
||||
func TestCrackTopActionMarksTaskTelemetryStaleUntilAuthoritativeSnapshot(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.width = 150
|
||||
model.height = 40
|
||||
initial := crackTopLiveTestSnapshot()
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: initial})
|
||||
actionUpdatedAt := initial.RefreshedAt.Add(time.Second).Unix()
|
||||
|
||||
model.runningJobAction = crackTopPendingJobAction{action: crackTopJobActionPause, jobID: crackTopTestJobID}
|
||||
_, _ = model.Update(crackTopJobActionCompletedMsg{
|
||||
action: crackTopJobActionPause,
|
||||
jobID: crackTopTestJobID,
|
||||
status: clientpb.CrackJobStatus_PAUSED,
|
||||
updatedAt: actionUpdatedAt,
|
||||
hasStatus: true,
|
||||
})
|
||||
|
||||
if !model.authoritativeSnapshotPending() {
|
||||
t.Fatal("successful lifecycle action did not mark task telemetry as awaiting an authoritative snapshot")
|
||||
}
|
||||
latest := model.history[len(model.history)-1]
|
||||
if latest.ClusterRateKnown || latest.ProgressKnown || latest.UtilizationKnown || latest.CountsKnown {
|
||||
t.Fatalf("post-action history sample presented stale task telemetry as current: %#v", latest)
|
||||
}
|
||||
plain := ansi.Strip(strings.Join([]string{
|
||||
model.renderOverviewStateCard(50, crackTopOverviewSummaryHeight),
|
||||
model.renderOverviewThroughputCard(50, crackTopOverviewSummaryHeight),
|
||||
model.renderOverviewDiagnosticsCard(50, crackTopOverviewSummaryHeight),
|
||||
model.renderOverviewMeters(150, crackTopOverviewMetersHeight),
|
||||
model.renderJobsPane(90, 8),
|
||||
model.renderWorkersPane(59, 8),
|
||||
}, "\n"))
|
||||
for _, expected := range []string{"SYNCING", "TASK DATA LAST", "RECOVERED 7 last snapshot", "task data pending", "QUEUE last 2 run", "last snapshot • syncing", "TASK DATA LAST/SYNCING", "LAST SNAPSHOT/SYNCING"} {
|
||||
if !strings.Contains(plain, expected) {
|
||||
t.Errorf("post-action overview is missing %q:\n%s", expected, plain)
|
||||
}
|
||||
}
|
||||
if strings.Contains(strings.ToLower(plain), "refreshing") {
|
||||
t.Errorf("post-action overview exposed refresh progress:\n%s", plain)
|
||||
}
|
||||
|
||||
stale := crackTopLiveTestSnapshot()
|
||||
stale.RefreshedAt = stale.RefreshedAt.Add(500 * time.Millisecond)
|
||||
stale.Jobs[0].UpdatedAt = actionUpdatedAt
|
||||
stale.Stations = stale.Stations[:1]
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: stale})
|
||||
if !model.authoritativeSnapshotPending() {
|
||||
t.Fatal("equal-version pre-action snapshot prematurely cleared lifecycle synchronization state")
|
||||
}
|
||||
staleSample := model.history[len(model.history)-1]
|
||||
if staleSample.ClusterRateKnown || staleSample.ProgressKnown || staleSample.UtilizationKnown || staleSample.CountsKnown {
|
||||
t.Fatalf("client-overridden pre-action snapshot was recorded as authoritative telemetry: %#v", staleSample)
|
||||
}
|
||||
foundUnrelatedTransition := false
|
||||
for _, entry := range model.journal {
|
||||
if entry.label == "WORKER" && entry.message == "Beta Rig disconnected" {
|
||||
foundUnrelatedTransition = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundUnrelatedTransition {
|
||||
t.Fatalf("pending-action snapshot discarded an unrelated worker transition: %#v", model.journal)
|
||||
}
|
||||
|
||||
authoritative := crackTopLiveTestSnapshot()
|
||||
authoritative.RefreshedAt = authoritative.RefreshedAt.Add(2 * time.Second)
|
||||
authoritative.Jobs[0].Status = clientpb.CrackJobStatus_PAUSED
|
||||
authoritative.Jobs[0].UpdatedAt = actionUpdatedAt
|
||||
for _, task := range authoritative.Jobs[0].Tasks {
|
||||
task.State = clientpb.CrackTaskState_CRACK_TASK_QUEUED
|
||||
}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: authoritative})
|
||||
if model.authoritativeSnapshotPending() {
|
||||
t.Fatal("matching authoritative snapshot did not clear lifecycle synchronization state")
|
||||
}
|
||||
diagnostics := ansi.Strip(model.renderOverviewDiagnosticsCard(50, crackTopOverviewSummaryHeight))
|
||||
if strings.Contains(diagnostics, "QUEUE last") || strings.Contains(diagnostics, "SYNC action") {
|
||||
t.Fatalf("authoritative snapshot remained labeled as stale:\n%s", diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopOverviewSelectionMarkerFollowsFocusedPane(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
model.dashboard = buildCrackTopDashboard(crackTopLiveTestSnapshot())
|
||||
model.normalizeJobSelection()
|
||||
model.normalizeWorkerSelection()
|
||||
|
||||
jobs := ansi.Strip(model.renderJobsPane(75, 8))
|
||||
workers := ansi.Strip(model.renderWorkersPane(55, 8))
|
||||
if !strings.Contains(jobs, "› aaaaaaaa") || strings.Contains(workers, "› ") {
|
||||
t.Fatalf("job focus markers are ambiguous:\njobs:\n%s\nworkers:\n%s", jobs, workers)
|
||||
}
|
||||
|
||||
_, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyTab})
|
||||
jobs = ansi.Strip(model.renderJobsPane(75, 8))
|
||||
workers = ansi.Strip(model.renderWorkersPane(55, 8))
|
||||
if strings.Contains(jobs, "› ") || !strings.Contains(workers, "› ") {
|
||||
t.Fatalf("worker focus markers are ambiguous:\njobs:\n%s\nworkers:\n%s", jobs, workers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopJournalRecordsRecoveredJobAndWorkerTransitions(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
first := crackTopLiveTestSnapshot()
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: first})
|
||||
|
||||
second := crackTopLiveTestSnapshot()
|
||||
second.RefreshedAt = first.RefreshedAt.Add(time.Second)
|
||||
second.Jobs[0].Status = clientpb.CrackJobStatus_COMPLETED
|
||||
second.Jobs[0].ResultCount = 10
|
||||
second.Stations = second.Stations[:1]
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: second})
|
||||
|
||||
wants := map[string]string{
|
||||
"JOB": "aaaaaaaa COMPLETED",
|
||||
"WORKER": "Beta Rig disconnected",
|
||||
"RECOVERED": "+3 across loaded jobs",
|
||||
}
|
||||
for label, message := range wants {
|
||||
found := false
|
||||
for _, entry := range model.journal {
|
||||
if entry.label == label && entry.message == message {
|
||||
found = true
|
||||
if !entry.at.Equal(second.RefreshedAt) {
|
||||
t.Errorf("%s journal time = %s, want %s", label, entry.at, second.RefreshedAt)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("journal missing %s transition %q: %#v", label, message, model.journal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackTopJournalRecordsActiveJobRemovedByAnotherOperator(t *testing.T) {
|
||||
model := newCrackTopModel(t.Context(), nil, nil, time.Second)
|
||||
first := crackTopLiveTestSnapshot()
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: first})
|
||||
|
||||
second := &crackTopSnapshot{RefreshedAt: first.RefreshedAt.Add(time.Second)}
|
||||
_, _ = model.Update(crackTopSnapshotMsg{snapshot: second})
|
||||
|
||||
for _, entry := range model.journal {
|
||||
if entry.label == "JOB" && entry.message == "aaaaaaaa removed from live set" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("journal did not record externally removed active job: %#v", model.journal)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
type crackTopQueueCounts struct {
|
||||
queued int
|
||||
leased int
|
||||
running int
|
||||
}
|
||||
|
||||
func TestBuildCrackTopDashboardCountsActiveJobTaskQueueStates(t *testing.T) {
|
||||
crackTasks := func(states ...clientpb.CrackTaskState) []*clientpb.CrackTask {
|
||||
tasks := make([]*clientpb.CrackTask, 0, len(states))
|
||||
for _, state := range states {
|
||||
tasks = append(tasks, &clientpb.CrackTask{
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobs []*clientpb.CrackJob
|
||||
want crackTopQueueCounts
|
||||
}{
|
||||
{
|
||||
name: "in-progress job",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "in-progress",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: crackTasks(
|
||||
clientpb.CrackTaskState_CRACK_TASK_QUEUED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_LEASED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
),
|
||||
}},
|
||||
want: crackTopQueueCounts{queued: 1, leased: 1, running: 1},
|
||||
},
|
||||
{
|
||||
name: "paused job",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "paused",
|
||||
Status: clientpb.CrackJobStatus_PAUSED,
|
||||
Tasks: crackTasks(
|
||||
clientpb.CrackTaskState_CRACK_TASK_QUEUED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_LEASED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
),
|
||||
}},
|
||||
want: crackTopQueueCounts{queued: 1, leased: 1, running: 1},
|
||||
},
|
||||
{
|
||||
name: "legacy unspecified crack task with shard telemetry",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "legacy-crack",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: []*clientpb.CrackTask{{
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
ShardLimit: 100,
|
||||
}},
|
||||
}},
|
||||
want: crackTopQueueCounts{running: 1},
|
||||
},
|
||||
{
|
||||
name: "keyspace and auxiliary scheduler stages",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "queries",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: []*clientpb.CrackTask{
|
||||
{Kind: clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED, State: clientpb.CrackTaskState_CRACK_TASK_QUEUED},
|
||||
{Kind: clientpb.CrackTaskKind_CRACK_TASK_KEYSPACE, State: clientpb.CrackTaskState_CRACK_TASK_QUEUED},
|
||||
{Kind: clientpb.CrackTaskKind_CRACK_TASK_BENCHMARK, State: clientpb.CrackTaskState_CRACK_TASK_LEASED},
|
||||
{Kind: clientpb.CrackTaskKind_CRACK_TASK_QUERY, State: clientpb.CrackTaskState_CRACK_TASK_RUNNING},
|
||||
},
|
||||
}},
|
||||
want: crackTopQueueCounts{queued: 2, leased: 1, running: 1},
|
||||
},
|
||||
{
|
||||
name: "terminal jobs",
|
||||
jobs: []*clientpb.CrackJob{
|
||||
{ID: "completed", Status: clientpb.CrackJobStatus_COMPLETED, Tasks: crackTasks(clientpb.CrackTaskState_CRACK_TASK_QUEUED)},
|
||||
{ID: "failed", Status: clientpb.CrackJobStatus_FAILED, Tasks: crackTasks(clientpb.CrackTaskState_CRACK_TASK_LEASED)},
|
||||
{ID: "cancelled", Status: clientpb.CrackJobStatus_CANCELLED, Tasks: crackTasks(clientpb.CrackTaskState_CRACK_TASK_RUNNING)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "terminal task states",
|
||||
jobs: []*clientpb.CrackJob{{
|
||||
ID: "active-with-terminal-tasks",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: append(
|
||||
crackTasks(
|
||||
clientpb.CrackTaskState_CRACK_TASK_COMPLETED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_FAILED,
|
||||
clientpb.CrackTaskState_CRACK_TASK_CANCELLED,
|
||||
),
|
||||
nil,
|
||||
),
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
dashboard := buildCrackTopDashboard(&crackTopSnapshot{Jobs: testCase.jobs})
|
||||
got := crackTopQueueCounts{
|
||||
queued: dashboard.QueuedTasks,
|
||||
leased: dashboard.LeasedTasks,
|
||||
running: dashboard.RunningTasks,
|
||||
}
|
||||
if got != testCase.want {
|
||||
t.Fatalf("task queue counts = %+v, want %+v", got, testCase.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package top
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/crack/internal/hashcatdisplay"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
// These views intentionally contain only the status fields used by Crack Top.
|
||||
// The production loader fills them from the lean CrackTop RPC; JSON decoding is
|
||||
// retained for compatibility with locally constructed snapshots and fixtures.
|
||||
type crackStatusView struct {
|
||||
ProgressCurrent string
|
||||
ProgressTotal string
|
||||
Devices []crackDeviceView
|
||||
}
|
||||
|
||||
type crackDeviceView struct {
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
Speed string
|
||||
Temperature string
|
||||
Utilization string
|
||||
}
|
||||
|
||||
func hashcatHashTypeName(mode int32) (string, bool) {
|
||||
return hashcatdisplay.TypeName(mode)
|
||||
}
|
||||
|
||||
func humanizeHashRate(rate uint64) string {
|
||||
return hashcatdisplay.HumanizeRate(rate)
|
||||
}
|
||||
|
||||
func crackJobSortTime(job *clientpb.CrackJob) time.Time {
|
||||
if job == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if updatedAt := job.GetUpdatedAt(); updatedAt > 0 {
|
||||
return time.Unix(updatedAt, 0)
|
||||
}
|
||||
createdAt := strings.TrimSpace(job.GetCreatedAt())
|
||||
if unix, err := strconv.ParseInt(createdAt, 10, 64); err == nil && unix > 0 {
|
||||
return time.Unix(unix, 0)
|
||||
}
|
||||
parsed, _ := time.Parse(time.RFC3339, createdAt)
|
||||
return parsed
|
||||
}
|
||||
|
||||
func crackJobTasks(job *clientpb.CrackJob) []*clientpb.CrackTask {
|
||||
if job == nil {
|
||||
return nil
|
||||
}
|
||||
return job.GetTasks()
|
||||
}
|
||||
|
||||
func crackTaskStatus(task *clientpb.CrackTask) (*crackStatusView, error) {
|
||||
if task == nil || len(bytes.TrimSpace(task.GetLatestStatusJSON())) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return parseCrackStatusJSON(task.GetLatestStatusJSON())
|
||||
}
|
||||
|
||||
func parseCrackStatusJSON(raw []byte) (*crackStatusView, error) {
|
||||
root, err := decodeLastCrackTopJSONObject(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := &crackStatusView{}
|
||||
status.ProgressCurrent, status.ProgressTotal = crackTopJSONPair(root["progress"])
|
||||
if devices, ok := root["devices"].([]any); ok {
|
||||
for _, item := range devices {
|
||||
values, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
status.Devices = append(status.Devices, crackDeviceView{
|
||||
ID: crackTopJSONText(firstCrackTopJSONValue(values, "device_id", "id")),
|
||||
Name: crackTopSafeCell(crackTopJSONText(firstCrackTopJSONValue(values, "device_name", "name"))),
|
||||
Type: crackTopSafeCell(crackTopJSONText(firstCrackTopJSONValue(values, "device_type", "type"))),
|
||||
Speed: crackTopJSONText(firstCrackTopJSONValue(values, "speed")),
|
||||
Temperature: crackTopJSONText(firstCrackTopJSONValue(values, "temp", "temperature")),
|
||||
Utilization: crackTopJSONText(firstCrackTopJSONValue(values, "util", "utilization")),
|
||||
})
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func decodeLastCrackTopJSONObject(raw []byte) (map[string]any, error) {
|
||||
decode := func(candidate []byte) (map[string]any, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(candidate))
|
||||
decoder.UseNumber()
|
||||
var last map[string]any
|
||||
for {
|
||||
value := map[string]any{}
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
break
|
||||
}
|
||||
last = value
|
||||
}
|
||||
if last == nil {
|
||||
return nil, errors.New("no JSON object")
|
||||
}
|
||||
return last, nil
|
||||
}
|
||||
|
||||
trimmed := bytes.TrimSpace(raw)
|
||||
if value, err := decode(trimmed); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
lines := bytes.Split(trimmed, []byte{'\n'})
|
||||
for index := len(lines) - 1; index >= 0; index-- {
|
||||
candidate := bytes.TrimSpace(lines[index])
|
||||
if len(candidate) == 0 {
|
||||
continue
|
||||
}
|
||||
if value, err := decode(candidate); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("hashcat status did not contain a JSON object")
|
||||
}
|
||||
|
||||
func firstCrackTopJSONValue(values map[string]any, names ...string) any {
|
||||
for _, name := range names {
|
||||
if value, ok := values[name]; ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func crackTopJSONPair(value any) (string, string) {
|
||||
items, ok := value.([]any)
|
||||
if !ok || len(items) < 2 {
|
||||
return "", ""
|
||||
}
|
||||
return crackTopJSONText(items[0]), crackTopJSONText(items[1])
|
||||
}
|
||||
|
||||
func crackTopJSONText(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return crackTopSafeCell(typed)
|
||||
case json.Number:
|
||||
return typed.String()
|
||||
case float64:
|
||||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||||
case bool:
|
||||
return strconv.FormatBool(typed)
|
||||
default:
|
||||
return crackTopSafeCell(fmt.Sprint(typed))
|
||||
}
|
||||
}
|
||||
|
||||
func valueOrDash(value string) string {
|
||||
value = crackTopSafeCell(value)
|
||||
if value == "" {
|
||||
return "-"
|
||||
}
|
||||
return value
|
||||
}
|
||||
+28
-7
@@ -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},
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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}
|
||||
|
||||
Generated
+1051
-1036
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 @@
|
||||
{"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\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\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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
module github.com/bishopfox/sliver
|
||||
|
||||
go 1.27.1
|
||||
go 1.27.3
|
||||
|
||||
require (
|
||||
filippo.io/age v1.1.1
|
||||
|
||||
@@ -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)
|
||||
@@ -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
Reference in new issue
Block a user