mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
70
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49498210f8 | ||
|
|
14e9121302 | ||
|
|
8bd0257dd9 | ||
|
|
93fdfb3add | ||
|
|
e02b03cc70 | ||
|
|
6831fe50a6 | ||
|
|
405108866d | ||
|
|
4d551b71e7 | ||
|
|
a7edd9838d | ||
|
|
9ef54ca6b1 | ||
|
|
4622c50288 | ||
|
|
b1ee0355af | ||
|
|
01d9b213f2 | ||
|
|
f56886bce5 | ||
|
|
88176dfd00 | ||
|
|
45dc3ea09b | ||
|
|
682b3c2409 | ||
|
|
3a93f312b4 | ||
|
|
2ca95f7a7c | ||
|
|
d2319432ab | ||
|
|
253f1a8806 | ||
|
|
2c025d5970 | ||
|
|
f0034683f9 | ||
|
|
8e8398b654 | ||
|
|
aa02293f05 | ||
|
|
f967a32308 | ||
|
|
99ec812455 | ||
|
|
eeef542eeb | ||
|
|
6d7a10b392 | ||
|
|
ca40c0a1e7 | ||
|
|
4a5ad6ea92 | ||
|
|
266dabf6c3 | ||
|
|
a4c8968539 | ||
|
|
613c030ea3 | ||
|
|
9f6bfb6714 | ||
|
|
a764814a8e | ||
|
|
0c9ce138a0 | ||
|
|
6c1dcf6a06 | ||
|
|
fffe93819d | ||
|
|
635ee421e3 | ||
|
|
3ce6d69474 | ||
|
|
cd523c7f77 | ||
|
|
1f1b759914 | ||
|
|
b78915fcbd | ||
|
|
fa0e13a5cc | ||
|
|
924bcddfd2 | ||
|
|
affbf61452 | ||
|
|
7c6b2f70e4 | ||
|
|
c46f2938de | ||
|
|
b355fe70d6 | ||
|
|
10b1079286 | ||
|
|
9c042e6977 | ||
|
|
62583cac8f | ||
|
|
95764945b8 | ||
|
|
26abda1c15 | ||
|
|
01dd3cac72 | ||
|
|
f8faa1f767 | ||
|
|
a9e48e84cd | ||
|
|
9d6704d925 | ||
|
|
ce5157e2e9 | ||
|
|
7d19051df8 | ||
|
|
a0d3bd579f | ||
|
|
82c309632a | ||
|
|
5c0b8ec460 | ||
|
|
7791a86eb4 | ||
|
|
7ac4db3fa0 | ||
|
|
512bc822ef | ||
|
|
f2bb9c3733 | ||
|
|
0bd4f100fd | ||
|
|
3230322fd6 |
No files matched your search
@@ -41,10 +41,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Go 1.25
|
||||
- name: Go 1.25.8
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.25.8"
|
||||
|
||||
- name: Install Minisign
|
||||
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
|
||||
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup go environment
|
||||
- name: Go 1.25
|
||||
- name: Go 1.25.8
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.25.8"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -6,11 +6,13 @@ on:
|
||||
paths:
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
- ".github/workflows/unit-tests.yml"
|
||||
- "Makefile"
|
||||
- "**/*.go"
|
||||
- "**/*.c"
|
||||
- "**/*.h"
|
||||
- "**/*.sh"
|
||||
- "**/*.ps1"
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
@@ -18,15 +20,15 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
linux-windows-build-test:
|
||||
name: Linux & Windows Test
|
||||
linux-build-test:
|
||||
name: Linux Build & Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
- name: Go 1.25.8
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.25.8"
|
||||
|
||||
- name: OS Packages
|
||||
run: |
|
||||
@@ -64,16 +66,59 @@ jobs:
|
||||
- name: Test Linux
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
windows-build-test:
|
||||
name: Windows Build & Test
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.25.8
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.8"
|
||||
|
||||
- name: Install make
|
||||
run: choco install make -y --no-progress
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
- name: Make Windows (amd64)
|
||||
run: make windows-amd64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: pwsh
|
||||
run: |
|
||||
$baseSha = "${{ github.event.pull_request.base.sha }}"
|
||||
$headSha = "${{ github.sha }}"
|
||||
$changedFiles = git diff --name-only $baseSha $headSha
|
||||
|
||||
if ($changedFiles | Select-String -Pattern '^(implant/|server/gogo/|server/generate/)') {
|
||||
"args=" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
} else {
|
||||
"args=--skip-generate" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
- name: Test Windows
|
||||
shell: pwsh
|
||||
run: |
|
||||
make clean
|
||||
make
|
||||
./go-tests.ps1 ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
|
||||
macos-build-test:
|
||||
name: MacOS Test
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
- name: Go 1.25.8
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.25.8"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -108,10 +153,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
- name: Go 1.25.8
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.25.8"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/crush
|
||||
|
||||
/.vscode
|
||||
/.vscode/*
|
||||
/.devcontainer
|
||||
/.idea
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
|
||||
- `make client` builds a CGO-free client; `make debug` keeps symbols.
|
||||
- `make pb` regenerates protobuf stubs from `protobuf/`.
|
||||
- `./go-tests.sh` runs the curated Go test suite with build tags.
|
||||
- `./go-tests.sh` runs the Go test suite, this is the only way you should run unit tests.
|
||||
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
@@ -1,184 +1,333 @@
|
||||
#
|
||||
# Makefile for Sliver
|
||||
#
|
||||
|
||||
GO ?= go
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ENV =
|
||||
TAGS ?= -tags go_sqlite
|
||||
CGO_ENABLED = 0
|
||||
|
||||
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
|
||||
CGO_ENABLED = 1
|
||||
endif
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 25
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
|
||||
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_P := $(shell uname -p)
|
||||
|
||||
# Programs required for generating protobuf/grpc files
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
ifeq ($(MAKECMDGOALS), pb)
|
||||
K := $(foreach exec,$(PB_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
||||
endif
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
#
|
||||
# Targets
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
#
|
||||
# Makefile for Sliver
|
||||
#
|
||||
|
||||
ifneq ($(OS),Windows_NT)
|
||||
|
||||
GO ?= go
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ENV =
|
||||
TAGS ?= -tags go_sqlite
|
||||
CGO_ENABLED = 0
|
||||
|
||||
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
|
||||
CGO_ENABLED = 1
|
||||
endif
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 25
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
|
||||
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_P := $(shell uname -p)
|
||||
|
||||
# Programs required for generating protobuf/grpc files
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
ifeq ($(MAKECMDGOALS), pb)
|
||||
K := $(foreach exec,$(PB_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
||||
endif
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
#
|
||||
# Targets
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
|
||||
|
||||
#
|
||||
# >>> WINDOWS <<<
|
||||
#
|
||||
else
|
||||
|
||||
GO ?= go
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ENV =
|
||||
TAGS ?= -tags go_sqlite
|
||||
CGO_ENABLED = 0
|
||||
|
||||
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
|
||||
CGO_ENABLED = 1
|
||||
endif
|
||||
|
||||
SHELL := cmd.exe
|
||||
.SHELLFLAGS := /C
|
||||
|
||||
GO_VERSION := $(patsubst go%,%,$(strip $(shell $(GO) env GOVERSION)))
|
||||
GO_MAJOR_VERSION := $(word 1,$(subst ., ,$(GO_VERSION)))
|
||||
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
|
||||
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 25
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
|
||||
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
|
||||
LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
COMMA := ,
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
define windows_exec
|
||||
$(strip $(foreach envvar,$(1),set "$(envvar)" && ))$(2)
|
||||
endef
|
||||
|
||||
define windows_go_build
|
||||
$(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build $(4) $(TAGS)$(COMMA)$(5) $(6) -o $(7) ./$(5))
|
||||
endef
|
||||
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client)
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX).exe)
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-386$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,windows,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-386$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,freebsd,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,freebsd,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-amd64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: pb
|
||||
pb: validate-pb-compilers
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor,server,$(LDFLAGS_DEBUG),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor,client,$(LDFLAGS_DEBUG),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: validate-pb-compilers
|
||||
validate-pb-compilers:
|
||||
@for %%P in ($(PB_COMPILERS)) do @where.exe %%P >NUL || (echo Missing protobuf util %%P in PATH & exit /b 1)
|
||||
|
||||
validate-go-version:
|
||||
@if $(GO_MAJOR_VERSION) GTR $(MIN_SUPPORTED_GO_MAJOR_VERSION) (exit /b 0) else if $(GO_MAJOR_VERSION) LSS $(MIN_SUPPORTED_GO_MAJOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1) else if $(GO_MINOR_VERSION) LSS $(MIN_SUPPORTED_GO_MINOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1)
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
-rmdir /S /Q server\assets\fs\darwin\amd64
|
||||
-rmdir /S /Q server\assets\fs\darwin\arm64
|
||||
-rmdir /S /Q server\assets\fs\windows\amd64
|
||||
-rmdir /S /Q server\assets\fs\linux\amd64
|
||||
-del /Q /F server\assets\fs\*.zip 2>NUL
|
||||
-del /Q /F .downloaded_assets 2>NUL
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
|
||||
|
||||
.downloaded_assets:
|
||||
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
|
||||
@type NUL > .downloaded_assets
|
||||
|
||||
endif
|
||||
+17
-7
@@ -34,15 +34,25 @@ const (
|
||||
ConfigDirName = "configs"
|
||||
)
|
||||
|
||||
// ClientWGConfig - Optional WireGuard wrapper configuration for multiplayer.
|
||||
type ClientWGConfig struct {
|
||||
ServerPubKey string `json:"server_pub_key"`
|
||||
ClientPrivateKey string `json:"client_private_key"`
|
||||
ClientPubKey string `json:"client_pub_key"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
ServerIP string `json:"server_ip"`
|
||||
}
|
||||
|
||||
// ClientConfig - Client JSON config
|
||||
type ClientConfig struct {
|
||||
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
|
||||
LHost string `json:"lhost"`
|
||||
LPort int `json:"lport"`
|
||||
Token string `json:"token"`
|
||||
CACertificate string `json:"ca_certificate"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
Certificate string `json:"certificate"`
|
||||
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
|
||||
LHost string `json:"lhost"`
|
||||
LPort int `json:"lport"`
|
||||
Token string `json:"token"`
|
||||
CACertificate string `json:"ca_certificate"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
Certificate string `json:"certificate"`
|
||||
WG *ClientWGConfig `json:"wg,omitempty"`
|
||||
}
|
||||
|
||||
// GetConfigDir - Returns the path to the config dir
|
||||
|
||||
@@ -52,6 +52,8 @@ func init() {
|
||||
|
||||
rootCmd.TraverseChildren = true
|
||||
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
||||
rootCmd.PersistentFlags().Bool(disableWGFlag, false, "connect to multiplayer directly even if the operator config includes a WireGuard wrapper")
|
||||
rootCmd.PersistentFlags().Bool(requireWGFlag, false, "require the operator config's WireGuard wrapper for multiplayer connections")
|
||||
|
||||
// Create the console client, without any RPC or commands bound to it yet.
|
||||
// This created before anything so that multiple commands can make use of
|
||||
|
||||
@@ -44,6 +44,9 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
|
||||
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
|
||||
pre = func(cmd *cobra.Command, _ []string) error {
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
|
||||
+5
-1
@@ -43,6 +43,10 @@ func mcpCmd(con *console.SliverClient) *cobra.Command {
|
||||
}
|
||||
|
||||
func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath, _ := cmd.Flags().GetString("config")
|
||||
config, err := loadMCPClientConfig(configPath)
|
||||
if err != nil {
|
||||
@@ -55,7 +59,7 @@ func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||
fmt.Printf("Connection to server failed %s\n", err)
|
||||
return nil
|
||||
}
|
||||
defer ln.Close()
|
||||
defer transport.CloseGRPCConnection(ln)
|
||||
|
||||
con.Rpc = rpc
|
||||
go handleConnectionLost(ln)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
requireWGFlag = "require-wg"
|
||||
disableWGFlag = "disable-wg"
|
||||
)
|
||||
|
||||
func applyMultiplayerConnectMode(cmd *cobra.Command) error {
|
||||
if cmd == nil {
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
return nil
|
||||
}
|
||||
|
||||
requireWG, err := cmd.Flags().GetBool(requireWGFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
disableWG, err := cmd.Flags().GetBool(disableWGFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requireWG && disableWG {
|
||||
return fmt.Errorf("--%s and --%s cannot be used together", requireWGFlag, disableWGFlag)
|
||||
}
|
||||
|
||||
mode := transport.MultiplayerConnectAuto
|
||||
switch {
|
||||
case requireWG:
|
||||
mode = transport.MultiplayerConnectRequireWG
|
||||
case disableWG:
|
||||
mode = transport.MultiplayerConnectDisableWG
|
||||
}
|
||||
transport.SetMultiplayerConnectMode(mode)
|
||||
return nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
|
||||
|
||||
## Sub-packages
|
||||
|
||||
- `ai/` – Implements the 'ai' Bubble Tea layout preview command for the Sliver client console.
|
||||
- `alias/` – Implements the 'alias' command group for the Sliver client console.
|
||||
- `armory/` – Implements the 'armory' command group for the Sliver client console.
|
||||
- `backdoor/` – Implements the 'backdoor' command group for the Sliver client console.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# client/command/ai
|
||||
|
||||
Implements the `ai` command for the Sliver client console.
|
||||
|
||||
The TUI loads server-backed AI conversation threads over gRPC, submits prompts to the server, and refreshes when `AIConversationEvent` updates arrive with assistant replies or failure messages.
|
||||
@@ -0,0 +1,145 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// AICmd launches the AI conversation TUI.
|
||||
func AICmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
if con == nil || con.Rpc == nil {
|
||||
if con != nil {
|
||||
con.PrintErrorf("Connect to a server before using `ai`.\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
listenerID, listener := con.CreateEventListener()
|
||||
defer con.RemoveEventListener(listenerID)
|
||||
|
||||
model := newAIModel(con, buildAIContext(con), listener)
|
||||
|
||||
width, height := 100, 30
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
width, height = w, h
|
||||
}
|
||||
|
||||
program := tea.NewProgram(
|
||||
model,
|
||||
tea.WithWindowSize(width, height),
|
||||
tea.WithColorProfile(colorprofile.TrueColor),
|
||||
)
|
||||
if _, err := program.Run(); err != nil {
|
||||
con.PrintErrorf("AI TUI error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
type aiContext struct {
|
||||
target aiTargetSummary
|
||||
connection aiConnectionSummary
|
||||
status string
|
||||
}
|
||||
|
||||
type aiTargetSummary struct {
|
||||
Label string
|
||||
Host string
|
||||
OS string
|
||||
Arch string
|
||||
C2 string
|
||||
Mode string
|
||||
Details []string
|
||||
}
|
||||
|
||||
type aiConnectionSummary struct {
|
||||
Profile string
|
||||
Server string
|
||||
Operator string
|
||||
State string
|
||||
}
|
||||
|
||||
func buildAIContext(con *console.SliverClient) aiContext {
|
||||
ctx := aiContext{
|
||||
target: aiTargetSummary{
|
||||
Label: "No active target",
|
||||
Host: "Select a session or beacon with `use`",
|
||||
Mode: "offline preview",
|
||||
C2: "n/a",
|
||||
OS: "unknown",
|
||||
Arch: "unknown",
|
||||
},
|
||||
connection: aiConnectionSummary{
|
||||
Profile: "<disconnected>",
|
||||
Server: "<unknown>",
|
||||
Operator: "<unknown>",
|
||||
State: "idle",
|
||||
},
|
||||
status: "Loading AI conversations from the server...",
|
||||
}
|
||||
|
||||
if con != nil {
|
||||
if details, state, ok := con.CurrentConnection(); ok {
|
||||
ctx.connection.State = strings.ToLower(state.String())
|
||||
if details != nil && details.Config != nil {
|
||||
ctx.connection.Profile = fallback(details.ConfigKey, "<profile unavailable>")
|
||||
ctx.connection.Server = fmt.Sprintf("%s:%d", details.Config.LHost, details.Config.LPort)
|
||||
ctx.connection.Operator = fallback(details.Config.Operator, "<unknown>")
|
||||
}
|
||||
}
|
||||
|
||||
session, beacon := con.ActiveTarget.Get()
|
||||
switch {
|
||||
case session != nil:
|
||||
ctx.target = aiTargetSummary{
|
||||
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
|
||||
Host: fallback(session.Hostname, "<unknown host>"),
|
||||
OS: fallback(session.OS, "unknown"),
|
||||
Arch: fallback(session.Arch, "unknown"),
|
||||
C2: fallback(session.ActiveC2, "unknown"),
|
||||
Mode: "interactive session",
|
||||
Details: []string{
|
||||
fmt.Sprintf("User: %s", fallback(session.Username, "<unknown>")),
|
||||
fmt.Sprintf("PID: %d", session.PID),
|
||||
fmt.Sprintf("Remote: %s", fallback(session.RemoteAddress, "<unknown>")),
|
||||
},
|
||||
}
|
||||
case beacon != nil:
|
||||
ctx.target = aiTargetSummary{
|
||||
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
|
||||
Host: fallback(beacon.Hostname, "<unknown host>"),
|
||||
OS: fallback(beacon.OS, "unknown"),
|
||||
Arch: fallback(beacon.Arch, "unknown"),
|
||||
C2: fallback(beacon.ActiveC2, "unknown"),
|
||||
Mode: "asynchronous beacon",
|
||||
Details: []string{
|
||||
fmt.Sprintf("User: %s", fallback(beacon.Username, "<unknown>")),
|
||||
fmt.Sprintf("Interval: %s", time.Duration(beacon.Interval).String()),
|
||||
fmt.Sprintf("Next checkin: %s", formatUnix(beacon.NextCheckin)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func fallback(value, def string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return def
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func formatUnix(ts int64) string {
|
||||
if ts <= 0 {
|
||||
return "<unknown>"
|
||||
}
|
||||
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Commands returns the ai command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newAICommand(consts.SliverCoreHelpGroup, con)}
|
||||
}
|
||||
|
||||
// ServerCommands returns the ai command for the top-level client REPL.
|
||||
func ServerCommands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newAICommand(consts.GenericHelpGroup, con)}
|
||||
}
|
||||
|
||||
func newAICommand(groupID string, con *console.SliverClient) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: consts.AIStr,
|
||||
Short: "Open the Sliver AI conversation TUI",
|
||||
Long: help.GetHelpFor([]string{consts.AIStr}),
|
||||
Args: cobra.NoArgs,
|
||||
GroupID: groupID,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
AICmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestLoadAIStateCmdLoadsConversationOverRPC(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
providersResp: &clientpb.AIProviderConfigs{
|
||||
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
|
||||
Config: &clientpb.AIConfigSummary{
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
conversationsResp: &clientpb.AIConversations{
|
||||
Conversations: []*clientpb.AIConversation{
|
||||
{ID: "conv-1", Provider: "openai", Model: "gpt-test", Title: "First"},
|
||||
{ID: "conv-2", Provider: "openai", Model: "gpt-test", Title: "Second"},
|
||||
},
|
||||
},
|
||||
conversationByID: map[string]*clientpb.AIConversation{
|
||||
"conv-1": {
|
||||
ID: "conv-1",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "First",
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "user", Content: "Hello"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
cmd := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, "")
|
||||
msg := cmd()
|
||||
|
||||
loaded, ok := msg.(aiLoadedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiLoadedMsg, got %T", msg)
|
||||
}
|
||||
if loaded.selectedID != "conv-1" {
|
||||
t.Fatalf("unexpected selected conversation id: got=%q want=%q", loaded.selectedID, "conv-1")
|
||||
}
|
||||
if loaded.conversation == nil || loaded.conversation.GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected loaded conversation: %+v", loaded.conversation)
|
||||
}
|
||||
if len(loaded.conversations) != 2 {
|
||||
t.Fatalf("unexpected conversation count: got=%d want=%d", len(loaded.conversations), 2)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if server.getProvidersCalls != 1 {
|
||||
t.Fatalf("expected GetAIProviders to be called once, got %d", server.getProvidersCalls)
|
||||
}
|
||||
if server.getConversationsCalls != 1 {
|
||||
t.Fatalf("expected GetAIConversations to be called once, got %d", server.getConversationsCalls)
|
||||
}
|
||||
if len(server.getConversationReqs) != 1 {
|
||||
t.Fatalf("expected GetAIConversation to be called once, got %d", len(server.getConversationReqs))
|
||||
}
|
||||
if server.getConversationReqs[0].GetID() != "conv-1" || !server.getConversationReqs[0].GetIncludeMessages() {
|
||||
t.Fatalf("unexpected GetAIConversation request: %+v", server.getConversationReqs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAIStateCmdCreatesConversationWhenNoneExist(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
providersResp: &clientpb.AIProviderConfigs{
|
||||
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
|
||||
Config: &clientpb.AIConfigSummary{
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
conversationsResp: &clientpb.AIConversations{},
|
||||
saveConversationResp: &clientpb.AIConversation{
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "New conversation",
|
||||
},
|
||||
conversationByID: map[string]*clientpb.AIConversation{
|
||||
"conv-created": {
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "New conversation",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, "")()
|
||||
|
||||
loaded, ok := msg.(aiLoadedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiLoadedMsg, got %T", msg)
|
||||
}
|
||||
if loaded.status != "Created a new AI conversation." {
|
||||
t.Fatalf("unexpected status: %q", loaded.status)
|
||||
}
|
||||
if loaded.selectedID != "conv-created" {
|
||||
t.Fatalf("unexpected selected conversation id: %q", loaded.selectedID)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
|
||||
}
|
||||
request := server.saveConversationReqs[0]
|
||||
if request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetTitle() != "New conversation" {
|
||||
t.Fatalf("unexpected SaveAIConversation request: %+v", request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitPromptCmdCreatesConversationAndSavesUserMessage(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
saveConversationResp: &clientpb.AIConversation{
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "Explain the workflow.",
|
||||
},
|
||||
saveMessageResp: &clientpb.AIConversationMessage{
|
||||
ID: "msg-1",
|
||||
ConversationID: "conv-created",
|
||||
Role: "user",
|
||||
Content: "Explain the workflow.\n\nWith details.",
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := submitPromptCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
nil,
|
||||
"openai",
|
||||
"gpt-test",
|
||||
"Explain the workflow.\n\nWith details.",
|
||||
)()
|
||||
|
||||
submitted, ok := msg.(aiPromptSubmittedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
|
||||
}
|
||||
if submitted.conversationID != "conv-created" {
|
||||
t.Fatalf("unexpected conversation id: %q", submitted.conversationID)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
|
||||
}
|
||||
if server.saveConversationReqs[0].GetTitle() != "Explain the workflow." {
|
||||
t.Fatalf("unexpected conversation title: %q", server.saveConversationReqs[0].GetTitle())
|
||||
}
|
||||
if len(server.saveMessageReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
|
||||
}
|
||||
request := server.saveMessageReqs[0]
|
||||
if request.GetConversationID() != "conv-created" || request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetRole() != "user" {
|
||||
t.Fatalf("unexpected SaveAIConversationMessage request: %+v", request)
|
||||
}
|
||||
if request.GetContent() != "Explain the workflow.\n\nWith details." {
|
||||
t.Fatalf("unexpected prompt content: %q", request.GetContent())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitPromptCmdUsesExistingConversationSettings(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
saveMessageResp: &clientpb.AIConversationMessage{
|
||||
ID: "msg-1",
|
||||
ConversationID: "conv-1",
|
||||
Role: "user",
|
||||
Content: "hello",
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := submitPromptCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
&clientpb.AIConversation{ID: "conv-1", Provider: "anthropic", Model: "claude-test", Title: "Thread"},
|
||||
"openai",
|
||||
"gpt-test",
|
||||
"hello",
|
||||
)()
|
||||
|
||||
if _, ok := msg.(aiPromptSubmittedMsg); !ok {
|
||||
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 0 {
|
||||
t.Fatalf("did not expect SaveAIConversation to be called, got %d calls", len(server.saveConversationReqs))
|
||||
}
|
||||
if len(server.saveMessageReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
|
||||
}
|
||||
request := server.saveMessageReqs[0]
|
||||
if request.GetProvider() != "anthropic" || request.GetModel() != "claude-test" {
|
||||
t.Fatalf("unexpected conversation settings in message request: %+v", request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteConversationCmdSendsDeleteRequest(t *testing.T) {
|
||||
server := &aiRPCServer{}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := deleteConversationCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
"conv-1",
|
||||
"conv-2",
|
||||
`Deleted "Thread".`,
|
||||
)()
|
||||
|
||||
deleted, ok := msg.(aiConversationDeletedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiConversationDeletedMsg, got %T", msg)
|
||||
}
|
||||
if deleted.conversationID != "conv-1" || deleted.selectedID != "conv-2" {
|
||||
t.Fatalf("unexpected delete message: %+v", deleted)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.deleteConversationReqs) != 1 {
|
||||
t.Fatalf("expected DeleteAIConversation to be called once, got %d", len(server.deleteConversationReqs))
|
||||
}
|
||||
if server.deleteConversationReqs[0].GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected DeleteAIConversation request: %+v", server.deleteConversationReqs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdFiltersForAIEvents(t *testing.T) {
|
||||
conversation := &clientpb.AIConversation{ID: "conv-1", OperatorName: "alice"}
|
||||
data, err := proto.Marshal(conversation)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal conversation: %v", err)
|
||||
}
|
||||
|
||||
listener := make(chan *clientpb.Event, 2)
|
||||
listener <- &clientpb.Event{EventType: consts.WatchtowerEvent}
|
||||
listener <- &clientpb.Event{EventType: consts.AIConversationEvent, Data: data}
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
|
||||
aiEvent, ok := msg.(aiConversationEventMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiConversationEventMsg, got %T", msg)
|
||||
}
|
||||
if aiEvent.conversation == nil || aiEvent.conversation.GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected AI conversation event payload: %+v", aiEvent.conversation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdReturnsClosedMessage(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
if _, ok := msg.(aiListenerClosedMsg); !ok {
|
||||
t.Fatalf("expected aiListenerClosedMsg, got %T", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIModelReloadsSharedConversationEvents(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
model := newAIModel(nil, aiContext{
|
||||
connection: aiConnectionSummary{Operator: "alice"},
|
||||
}, listener)
|
||||
model.loading = false
|
||||
|
||||
updated, cmd := model.Update(aiConversationEventMsg{conversation: &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
OperatorName: "bob",
|
||||
}})
|
||||
|
||||
nextModel := updated.(*aiModel)
|
||||
if !nextModel.loading {
|
||||
t.Fatal("expected shared conversation event to trigger a reload")
|
||||
}
|
||||
if nextModel.status != "Conversation updated on the server. Refreshing..." {
|
||||
t.Fatalf("unexpected reload status: %q", nextModel.status)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected shared conversation event to schedule a refresh")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIModelReloadsDeleteEventsWhileAwaitingResponse(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
model := newAIModel(nil, aiContext{
|
||||
connection: aiConnectionSummary{Operator: "alice"},
|
||||
}, listener)
|
||||
model.loading = false
|
||||
model.awaitingResponse = true
|
||||
model.currentConversation = &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
OperatorName: "alice",
|
||||
UpdatedAt: 100,
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "user", Content: "still waiting"},
|
||||
},
|
||||
}
|
||||
|
||||
updated, cmd := model.Update(aiConversationEventMsg{conversation: &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
OperatorName: "bob",
|
||||
}})
|
||||
|
||||
nextModel := updated.(*aiModel)
|
||||
if !nextModel.loading {
|
||||
t.Fatal("expected delete tombstone to reload the conversation")
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected delete tombstone to schedule a refresh")
|
||||
}
|
||||
}
|
||||
|
||||
type aiRPCServer struct {
|
||||
rpcpb.UnimplementedSliverRPCServer
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
providersResp *clientpb.AIProviderConfigs
|
||||
conversationsResp *clientpb.AIConversations
|
||||
conversationByID map[string]*clientpb.AIConversation
|
||||
saveConversationResp *clientpb.AIConversation
|
||||
saveMessageResp *clientpb.AIConversationMessage
|
||||
|
||||
getProvidersCalls int
|
||||
getConversationsCalls int
|
||||
getConversationReqs []*clientpb.AIConversationReq
|
||||
deleteConversationReqs []*clientpb.AIConversationReq
|
||||
saveConversationReqs []*clientpb.AIConversation
|
||||
saveMessageReqs []*clientpb.AIConversationMessage
|
||||
getConversationStart chan struct{}
|
||||
getConversationWait <-chan struct{}
|
||||
saveMessageStarted chan struct{}
|
||||
saveMessageRelease <-chan struct{}
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetAIProviders(context.Context, *commonpb.Empty) (*clientpb.AIProviderConfigs, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getProvidersCalls++
|
||||
if s.providersResp == nil {
|
||||
return &clientpb.AIProviderConfigs{}, nil
|
||||
}
|
||||
return proto.Clone(s.providersResp).(*clientpb.AIProviderConfigs), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetAIConversations(context.Context, *commonpb.Empty) (*clientpb.AIConversations, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getConversationsCalls++
|
||||
if s.conversationsResp == nil {
|
||||
return &clientpb.AIConversations{}, nil
|
||||
}
|
||||
return proto.Clone(s.conversationsResp).(*clientpb.AIConversations), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*clientpb.AIConversation, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getConversationReqs = append(s.getConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
|
||||
if s.getConversationStart != nil {
|
||||
select {
|
||||
case s.getConversationStart <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if s.getConversationWait != nil {
|
||||
s.mu.Unlock()
|
||||
<-s.getConversationWait
|
||||
s.mu.Lock()
|
||||
}
|
||||
if s.conversationByID == nil {
|
||||
return &clientpb.AIConversation{}, nil
|
||||
}
|
||||
conversation := s.conversationByID[req.GetID()]
|
||||
if conversation == nil {
|
||||
return &clientpb.AIConversation{}, nil
|
||||
}
|
||||
return proto.Clone(conversation).(*clientpb.AIConversation), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) SaveAIConversation(_ context.Context, req *clientpb.AIConversation) (*clientpb.AIConversation, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.saveConversationReqs = append(s.saveConversationReqs, proto.Clone(req).(*clientpb.AIConversation))
|
||||
if s.saveConversationResp == nil {
|
||||
return proto.Clone(req).(*clientpb.AIConversation), nil
|
||||
}
|
||||
return proto.Clone(s.saveConversationResp).(*clientpb.AIConversation), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) DeleteAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*commonpb.Empty, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.deleteConversationReqs = append(s.deleteConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) SaveAIConversationMessage(_ context.Context, req *clientpb.AIConversationMessage) (*clientpb.AIConversationMessage, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.saveMessageReqs = append(s.saveMessageReqs, proto.Clone(req).(*clientpb.AIConversationMessage))
|
||||
if s.saveMessageStarted != nil {
|
||||
select {
|
||||
case s.saveMessageStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if s.saveMessageRelease != nil {
|
||||
s.mu.Unlock()
|
||||
<-s.saveMessageRelease
|
||||
s.mu.Lock()
|
||||
}
|
||||
if s.saveMessageResp == nil {
|
||||
return proto.Clone(req).(*clientpb.AIConversationMessage), nil
|
||||
}
|
||||
return proto.Clone(s.saveMessageResp).(*clientpb.AIConversationMessage), nil
|
||||
}
|
||||
|
||||
func newAITestRPCClient(t *testing.T, srv rpcpb.SliverRPCServer) (rpcpb.SliverRPCClient, func()) {
|
||||
t.Helper()
|
||||
|
||||
listener := bufconn.Listen(1024 * 1024)
|
||||
grpcServer := grpc.NewServer()
|
||||
rpcpb.RegisterSliverRPCServer(grpcServer, srv)
|
||||
|
||||
go func() {
|
||||
_ = grpcServer.Serve(listener)
|
||||
}()
|
||||
|
||||
dialer := func(context.Context, string) (net.Conn, error) { return listener.Dial() }
|
||||
conn, err := grpc.DialContext(context.Background(), "bufnet",
|
||||
grpc.WithContextDialer(dialer),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
grpcServer.Stop()
|
||||
_ = listener.Close()
|
||||
t.Fatalf("dial bufconn: %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
_ = conn.Close()
|
||||
grpcServer.Stop()
|
||||
_ = listener.Close()
|
||||
}
|
||||
|
||||
return rpcpb.NewSliverRPCClient(conn), cleanup
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -250,7 +250,7 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
|
||||
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
|
||||
f.BoolP("save", "s", false, "Save output to disk")
|
||||
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
addAliasCmd.Flags().AddFlagSet(f)
|
||||
|
||||
// Setup completions for alias arguments
|
||||
|
||||
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
},
|
||||
}
|
||||
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("kill", "k", "", "kill the designated beacon")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Commands returns the docs command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newDocsCommand(consts.SliverCoreHelpGroup, con)}
|
||||
}
|
||||
|
||||
// ServerCommands returns the docs command for the top-level client REPL.
|
||||
func ServerCommands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newDocsCommand(consts.GenericHelpGroup, con)}
|
||||
}
|
||||
|
||||
func newDocsCommand(groupID string, con *console.SliverClient) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: consts.DocsStr,
|
||||
Short: "Browse the embedded Sliver docs in a TUI",
|
||||
Long: help.GetHelpFor([]string{consts.DocsStr}),
|
||||
Args: cobra.NoArgs,
|
||||
GroupID: groupID,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
DocsCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// DocsCmd launches the embedded docs browser TUI.
|
||||
func DocsCmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
entries, err := loadDocEntries()
|
||||
if err != nil {
|
||||
if con != nil {
|
||||
con.PrintErrorf("Failed to load embedded docs: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
model := newDocsModel(entries)
|
||||
|
||||
width, height := 100, 32
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
width, height = w, h
|
||||
}
|
||||
|
||||
program := tea.NewProgram(
|
||||
model,
|
||||
tea.WithWindowSize(width, height),
|
||||
tea.WithColorProfile(colorprofile.TrueColor),
|
||||
)
|
||||
if _, err := program.Run(); err != nil && con != nil {
|
||||
con.PrintErrorf("Docs TUI error: %s\n", err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,199 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
func sampleDocs() []docEntry {
|
||||
return []docEntry{
|
||||
{
|
||||
Name: "Getting Started",
|
||||
Content: "# Getting Started\n\n- first step\n- second step",
|
||||
Description: "Getting Started",
|
||||
},
|
||||
{
|
||||
Name: "Operators",
|
||||
Content: "# Operators\n\nTeam workflows",
|
||||
Description: "Team workflows",
|
||||
},
|
||||
{
|
||||
Name: "Networking",
|
||||
Content: "# Networking\n\nTransport details",
|
||||
Description: "Transport details",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMarkdownWithGlowRendersBulletLists(t *testing.T) {
|
||||
rendered, err := renderMarkdownWithGlow(48, "# Title\n\n- first item")
|
||||
if err != nil {
|
||||
t.Fatalf("renderMarkdownWithGlow(): %v", err)
|
||||
}
|
||||
|
||||
plain := ansi.Strip(rendered)
|
||||
if !strings.Contains(plain, "Title") {
|
||||
t.Fatalf("expected heading text, got %q", plain)
|
||||
}
|
||||
if !strings.Contains(plain, "• first item") {
|
||||
t.Fatalf("expected glow-style bullet rendering, got %q", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeMarkdownPrefersFirstMeaningfulLine(t *testing.T) {
|
||||
summary := summarizeMarkdown("# Title\n\n- Bullet\n\nParagraph")
|
||||
if summary != "Title" {
|
||||
t.Fatalf("expected first heading summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeMarkdownStripsMarkdownAndUnicodeSymbols(t *testing.T) {
|
||||
summary := summarizeMarkdown("⚠️ **IMPORTANT:** This content was NOT created by the Sliver authors.")
|
||||
if summary != "IMPORTANT: This content was NOT created by the Sliver authors." {
|
||||
t.Fatalf("expected sanitized browser summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelDefaultsToGettingStarted(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
if model.currentDocName != "Getting Started" {
|
||||
t.Fatalf("expected Getting Started to be selected, got %q", model.currentDocName)
|
||||
}
|
||||
plain := ansi.Strip(model.viewer.View())
|
||||
if !strings.Contains(plain, "first step") {
|
||||
t.Fatalf("expected initial viewer content to render selected doc, got %q", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelSlashFocusesBrowserAndStartsFiltering(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
model.focus = docsFocusViewer
|
||||
|
||||
updated, cmd := model.Update(tea.KeyPressMsg{Text: "/", Code: '/'})
|
||||
if cmd == nil {
|
||||
t.Fatal("expected slash to focus the search input")
|
||||
}
|
||||
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.focus != docsFocusBrowser {
|
||||
t.Fatalf("expected slash to move focus to browser, got %v", updatedModel.focus)
|
||||
}
|
||||
if updatedModel.filterState != docsFilterStateFiltering {
|
||||
t.Fatalf("expected slash to enter filtering state, got %v", updatedModel.filterState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelFilteringUpdatesVisibleEntries(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("oper")
|
||||
model.updateFilteredEntries(false)
|
||||
|
||||
visible := model.visibleEntries()
|
||||
if len(visible) != 1 || visible[0].Name != "Operators" {
|
||||
t.Fatalf("expected Operators to be the only filtered result, got %+v", visible)
|
||||
}
|
||||
if model.currentDocName != "Operators" {
|
||||
t.Fatalf("expected filtering to sync selection to Operators, got %q", model.currentDocName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelEnterMovesFocusToViewer(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.focus != docsFocusViewer {
|
||||
t.Fatalf("expected enter to focus viewer, got %v", updatedModel.focus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelEscClearsAppliedFilter(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("net")
|
||||
model.finishFiltering(false)
|
||||
|
||||
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.filterState != docsFilterStateUnfiltered {
|
||||
t.Fatalf("expected esc to clear the filter, got %v", updatedModel.filterState)
|
||||
}
|
||||
if len(updatedModel.visibleEntries()) != len(sampleDocs()) {
|
||||
t.Fatalf("expected filter clear to restore all docs, got %d visible", len(updatedModel.visibleEntries()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelBrowserBodyPinsHelpToBottom(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 28)
|
||||
|
||||
body := ansi.Strip(model.renderBrowserBody(model.browserWidth, model.browserHeight))
|
||||
lines := strings.Split(body, "\n")
|
||||
if len(lines) != model.browserHeight {
|
||||
t.Fatalf("expected browser body height %d, got %d lines", model.browserHeight, len(lines))
|
||||
}
|
||||
if !strings.Contains(lines[len(lines)-1], "q quit") {
|
||||
t.Fatalf("expected help controls on the last browser line, got %q", lines[len(lines)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsWindowPollSchedulesResizeAndKeepsPolling(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(100, 28)
|
||||
|
||||
updated, cmd := model.Update(docsWindowPollMsg{width: 120, height: 34})
|
||||
if cmd == nil {
|
||||
t.Fatal("expected window poll to schedule resize handling")
|
||||
}
|
||||
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.width != 100 || updatedModel.height != 28 {
|
||||
t.Fatalf("expected poll tick to leave dimensions unchanged until WindowSizeMsg, got %dx%d", updatedModel.width, updatedModel.height)
|
||||
}
|
||||
|
||||
msg := cmd()
|
||||
batch, ok := msg.(tea.BatchMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected poll tick to return tea.BatchMsg, got %T", msg)
|
||||
}
|
||||
if len(batch) != 2 {
|
||||
t.Fatalf("expected poll tick batch size 2, got %d", len(batch))
|
||||
}
|
||||
|
||||
var sawWindowSize bool
|
||||
var sawPollTick bool
|
||||
for _, subcmd := range batch {
|
||||
if subcmd == nil {
|
||||
continue
|
||||
}
|
||||
submsg := subcmd()
|
||||
switch msg := submsg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
sawWindowSize = true
|
||||
if msg.Width != 120 || msg.Height != 34 {
|
||||
t.Fatalf("expected resize 120x34, got %dx%d", msg.Width, msg.Height)
|
||||
}
|
||||
case docsWindowPollMsg:
|
||||
sawPollTick = true
|
||||
}
|
||||
}
|
||||
|
||||
if !sawWindowSize {
|
||||
t.Fatal("expected poll tick to emit WindowSizeMsg")
|
||||
}
|
||||
if !sawPollTick {
|
||||
t.Fatal("expected poll tick to schedule another poll")
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package extensions
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/util"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -114,6 +116,8 @@ const (
|
||||
)
|
||||
|
||||
func TestParseExtensionManifest(t *testing.T) {
|
||||
expectedPath := util.ResolvePath("foo/test1.dll")
|
||||
|
||||
extManifest, err := ParseExtensionManifest([]byte(sample1))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing extension manifest: %s", err)
|
||||
@@ -150,8 +154,8 @@ func TestParseExtensionManifest(t *testing.T) {
|
||||
if extCmd.Files[0].Arch != "amd64" {
|
||||
t.Errorf("Expected Arch 'amd64', got '%s'", extCmd.Files[0].Arch)
|
||||
}
|
||||
if extCmd.Files[0].Path != "/foo/test1.dll" {
|
||||
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extCmd.Files[0].Path)
|
||||
if extCmd.Files[0].Path != expectedPath {
|
||||
t.Errorf("Expected path '%s', got '%s'", expectedPath, extCmd.Files[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,14 +182,16 @@ func TestParseExtensionManifest(t *testing.T) {
|
||||
if extManifest2.Files[0].Arch != "amd64" {
|
||||
t.Errorf("Expected Arch 'amd64', got '%s'", extManifest2.Files[0].Arch)
|
||||
}
|
||||
if extManifest2.Files[0].Path != "/foo/test1.dll" {
|
||||
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extManifest2.Files[0].Path)
|
||||
if extManifest2.Files[0].Path != expectedPath {
|
||||
t.Errorf("Expected path '%s', got '%s'", expectedPath, extManifest2.Files[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestParseMultipleCmdManifest(t *testing.T) {
|
||||
expectedPath := util.ResolvePath("ex.dll")
|
||||
|
||||
mextManifest, err := ParseExtensionManifest([]byte(multicmd))
|
||||
if err != nil {
|
||||
t.Errorf("error parsing manifest: %s", err)
|
||||
@@ -206,11 +212,11 @@ func TestParseMultipleCmdManifest(t *testing.T) {
|
||||
if mextManifest.ExtCommand[1].Entrypoint != "Test2" {
|
||||
t.Errorf("expected entrypoint Test2, got %s", mextManifest.ExtCommand[1].Entrypoint)
|
||||
}
|
||||
if mextManifest.ExtCommand[0].Files[0].Path != "/ex.dll" { //path cleaning adds a root path here? I am not sure if this should be a bug or not... works fine in prod
|
||||
t.Errorf("expected path ex.dll, got %s", mextManifest.ExtCommand[0].Files[0].Path)
|
||||
if mextManifest.ExtCommand[0].Files[0].Path != expectedPath { // path normalization is platform-specific
|
||||
t.Errorf("expected path %s, got %s", expectedPath, mextManifest.ExtCommand[0].Files[0].Path)
|
||||
}
|
||||
if mextManifest.ExtCommand[1].Files[0].Path != "/ex.dll" { //path cleaning adds a root path here? I am not sure if this should be a bug or not... works fine in prod
|
||||
t.Errorf("expected path ex.dll, got %s", mextManifest.ExtCommand[0].Files[0].Path)
|
||||
if mextManifest.ExtCommand[1].Files[0].Path != expectedPath { // path normalization is platform-specific
|
||||
t.Errorf("expected path %s, got %s", expectedPath, mextManifest.ExtCommand[1].Files[0].Path)
|
||||
}
|
||||
//maybe some more? args?
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ func ExtensionRegisterCommand(extCmd *ExtCommand, cmd *cobra.Command, con *conso
|
||||
// Flags
|
||||
f := pflag.NewFlagSet(extCmd.CommandName, pflag.ContinueOnError)
|
||||
f.BoolP("save", "s", false, "Save output to disk")
|
||||
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
extensionCmd.Flags().AddFlagSet(f)
|
||||
extensionCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
GroupID: consts.PayloadsHelpGroup,
|
||||
}
|
||||
flags.Bind("generate", true, generateCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
// Session flags and completions.
|
||||
@@ -340,6 +340,13 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
|
||||
|
||||
f.Uint32P("key-exchange", "X", DefaultWGKeyExPort, "wg key-exchange port")
|
||||
f.Uint32P("tcp-comms", "T", DefaultWGNPort, "wg c2 comms port")
|
||||
f.Bool("include-mtls", false, "force include mtls transport")
|
||||
f.Bool("include-http", false, "force include http transport")
|
||||
f.Bool("include-wg", false, "force include wireguard transport")
|
||||
f.Bool("include-dns", false, "force include dns transport")
|
||||
f.Bool("include-named-pipe", false, "force include named-pipe transport")
|
||||
f.Bool("include-tcp-pivot", false, "force include tcp-pivot transport")
|
||||
f.Bool("all-protocols", false, "force include all transport protocols")
|
||||
|
||||
f.BoolP("run-at-load", "R", false, "run the implant entrypoint from DllMain/Constructor (shared library only)")
|
||||
f.BoolP("netgo", "q", false, "force the use of netgo")
|
||||
|
||||
@@ -483,6 +483,23 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
||||
c2Profile = consts.DefaultC2Profile
|
||||
}
|
||||
|
||||
allProtocols, _ := cmd.Flags().GetBool("all-protocols")
|
||||
includeMTLS, _ := cmd.Flags().GetBool("include-mtls")
|
||||
includeHTTP, _ := cmd.Flags().GetBool("include-http")
|
||||
includeWG, _ := cmd.Flags().GetBool("include-wg")
|
||||
includeDNS, _ := cmd.Flags().GetBool("include-dns")
|
||||
includeNamedPipe, _ := cmd.Flags().GetBool("include-named-pipe")
|
||||
includeTCP, _ := cmd.Flags().GetBool("include-tcp-pivot")
|
||||
|
||||
if allProtocols {
|
||||
includeMTLS = true
|
||||
includeHTTP = true
|
||||
includeWG = true
|
||||
includeDNS = true
|
||||
includeNamedPipe = true
|
||||
includeTCP = true
|
||||
}
|
||||
|
||||
// exports if its a shared library
|
||||
|
||||
config := &clientpb.ImplantConfig{
|
||||
@@ -527,6 +544,13 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
||||
DebugFile: debugFile,
|
||||
HTTPC2ConfigName: c2Profile,
|
||||
ShellcodeConfig: shellcodeConfig,
|
||||
|
||||
IncludeMTLS: includeMTLS,
|
||||
IncludeHTTP: includeHTTP,
|
||||
IncludeWG: includeWG,
|
||||
IncludeDNS: includeDNS,
|
||||
IncludeNamePipe: includeNamedPipe,
|
||||
IncludeTCP: includeTCP,
|
||||
}
|
||||
|
||||
return name, config
|
||||
|
||||
@@ -56,6 +56,9 @@ var (
|
||||
consts.LsStr: lsHelp,
|
||||
consts.CdStr: cdHelp,
|
||||
consts.PwdStr: pwdHelp,
|
||||
consts.AIStr: aiHelp,
|
||||
consts.AIConfigStr: aiConfigHelp,
|
||||
consts.DocsStr: docsHelp,
|
||||
consts.CatStr: catHelp,
|
||||
consts.EditStr: editHelp,
|
||||
consts.HexEditStr: hexEditHelp,
|
||||
@@ -167,6 +170,29 @@ _=/usr/bin/env
|
||||
infoHelp = `[[.Bold]]Command:[[.Normal]] info <sliver name/session>
|
||||
[[.Bold]]About:[[.Normal]] Get information about a Sliver by name, or for the active Sliver.`
|
||||
|
||||
aiHelp = `[[.Bold]]Command:[[.Normal]] ai
|
||||
[[.Bold]]About:[[.Normal]] Launch the Sliver AI Bubble Tea TUI. The AI workspace now loads server-side conversation threads over gRPC, keeps them synced through the shared event stream, and renders conversation transcripts as markdown in the main pane.
|
||||
|
||||
[[.Bold]]Example:[[.Normal]]
|
||||
use my-session
|
||||
ai
|
||||
`
|
||||
|
||||
aiConfigHelp = `[[.Bold]]Command:[[.Normal]] ai-config
|
||||
[[.Bold]]About:[[.Normal]] Open a server-side form to update the AI block in server.yaml. The form stores the default provider, model, thinking level, and provider-specific credentials such as the API key and optional base URL.
|
||||
|
||||
[[.Bold]]Example:[[.Normal]]
|
||||
ai-config
|
||||
`
|
||||
|
||||
docsHelp = `[[.Bold]]Command:[[.Normal]] docs
|
||||
[[.Bold]]About:[[.Normal]] Browse the embedded Sliver documentation in a Bubble Tea TUI. The left pane is a searchable docs browser, and the right pane renders markdown with Glow styling.
|
||||
[[.Bold]]Keys:[[.Normal]] Use / to filter docs, enter to focus the selected document, tab to switch panes, and q to quit.
|
||||
|
||||
[[.Bold]]Example:[[.Normal]]
|
||||
docs
|
||||
`
|
||||
|
||||
useHelp = `[[.Bold]]Command:[[.Normal]] use [sliver name/session]
|
||||
[[.Bold]]About:[[.Normal]] Switch the active Sliver, a valid name must be provided (see sessions).`
|
||||
|
||||
|
||||
@@ -43,12 +43,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
GroupID: consts.NetworkHelpGroup,
|
||||
}
|
||||
flags.Bind("jobs", true, jobsCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("jobs", false, jobsCmd, func(f *pflag.FlagSet) {
|
||||
f.Int32P("kill", "k", -1, "kill a background job")
|
||||
f.BoolP("kill-all", "K", false, "kill all jobs")
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.BindFlagCompletions(jobsCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["kill"] = JobsIDCompleter(con)
|
||||
|
||||
@@ -24,7 +24,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
GroupID: consts.SliverHelpGroup,
|
||||
}
|
||||
flags.Bind("loot", true, lootCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("loot", false, lootCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("filter", "f", "", "filter based on loot type")
|
||||
|
||||
@@ -81,7 +81,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
pivotsCmd.AddCommand(pivotDetailsCmd)
|
||||
flags.Bind("", false, pivotDetailsCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("id", "i", 0, "id of the pivot listener to get details for")
|
||||
f.Uint32P("id", "i", 0, "id of the pivot listener to get details for")
|
||||
})
|
||||
flags.BindFlagCompletions(pivotDetailsCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["id"] = PivotIDCompleter(con)
|
||||
|
||||
@@ -39,6 +39,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.Bind("", false, addCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("remote", "r", "", "remote target host:port (e.g., 10.0.0.1:445)")
|
||||
f.StringP("bind", "b", "127.0.0.1:8080", "bind port forward to interface")
|
||||
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
|
||||
})
|
||||
flags.BindFlagCompletions(addCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["bind"] = completers.ClientInterfacesCompleter()
|
||||
|
||||
@@ -68,13 +68,23 @@ func PortfwdAddCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
bindAddr = fmt.Sprintf("127.0.0.1:%s", bindAddr)
|
||||
}
|
||||
|
||||
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
|
||||
var keepAlivePeriod time.Duration
|
||||
if keepAlive > 0 {
|
||||
keepAlivePeriod = time.Duration(keepAlive) * time.Second
|
||||
} else if keepAlive < 0 {
|
||||
keepAlivePeriod = -1 * time.Second
|
||||
} else {
|
||||
keepAlivePeriod = 30 * time.Second
|
||||
}
|
||||
|
||||
tcpProxy := &tcpproxy.Proxy{}
|
||||
channelProxy := &core.ChannelProxy{
|
||||
Rpc: con.Rpc,
|
||||
Session: session,
|
||||
RemoteAddr: remoteAddr,
|
||||
BindAddr: bindAddr,
|
||||
KeepAlivePeriod: 60 * time.Second,
|
||||
KeepAlivePeriod: keepAlivePeriod,
|
||||
DialTimeout: 30 * time.Second,
|
||||
}
|
||||
tcpProxy.AddRoute(bindAddr, channelProxy)
|
||||
|
||||
@@ -74,7 +74,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
GetSystemCmd(cmd, con, args)
|
||||
},
|
||||
GroupID: consts.PrivilegesHelpGroup,
|
||||
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
|
||||
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter, consts.SessionCmdsFilter),
|
||||
}
|
||||
flags.Bind("", false, getSystemCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("process", "p", "spoolsv.exe", "SYSTEM process to inject into")
|
||||
|
||||
@@ -37,6 +37,10 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
if session == nil {
|
||||
con.PrintErrorf("Command is only supported for sessions.\n")
|
||||
return
|
||||
}
|
||||
targetOS := getOS(session, beacon)
|
||||
if targetOS != "windows" {
|
||||
con.PrintErrorf("Command only supported on Windows.\n")
|
||||
@@ -45,6 +49,10 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
|
||||
process, _ := cmd.Flags().GetString("process")
|
||||
config := con.GetActiveSessionConfig()
|
||||
if config == nil {
|
||||
con.PrintErrorf("Failed to derive active session config.\n")
|
||||
return
|
||||
}
|
||||
|
||||
/* If the HTTP C2 Config name is not defined, then put in the default value
|
||||
This will have no effect on implants that do not use HTTP C2
|
||||
|
||||
@@ -52,7 +52,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.BoolP("loot", "X", false, "save output as loot")
|
||||
f.StringP("loot-name", "N", "", "name to assign when adding the memory dump to the loot store (optional)")
|
||||
|
||||
f.Int32P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["save"] = carapace.ActionFiles()
|
||||
|
||||
@@ -55,7 +55,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
timeout, _ := cmd.Flags().GetInt32("timeout")
|
||||
timeout, _ := cmd.Flags().GetInt64("timeout")
|
||||
|
||||
if timeout < 1 {
|
||||
con.PrintErrorf("Invalid timeout argument\n")
|
||||
@@ -68,7 +68,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
dump, err := con.Rpc.ProcessDump(context.Background(), &sliverpb.ProcessDumpReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Pid: int32(pid),
|
||||
Timeout: timeout,
|
||||
Timeout: int32(timeout),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -94,16 +95,24 @@ func ReconfigCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if activeC2 != "" {
|
||||
currentURI, err := url.Parse(activeC2)
|
||||
if err == nil {
|
||||
// must be the same protocol
|
||||
if newURI.Scheme != currentURI.Scheme {
|
||||
con.PrintErrorf("Cannot switch protocol from %s to %s (protocol must match compiled implant)\n",
|
||||
confirm := false
|
||||
con.PrintWarnf("Switching protocol from %s to %s. This will only work if the protocol was included in the compiled implant. If it was NOT included, YOU WILL LOSE THE BEACON CONNECTION PERMANENTLY.\n",
|
||||
currentURI.Scheme, newURI.Scheme)
|
||||
return
|
||||
_ = forms.Confirm("Do you want to continue?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
}
|
||||
// for now only support the same Host/C2 Server (becuase the implant crypto keys problem)
|
||||
// Warning: Switching to a different host typically fails because the implant and server must share the same crypto keys.
|
||||
if newURI.Hostname() != currentURI.Hostname() {
|
||||
con.PrintWarnf("Switching to a different host (%s -> %s). This only works if both servers share the same crypto keys.\n",
|
||||
currentURI.Hostname(), newURI.Hostname())
|
||||
confirm := false
|
||||
_ = forms.Confirm("Do you want to continue?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
|
||||
}
|
||||
flags.Bind("registry", true, registryCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
registryReadCmd := &cobra.Command{
|
||||
|
||||
@@ -39,6 +39,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.Bind("", false, rportfwdAddCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("remote", "r", "", "remote address <ip>:<port> connection is forwarded to")
|
||||
f.StringP("bind", "b", "", "bind address <ip>:<port> for implants to listen on")
|
||||
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
|
||||
})
|
||||
flags.BindFlagCompletions(rportfwdAddCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["remote"] = completers.ClientInterfacesCompleter()
|
||||
|
||||
@@ -53,10 +53,13 @@ func StartRportFwdListenerCmd(cmd *cobra.Command, con *console.SliverClient, arg
|
||||
if portNumberOnlyRegexp.MatchString(forwardAddress) {
|
||||
forwardAddress = fmt.Sprintf("127.0.0.1:%s", forwardAddress)
|
||||
}
|
||||
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
|
||||
|
||||
rportfwdListener, err := con.Rpc.StartRportFwdListener(context.Background(), &sliverpb.RportFwdStartListenerReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
BindAddress: bindAddress,
|
||||
ForwardAddress: forwardAddress,
|
||||
KeepAlive: keepAlive,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintWarnf("%s\n", err)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/ai"
|
||||
"github.com/bishopfox/sliver/client/command/aka"
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/armory"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/clean"
|
||||
"github.com/bishopfox/sliver/client/command/crack"
|
||||
"github.com/bishopfox/sliver/client/command/creds"
|
||||
docscmd "github.com/bishopfox/sliver/client/command/docs"
|
||||
"github.com/bishopfox/sliver/client/command/exit"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/command/generate"
|
||||
@@ -98,6 +100,8 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
|
||||
// Core
|
||||
bind(consts.GenericHelpGroup,
|
||||
exit.Command,
|
||||
ai.ServerCommands,
|
||||
docscmd.ServerCommands,
|
||||
serverctx.Commands,
|
||||
licenses.Commands,
|
||||
settings.Commands,
|
||||
|
||||
@@ -13,15 +13,15 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/huh/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
clienttheme "github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
@@ -158,7 +158,7 @@ func serverSwitch(_ *cobra.Command, con *console.SliverClient) {
|
||||
}
|
||||
|
||||
if err := con.SetConnection(rpc, conn, &console.ConnectionDetails{ConfigKey: inst.ConfigKey, Config: inst.Config}); err != nil {
|
||||
_ = conn.Close()
|
||||
_ = transport.CloseGRPCConnection(conn)
|
||||
con.PrintErrorf("Switch failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func serverSwitch(_ *cobra.Command, con *console.SliverClient) {
|
||||
}
|
||||
|
||||
if err := con.SetConnection(rpc, conn, &console.ConnectionDetails{ConfigKey: result.ConfigKey, Config: cfg}); err != nil {
|
||||
_ = conn.Close()
|
||||
_ = transport.CloseGRPCConnection(conn)
|
||||
con.PrintErrorf("Switch failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
@@ -347,7 +347,6 @@ type switchTUIModel struct {
|
||||
result switchResult
|
||||
width int
|
||||
height int
|
||||
theme *huh.Theme
|
||||
styles switchStyles
|
||||
activeID string
|
||||
counts map[string]int
|
||||
@@ -367,7 +366,7 @@ type switchStyles struct {
|
||||
|
||||
func runSwitchTUI(instances []*consoleInstance, activeID string, configKeys []string, counts map[string]int) (switchResult, error) {
|
||||
m := newSwitchTUIModel(instances, activeID, configKeys, counts)
|
||||
prog := tea.NewProgram(m, tea.WithAltScreen())
|
||||
prog := tea.NewProgram(m)
|
||||
finalModel, err := prog.Run()
|
||||
if err != nil {
|
||||
return switchResult{}, err
|
||||
@@ -465,7 +464,6 @@ func newSwitchTUIModel(instances []*consoleInstance, activeID string, configKeys
|
||||
tab: tabExisting,
|
||||
existing: existingForm,
|
||||
newConn: newForm,
|
||||
theme: huhTheme,
|
||||
styles: styles,
|
||||
activeID: activeID,
|
||||
counts: counts,
|
||||
@@ -491,7 +489,7 @@ func (m switchTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.newConn = modelB.(*huh.Form)
|
||||
return m, tea.Batch(cmdA, cmdB)
|
||||
|
||||
case tea.KeyMsg:
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "tab":
|
||||
if m.tab == tabExisting {
|
||||
@@ -548,7 +546,7 @@ func (m switchTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m switchTUIModel) View() string {
|
||||
func (m switchTUIModel) View() tea.View {
|
||||
header := m.renderHeader()
|
||||
|
||||
body := ""
|
||||
@@ -562,7 +560,9 @@ func (m switchTUIModel) View() string {
|
||||
footer := m.renderFooter()
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left, header, body, footer)
|
||||
return m.styles.frame.Render(content)
|
||||
view := tea.NewView(m.styles.frame.Render(content))
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
|
||||
func (m switchTUIModel) renderHeader() string {
|
||||
|
||||
@@ -27,7 +27,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
GroupID: consts.SliverHelpGroup,
|
||||
}
|
||||
flags.Bind("sessions", true, sessionsCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("sessions", false, sessionsCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("interact", "i", "", "interact with a session")
|
||||
|
||||
@@ -22,16 +22,19 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/ai"
|
||||
"github.com/bishopfox/sliver/client/command/aka"
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/backdoor"
|
||||
"github.com/bishopfox/sliver/client/command/cursed"
|
||||
"github.com/bishopfox/sliver/client/command/dllhijack"
|
||||
docscmd "github.com/bishopfox/sliver/client/command/docs"
|
||||
"github.com/bishopfox/sliver/client/command/edit"
|
||||
"github.com/bishopfox/sliver/client/command/environment"
|
||||
"github.com/bishopfox/sliver/client/command/exec"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/command/filesystem"
|
||||
"github.com/bishopfox/sliver/client/command/hexedit"
|
||||
"github.com/bishopfox/sliver/client/command/info"
|
||||
"github.com/bishopfox/sliver/client/command/kill"
|
||||
"github.com/bishopfox/sliver/client/command/network"
|
||||
@@ -49,7 +52,6 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/tasks"
|
||||
"github.com/bishopfox/sliver/client/command/wasm"
|
||||
"github.com/bishopfox/sliver/client/command/wireguard"
|
||||
"github.com/bishopfox/sliver/client/command/hexedit"
|
||||
client "github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/reeflective/console"
|
||||
@@ -78,6 +80,8 @@ func SliverCommands(con *client.SliverClient) console.Commands {
|
||||
|
||||
// [ Core ]
|
||||
bind(consts.SliverCoreHelpGroup,
|
||||
ai.Commands,
|
||||
docscmd.Commands,
|
||||
reconfig.Commands,
|
||||
// sessions.Commands,
|
||||
sessions.SliverCommands,
|
||||
|
||||
@@ -23,7 +23,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
Annotations: flags.RestrictTargets(consts.BeaconCmdsFilter),
|
||||
}
|
||||
flags.Bind("tasks", true, tasksCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.BoolP("overflow", "O", false, "overflow terminal width (display truncated rows)")
|
||||
f.IntP("skip-pages", "S", 0, "skip the first n page(s)")
|
||||
f.StringP("filter", "f", "", "filter based on task type (case-insensitive prefix matching)")
|
||||
|
||||
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("save", "s", "", "save downloaded files to specific directory (default downloads dir)")
|
||||
f.BoolP("insecure", "I", false, "skip tls certificate validation")
|
||||
f.Bool("force", false, "skip version comparison and download the latest release")
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["proxy"] = completers.LocalProxyCompleter()
|
||||
|
||||
@@ -60,7 +60,7 @@ const (
|
||||
func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
VerboseVersionsCmd(cmd, con, args)
|
||||
|
||||
timeoutF, _ := cmd.Flags().GetInt("timeout")
|
||||
timeoutF, _ := cmd.Flags().GetInt64("timeout")
|
||||
timeout := time.Duration(timeoutF) * time.Second
|
||||
|
||||
insecure, _ := cmd.Flags().GetBool("insecure")
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
@@ -123,7 +124,7 @@ func (con *SliverClient) detachConnectionLocked() {
|
||||
}
|
||||
|
||||
if con.grpcConn != nil {
|
||||
_ = con.grpcConn.Close()
|
||||
_ = transport.CloseGRPCConnection(con.grpcConn)
|
||||
con.grpcConn = nil
|
||||
}
|
||||
con.Rpc = nil
|
||||
|
||||
@@ -86,6 +86,11 @@ type SliverClient struct {
|
||||
stdoutPipeWriter *os.File
|
||||
stdoutPipeDone chan struct{}
|
||||
stdoutPipeOnce sync.Once
|
||||
stdoutSyncMu sync.Mutex
|
||||
stdoutSyncSeq uint64
|
||||
stdoutSyncMarker []byte
|
||||
stdoutSyncAcks map[uint64]chan struct{}
|
||||
stdoutSyncAcksMu sync.Mutex
|
||||
|
||||
connMu sync.Mutex
|
||||
grpcConn *grpc.ClientConn
|
||||
@@ -317,6 +322,8 @@ func (con *SliverClient) applyConnectionHooksOnce() {
|
||||
}
|
||||
con.connectionHooksApplied = true
|
||||
|
||||
con.App.PreReadlineHooks = append(con.App.PreReadlineHooks, con.syncOutputHook)
|
||||
con.App.PostCmdRunHooks = append(con.App.PostCmdRunHooks, con.syncOutputHook)
|
||||
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.allowServerRootCommands)
|
||||
if shell := con.App.Shell(); shell != nil && shell.Completer != nil {
|
||||
baseCompleter := shell.Completer
|
||||
|
||||
+156
-4
@@ -19,7 +19,10 @@ package console
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -38,6 +41,11 @@ import (
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
stdoutSyncMarkerSize = 16
|
||||
stdoutSyncWait = 2 * time.Second
|
||||
)
|
||||
|
||||
// ConsoleClientLogger is an io.Writer that sends data to the server.
|
||||
type ConsoleClientLogger struct {
|
||||
name string
|
||||
@@ -233,14 +241,158 @@ func (con *SliverClient) setupAsciicastRecord(logFile *os.File, server io.Writer
|
||||
os.Stdout = w
|
||||
os.Stderr = w
|
||||
|
||||
marker := make([]byte, stdoutSyncMarkerSize)
|
||||
if _, err := rand.Read(marker); err != nil {
|
||||
copy(marker, []byte(fmt.Sprintf("sliver-sync-%d", time.Now().UnixNano())))
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
con.stdoutPipeWriter = w
|
||||
con.stdoutPipeDone = done
|
||||
con.stdoutSyncMarker = marker
|
||||
con.stdoutSyncAcks = map[uint64]chan struct{}{}
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(mw, r)
|
||||
close(done)
|
||||
}()
|
||||
go con.copyStdoutPipe(r, mw, done)
|
||||
}
|
||||
|
||||
func buildStdoutSyncFrame(marker []byte, seq uint64) []byte {
|
||||
frame := make([]byte, len(marker)*2+8)
|
||||
copy(frame, marker)
|
||||
binary.BigEndian.PutUint64(frame[len(marker):], seq)
|
||||
copy(frame[len(marker)+8:], marker)
|
||||
return frame
|
||||
}
|
||||
|
||||
func pendingMarkerPrefixLen(pending []byte, marker []byte) int {
|
||||
max := len(marker) - 1
|
||||
if max > len(pending) {
|
||||
max = len(pending)
|
||||
}
|
||||
for size := max; size > 0; size-- {
|
||||
if bytes.Equal(pending[len(pending)-size:], marker[:size]) {
|
||||
return size
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func drainStdoutPipeBuffer(dst io.Writer, pending []byte, marker []byte, ack func(uint64)) []byte {
|
||||
if len(pending) == 0 {
|
||||
return pending
|
||||
}
|
||||
if len(marker) == 0 {
|
||||
_, _ = dst.Write(pending)
|
||||
return pending[:0]
|
||||
}
|
||||
|
||||
frameLen := len(marker)*2 + 8
|
||||
for {
|
||||
idx := bytes.Index(pending, marker)
|
||||
if idx == -1 {
|
||||
keep := pendingMarkerPrefixLen(pending, marker)
|
||||
flush := len(pending) - keep
|
||||
if flush > 0 {
|
||||
_, _ = dst.Write(pending[:flush])
|
||||
pending = pending[flush:]
|
||||
}
|
||||
return pending
|
||||
}
|
||||
|
||||
if idx > 0 {
|
||||
_, _ = dst.Write(pending[:idx])
|
||||
pending = pending[idx:]
|
||||
}
|
||||
if len(pending) < frameLen {
|
||||
return pending
|
||||
}
|
||||
if bytes.Equal(pending[len(marker)+8:frameLen], marker) {
|
||||
if ack != nil {
|
||||
ack(binary.BigEndian.Uint64(pending[len(marker) : len(marker)+8]))
|
||||
}
|
||||
pending = pending[frameLen:]
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = dst.Write(pending[:1])
|
||||
pending = pending[1:]
|
||||
}
|
||||
}
|
||||
|
||||
func (con *SliverClient) copyStdoutPipe(src *os.File, dst io.Writer, done chan struct{}) {
|
||||
defer close(done)
|
||||
defer src.Close()
|
||||
|
||||
readBuf := make([]byte, 4096)
|
||||
pending := make([]byte, 0, 4096)
|
||||
for {
|
||||
n, err := src.Read(readBuf)
|
||||
if n > 0 {
|
||||
pending = append(pending, readBuf[:n]...)
|
||||
pending = drainStdoutPipeBuffer(dst, pending, con.stdoutSyncMarker, con.ackStdoutSync)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(pending) > 0 {
|
||||
_, _ = dst.Write(pending)
|
||||
}
|
||||
}
|
||||
|
||||
func (con *SliverClient) ackStdoutSync(seq uint64) {
|
||||
con.stdoutSyncAcksMu.Lock()
|
||||
ack := con.stdoutSyncAcks[seq]
|
||||
delete(con.stdoutSyncAcks, seq)
|
||||
con.stdoutSyncAcksMu.Unlock()
|
||||
|
||||
if ack != nil {
|
||||
close(ack)
|
||||
}
|
||||
}
|
||||
|
||||
func (con *SliverClient) clearStdoutSync(seq uint64) {
|
||||
con.stdoutSyncAcksMu.Lock()
|
||||
delete(con.stdoutSyncAcks, seq)
|
||||
con.stdoutSyncAcksMu.Unlock()
|
||||
}
|
||||
|
||||
func (con *SliverClient) syncOutputHook() error {
|
||||
con.syncOutput()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (con *SliverClient) syncOutput() {
|
||||
if con.stdoutPipeWriter == nil {
|
||||
_ = os.Stdout.Sync()
|
||||
return
|
||||
}
|
||||
if len(con.stdoutSyncMarker) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
con.stdoutSyncMu.Lock()
|
||||
con.stdoutSyncSeq++
|
||||
seq := con.stdoutSyncSeq
|
||||
ack := make(chan struct{})
|
||||
con.stdoutSyncAcksMu.Lock()
|
||||
if con.stdoutSyncAcks == nil {
|
||||
con.stdoutSyncAcks = map[uint64]chan struct{}{}
|
||||
}
|
||||
con.stdoutSyncAcks[seq] = ack
|
||||
con.stdoutSyncAcksMu.Unlock()
|
||||
frame := buildStdoutSyncFrame(con.stdoutSyncMarker, seq)
|
||||
_, err := con.stdoutPipeWriter.Write(frame)
|
||||
con.stdoutSyncMu.Unlock()
|
||||
if err != nil {
|
||||
con.clearStdoutSync(seq)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ack:
|
||||
case <-time.After(stdoutSyncWait):
|
||||
con.clearStdoutSync(seq)
|
||||
}
|
||||
}
|
||||
|
||||
func getConsoleLogFile() *os.File {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDrainStdoutPipeBufferStripsSyncFrames(t *testing.T) {
|
||||
marker := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
frame := buildStdoutSyncFrame(marker, 7)
|
||||
|
||||
var got bytes.Buffer
|
||||
var acked []uint64
|
||||
pending := drainStdoutPipeBuffer(&got, append(append([]byte("before"), frame...), []byte("after")...), marker, func(seq uint64) {
|
||||
acked = append(acked, seq)
|
||||
})
|
||||
if len(pending) > 0 {
|
||||
_, _ = got.Write(pending)
|
||||
}
|
||||
|
||||
if got.String() != "beforeafter" {
|
||||
t.Fatalf("unexpected output %q", got.String())
|
||||
}
|
||||
if len(acked) != 1 || acked[0] != 7 {
|
||||
t.Fatalf("unexpected sync acknowledgements: %v", acked)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrainStdoutPipeBufferHandlesSplitFrame(t *testing.T) {
|
||||
marker := []byte{0xaa, 0xbb, 0xcc, 0xdd}
|
||||
frame := buildStdoutSyncFrame(marker, 11)
|
||||
|
||||
var got bytes.Buffer
|
||||
var acked []uint64
|
||||
|
||||
chunk1 := append([]byte("left"), frame[:5]...)
|
||||
pending := drainStdoutPipeBuffer(&got, chunk1, marker, func(seq uint64) {
|
||||
acked = append(acked, seq)
|
||||
})
|
||||
if got.String() != "left" {
|
||||
t.Fatalf("expected prefix output to flush, got %q", got.String())
|
||||
}
|
||||
|
||||
chunk2 := append(append([]byte{}, pending...), frame[5:]...)
|
||||
chunk2 = append(chunk2, []byte("right")...)
|
||||
pending = drainStdoutPipeBuffer(&got, chunk2, marker, func(seq uint64) {
|
||||
acked = append(acked, seq)
|
||||
})
|
||||
if len(pending) > 0 {
|
||||
_, _ = got.Write(pending)
|
||||
}
|
||||
|
||||
if got.String() != "leftright" {
|
||||
t.Fatalf("unexpected split-frame output %q", got.String())
|
||||
}
|
||||
if len(acked) != 1 || acked[0] != 11 {
|
||||
t.Fatalf("unexpected split-frame acknowledgements: %v", acked)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncOutputWaitsForPipeDrain(t *testing.T) {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("pipe: %v", err)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
con := &SliverClient{
|
||||
stdoutPipeWriter: w,
|
||||
stdoutPipeDone: done,
|
||||
stdoutSyncMarker: []byte("0123456789abcdef"),
|
||||
stdoutSyncAcks: map[uint64]chan struct{}{},
|
||||
}
|
||||
|
||||
var got bytes.Buffer
|
||||
go con.copyStdoutPipe(r, &got, done)
|
||||
|
||||
if _, err := w.Write([]byte("hello")); err != nil {
|
||||
t.Fatalf("write prefix: %v", err)
|
||||
}
|
||||
con.syncOutput()
|
||||
if got.String() != "hello" {
|
||||
t.Fatalf("syncOutput returned before draining stdout: %q", got.String())
|
||||
}
|
||||
|
||||
if _, err := w.Write([]byte(" world")); err != nil {
|
||||
t.Fatalf("write suffix: %v", err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatalf("close writer: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for stdout copier to finish")
|
||||
}
|
||||
|
||||
if got.String() != "hello world" {
|
||||
t.Fatalf("unexpected final output %q", got.String())
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,12 @@ const (
|
||||
CrackStatusEvent = "crack-status"
|
||||
|
||||
// WireGuardNewPeer - New Wireguard peer added.
|
||||
WireGuardNewPeer = "wireguard-newpeer"
|
||||
WireGuardNewPeer = "wireguard-newpeer"
|
||||
MultiplayerWireGuardNewPeer = "multiplayer-wireguard-newpeer"
|
||||
MultiplayerWireGuardRemoved = "multiplayer-wireguard-removed"
|
||||
|
||||
// AIConversationEvent - Fires whenever an AI conversation or message changes.
|
||||
AIConversationEvent = "ai-conversation"
|
||||
)
|
||||
|
||||
// Commands.
|
||||
@@ -220,6 +225,9 @@ const (
|
||||
MkdirStr = "mkdir"
|
||||
CdStr = "cd"
|
||||
PwdStr = "pwd"
|
||||
AIStr = "ai"
|
||||
AIConfigStr = "ai-config"
|
||||
DocsStr = "docs"
|
||||
CatStr = "cat"
|
||||
EditStr = "edit"
|
||||
HexEditStr = "hex-edit"
|
||||
|
||||
@@ -216,6 +216,7 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
|
||||
Port: p.Port(),
|
||||
Protocol: sliverpb.PortFwdProtoTCP,
|
||||
TunnelID: tunnel.ID,
|
||||
KeepAlive: int32(p.KeepAlivePeriod.Seconds()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -3,8 +3,8 @@ package forms
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// ArmoryInstallOption represents an armory install option.
|
||||
@@ -46,7 +46,7 @@ func ArmoryInstallForm(options []ArmoryInstallOption) (*ArmoryInstallFormResult,
|
||||
Value(&result.SelectedNames)
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
if err := runForm(form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package forms
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// ArmoryUpdateOption represents an update option in the armory update form.
|
||||
@@ -45,7 +45,7 @@ func ArmoryUpdateForm(options []ArmoryUpdateOption) (*ArmoryUpdateFormResult, er
|
||||
Value(&result.SelectedIDs)
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
if err := runForm(form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// ErrUserAborted exposes the huh user-abort sentinel for callers.
|
||||
@@ -126,7 +126,7 @@ func GenerateForm(compiler *clientpb.Compiler) (*GenerateFormResult, error) {
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
if err := runForm(form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// GenerateBeaconFormResult captures the inputs needed to drive the generate beacon command.
|
||||
@@ -156,7 +156,7 @@ func GenerateBeaconForm(compiler *clientpb.Compiler) (*GenerateBeaconFormResult,
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
if err := runForm(form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// GenerateProfilesNewFormResult captures the inputs needed to drive the profiles new command.
|
||||
@@ -136,7 +136,7 @@ func GenerateProfilesNewForm() (*GenerateProfilesNewFormResult, error) {
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
if err := runForm(form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// GenerateProfilesNewBeaconFormResult captures the inputs needed to drive the profiles new beacon command.
|
||||
@@ -168,7 +168,7 @@ func GenerateProfilesNewBeaconForm() (*GenerateProfilesNewBeaconFormResult, erro
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
if err := runForm(form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package forms
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
type formatOption struct {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/huh"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ func Confirm(title string, value *bool) error {
|
||||
),
|
||||
).WithTheme(theme.HuhTheme())
|
||||
|
||||
return form.Run()
|
||||
return runForm(form)
|
||||
}
|
||||
|
||||
// Input prompts for a single-line string.
|
||||
@@ -52,7 +52,7 @@ func Input(title string, value *string) error {
|
||||
),
|
||||
).WithTheme(theme.HuhTheme())
|
||||
|
||||
return form.Run()
|
||||
return runForm(form)
|
||||
}
|
||||
|
||||
// Text prompts for multi-line input.
|
||||
@@ -70,7 +70,7 @@ func Text(title string, value *string) error {
|
||||
),
|
||||
).WithTheme(theme.HuhTheme())
|
||||
|
||||
return form.Run()
|
||||
return runForm(form)
|
||||
}
|
||||
|
||||
// Select prompts for a single selection from options.
|
||||
@@ -102,7 +102,7 @@ func MultiSelect(title string, options []string, value *[]string) error {
|
||||
form := huh.NewForm(huh.NewGroup(field)).
|
||||
WithTheme(theme.HuhTheme()).
|
||||
WithWidth(getTerminalWidth())
|
||||
return form.Run()
|
||||
return runForm(form)
|
||||
}
|
||||
|
||||
func selectPrompt(title string, options []string, value *string, required bool) error {
|
||||
@@ -136,7 +136,7 @@ func selectPrompt(title string, options []string, value *string, required bool)
|
||||
form := huh.NewForm(huh.NewGroup(field)).
|
||||
WithTheme(theme.HuhTheme()).
|
||||
WithWidth(getTerminalWidth())
|
||||
err := form.Run()
|
||||
err := runForm(form)
|
||||
|
||||
// On error restore the originalValue and return err
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/client/termio"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var openFormTTY = tea.OpenTTY
|
||||
|
||||
func runForm(form *huh.Form) error {
|
||||
cleanup := configureFormTTY(form)
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
// Console logging swaps stdout/stderr to pipes so the session can be tee'd to
|
||||
// disk. Bubble Tea forms need a real TTY output, so temporarily bind them back
|
||||
// to the controlling terminal when the interactive console has piped stdio.
|
||||
func configureFormTTY(form *huh.Form) func() {
|
||||
if form == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stdinTTY := isTTY(os.Stdin)
|
||||
stdoutTTY := isTTY(os.Stdout)
|
||||
stderrTTY := isTTY(os.Stderr)
|
||||
|
||||
switch {
|
||||
case shouldUseStdoutTTYForForm(stdinTTY, stdoutTTY, stderrTTY):
|
||||
form.WithInput(os.Stdin)
|
||||
form.WithOutput(os.Stdout)
|
||||
return nil
|
||||
|
||||
case needsDedicatedTTYForForm(stdinTTY, stdoutTTY, stderrTTY):
|
||||
if isTTY(termio.InteractiveInput()) && isTTY(termio.InteractiveOutput()) {
|
||||
form.WithInput(termio.InteractiveInput())
|
||||
form.WithOutput(termio.InteractiveOutput())
|
||||
return nil
|
||||
}
|
||||
|
||||
inTTY, outTTY, err := openFormTTY()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
form.WithInput(inTTY)
|
||||
form.WithOutput(outTTY)
|
||||
return func() {
|
||||
_ = inTTY.Close()
|
||||
if outTTY != inTTY {
|
||||
_ = outTTY.Close()
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func isTTY(file *os.File) bool {
|
||||
return file != nil && term.IsTerminal(int(file.Fd()))
|
||||
}
|
||||
|
||||
func shouldUseStdoutTTYForForm(stdinTTY, stdoutTTY, stderrTTY bool) bool {
|
||||
return stdinTTY && stdoutTTY && !stderrTTY
|
||||
}
|
||||
|
||||
func needsDedicatedTTYForForm(stdinTTY, stdoutTTY, stderrTTY bool) bool {
|
||||
return stdinTTY && !stdoutTTY && !stderrTTY
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package forms
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestShouldUseStdoutTTYForForm(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stdinTTY bool
|
||||
stdoutTTY bool
|
||||
stderrTTY bool
|
||||
want bool
|
||||
}{
|
||||
{name: "stderr redirected but stdout interactive", stdinTTY: true, stdoutTTY: true, stderrTTY: false, want: true},
|
||||
{name: "default stderr already interactive", stdinTTY: true, stdoutTTY: true, stderrTTY: true, want: false},
|
||||
{name: "non interactive stdin", stdinTTY: false, stdoutTTY: true, stderrTTY: false, want: false},
|
||||
{name: "all stdio piped", stdinTTY: true, stdoutTTY: false, stderrTTY: false, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := shouldUseStdoutTTYForForm(tt.stdinTTY, tt.stdoutTTY, tt.stderrTTY); got != tt.want {
|
||||
t.Fatalf("shouldUseStdoutTTYForForm(%v, %v, %v) = %v, want %v", tt.stdinTTY, tt.stdoutTTY, tt.stderrTTY, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedsDedicatedTTYForForm(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stdinTTY bool
|
||||
stdoutTTY bool
|
||||
stderrTTY bool
|
||||
want bool
|
||||
}{
|
||||
{name: "interactive stdin with piped stdout and stderr", stdinTTY: true, stdoutTTY: false, stderrTTY: false, want: true},
|
||||
{name: "stdout can be reused", stdinTTY: true, stdoutTTY: true, stderrTTY: false, want: false},
|
||||
{name: "stderr already interactive", stdinTTY: true, stdoutTTY: false, stderrTTY: true, want: false},
|
||||
{name: "non interactive stdin", stdinTTY: false, stdoutTTY: false, stderrTTY: false, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := needsDedicatedTTYForForm(tt.stdinTTY, tt.stdoutTTY, tt.stderrTTY); got != tt.want {
|
||||
t.Fatalf("needsDedicatedTTYForForm(%v, %v, %v) = %v, want %v", tt.stdinTTY, tt.stdoutTTY, tt.stderrTTY, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// SettingsFormResult captures the inputs needed to update client settings.
|
||||
@@ -108,7 +108,7 @@ func SettingsForm(settings *assets.ClientSettings, tableStyleOptions []string) (
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
if err := runForm(form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
// Package thinking provides Crush's pending-request animation as a reusable
|
||||
// Bubble Tea component.
|
||||
package thinking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/exp/charmtone"
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
"github.com/zeebo/xxh3"
|
||||
)
|
||||
|
||||
const (
|
||||
fps = 20
|
||||
initialChar = '.'
|
||||
labelGap = " "
|
||||
labelGapWidth = 1
|
||||
|
||||
// If the FPS is 20 (50 milliseconds) this means that the ellipsis will
|
||||
// change every 8 frames (400 milliseconds).
|
||||
ellipsisAnimSpeed = 8
|
||||
|
||||
// The maximum amount of time that can pass before a character appears.
|
||||
// This is used to create a staggered entrance effect.
|
||||
maxBirthOffset = time.Second
|
||||
|
||||
// Number of frames to prerender for the animation. After this number of
|
||||
// frames, the animation will loop. This only applies when color cycling is
|
||||
// disabled.
|
||||
prerenderedFrames = 10
|
||||
|
||||
// Default number of cycling chars.
|
||||
defaultNumCyclingChars = 10
|
||||
)
|
||||
|
||||
var (
|
||||
availableRunes = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_")
|
||||
ellipsisFrames = []string{".", "..", "...", ""}
|
||||
)
|
||||
|
||||
// Internal ID management. Used during animating to ensure that frame messages
|
||||
// are received only by animation components that sent them.
|
||||
var lastID int64
|
||||
|
||||
func nextID() int {
|
||||
return int(atomic.AddInt64(&lastID, 1))
|
||||
}
|
||||
|
||||
type animCache struct {
|
||||
initialFrames [][]string
|
||||
cyclingFrames [][]string
|
||||
width int
|
||||
labelWidth int
|
||||
label []string
|
||||
ellipsisFrames []string
|
||||
}
|
||||
|
||||
var animCacheMap sync.Map
|
||||
|
||||
// Colors groups the animation's configurable colors.
|
||||
type Colors struct {
|
||||
GradientA color.Color
|
||||
GradientB color.Color
|
||||
Label color.Color
|
||||
}
|
||||
|
||||
// DefaultColors returns the same color palette Crush uses today for pending
|
||||
// and thinking animations.
|
||||
func DefaultColors() Colors {
|
||||
return Colors{
|
||||
GradientA: charmtone.Charple,
|
||||
GradientB: charmtone.Dolly,
|
||||
Label: charmtone.Ash,
|
||||
}
|
||||
}
|
||||
|
||||
func settingsHash(opts Settings) string {
|
||||
h := xxh3.New()
|
||||
fmt.Fprintf(h, "%d-%s-%v-%v-%v-%t",
|
||||
opts.Size, opts.Label, opts.LabelColor, opts.GradColorA, opts.GradColorB, opts.CycleColors)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
func withDefaultColors(opts Settings) Settings {
|
||||
defaults := DefaultColors()
|
||||
if colorIsUnset(opts.GradColorA) {
|
||||
opts.GradColorA = defaults.GradientA
|
||||
}
|
||||
if colorIsUnset(opts.GradColorB) {
|
||||
opts.GradColorB = defaults.GradientB
|
||||
}
|
||||
if colorIsUnset(opts.LabelColor) {
|
||||
opts.LabelColor = defaults.Label
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// StepMsg is a message type used to trigger the next step in the animation.
|
||||
type StepMsg struct{ ID string }
|
||||
|
||||
// Settings defines animation settings.
|
||||
type Settings struct {
|
||||
ID string
|
||||
Size int
|
||||
Label string
|
||||
LabelColor color.Color
|
||||
GradColorA color.Color
|
||||
GradColorB color.Color
|
||||
CycleColors bool
|
||||
SkipIntro bool
|
||||
}
|
||||
|
||||
// Anim is a Bubble Tea component for Crush's pending/thinking animation.
|
||||
type Anim struct {
|
||||
mu sync.RWMutex
|
||||
width int
|
||||
cyclingCharWidth int
|
||||
label []string
|
||||
labelWidth int
|
||||
labelColor color.Color
|
||||
startTime time.Time
|
||||
birthOffsets []time.Duration
|
||||
initialFrames [][]string
|
||||
initialized atomic.Bool
|
||||
cyclingFrames [][]string
|
||||
step atomic.Int64
|
||||
ellipsisStep atomic.Int64
|
||||
ellipsisFrames []string
|
||||
id string
|
||||
}
|
||||
|
||||
// New creates a new animation instance with the specified settings.
|
||||
func New(opts Settings) *Anim {
|
||||
a := &Anim{}
|
||||
|
||||
if opts.Size < 1 {
|
||||
opts.Size = defaultNumCyclingChars
|
||||
}
|
||||
opts = withDefaultColors(opts)
|
||||
|
||||
if opts.ID != "" {
|
||||
a.id = opts.ID
|
||||
} else {
|
||||
a.id = fmt.Sprintf("%d", nextID())
|
||||
}
|
||||
a.startTime = time.Now()
|
||||
a.cyclingCharWidth = opts.Size
|
||||
a.labelColor = opts.LabelColor
|
||||
|
||||
cacheKey := settingsHash(opts)
|
||||
if cached, ok := loadAnimCache(cacheKey); ok {
|
||||
a.width = cached.width
|
||||
a.labelWidth = cached.labelWidth
|
||||
a.label = slices.Clone(cached.label)
|
||||
a.ellipsisFrames = slices.Clone(cached.ellipsisFrames)
|
||||
a.initialFrames = cached.initialFrames
|
||||
a.cyclingFrames = cached.cyclingFrames
|
||||
} else {
|
||||
a.labelWidth = lipgloss.Width(opts.Label)
|
||||
a.width = opts.Size
|
||||
if opts.Label != "" {
|
||||
a.width += labelGapWidth + a.labelWidth
|
||||
}
|
||||
|
||||
a.renderLabel(opts.Label)
|
||||
|
||||
var ramp []color.Color
|
||||
numFrames := prerenderedFrames
|
||||
if opts.CycleColors {
|
||||
ramp = makeGradientRamp(a.width*3, opts.GradColorA, opts.GradColorB, opts.GradColorA, opts.GradColorB)
|
||||
numFrames = a.width * 2
|
||||
} else {
|
||||
ramp = makeGradientRamp(a.width, opts.GradColorA, opts.GradColorB)
|
||||
}
|
||||
|
||||
a.initialFrames = make([][]string, numFrames)
|
||||
offset := 0
|
||||
for i := range a.initialFrames {
|
||||
a.initialFrames[i] = make([]string, a.width+labelGapWidth+a.labelWidth)
|
||||
for j := range a.initialFrames[i] {
|
||||
if j+offset >= len(ramp) {
|
||||
continue
|
||||
}
|
||||
|
||||
c := opts.LabelColor
|
||||
if j <= a.cyclingCharWidth {
|
||||
c = ramp[j+offset]
|
||||
}
|
||||
|
||||
a.initialFrames[i][j] = lipgloss.NewStyle().
|
||||
Foreground(c).
|
||||
Render(string(initialChar))
|
||||
}
|
||||
if opts.CycleColors {
|
||||
offset++
|
||||
}
|
||||
}
|
||||
|
||||
a.cyclingFrames = make([][]string, numFrames)
|
||||
offset = 0
|
||||
for i := range a.cyclingFrames {
|
||||
a.cyclingFrames[i] = make([]string, a.width)
|
||||
for j := range a.cyclingFrames[i] {
|
||||
if j+offset >= len(ramp) {
|
||||
continue
|
||||
}
|
||||
|
||||
r := availableRunes[rand.IntN(len(availableRunes))]
|
||||
a.cyclingFrames[i][j] = lipgloss.NewStyle().
|
||||
Foreground(ramp[j+offset]).
|
||||
Render(string(r))
|
||||
}
|
||||
if opts.CycleColors {
|
||||
offset++
|
||||
}
|
||||
}
|
||||
|
||||
storeAnimCache(cacheKey, &animCache{
|
||||
initialFrames: a.initialFrames,
|
||||
cyclingFrames: a.cyclingFrames,
|
||||
width: a.width,
|
||||
labelWidth: a.labelWidth,
|
||||
label: slices.Clone(a.label),
|
||||
ellipsisFrames: slices.Clone(a.ellipsisFrames),
|
||||
})
|
||||
}
|
||||
|
||||
a.birthOffsets = make([]time.Duration, a.width)
|
||||
for i := range a.birthOffsets {
|
||||
a.birthOffsets[i] = time.Duration(rand.N(int64(maxBirthOffset))) * time.Nanosecond
|
||||
}
|
||||
if opts.SkipIntro {
|
||||
a.initialized.Store(true)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func loadAnimCache(key string) (*animCache, bool) {
|
||||
cached, ok := animCacheMap.Load(key)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
animCache, ok := cached.(*animCache)
|
||||
return animCache, ok
|
||||
}
|
||||
|
||||
func storeAnimCache(key string, cached *animCache) {
|
||||
animCacheMap.Store(key, cached)
|
||||
}
|
||||
|
||||
// SetLabel updates the label text and re-renders it.
|
||||
func (a *Anim) SetLabel(newLabel string) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.labelWidth = lipgloss.Width(newLabel)
|
||||
a.width = a.cyclingCharWidth
|
||||
if newLabel != "" {
|
||||
a.width += labelGapWidth + a.labelWidth
|
||||
}
|
||||
|
||||
a.renderLabel(newLabel)
|
||||
}
|
||||
|
||||
func (a *Anim) renderLabel(label string) {
|
||||
if a.labelWidth > 0 {
|
||||
labelRunes := []rune(label)
|
||||
a.label = make([]string, 0, len(labelRunes))
|
||||
for i := range labelRunes {
|
||||
rendered := lipgloss.NewStyle().
|
||||
Foreground(a.labelColor).
|
||||
Render(string(labelRunes[i]))
|
||||
a.label = append(a.label, rendered)
|
||||
}
|
||||
|
||||
a.ellipsisFrames = make([]string, 0, len(ellipsisFrames))
|
||||
for _, frame := range ellipsisFrames {
|
||||
rendered := lipgloss.NewStyle().
|
||||
Foreground(a.labelColor).
|
||||
Render(frame)
|
||||
a.ellipsisFrames = append(a.ellipsisFrames, rendered)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
a.label = nil
|
||||
a.ellipsisFrames = nil
|
||||
}
|
||||
|
||||
// Width returns the total width of the animation, including the animated
|
||||
// ellipsis when a label is present.
|
||||
func (a *Anim) Width() int {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
|
||||
w := a.cyclingCharWidth
|
||||
if a.labelWidth > 0 {
|
||||
w += labelGapWidth + a.labelWidth
|
||||
|
||||
var widestEllipsisFrame int
|
||||
for _, frame := range ellipsisFrames {
|
||||
widestEllipsisFrame = max(widestEllipsisFrame, lipgloss.Width(frame))
|
||||
}
|
||||
w += widestEllipsisFrame
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// Start starts the animation.
|
||||
func (a *Anim) Start() tea.Cmd {
|
||||
return a.Step()
|
||||
}
|
||||
|
||||
// Animate advances the animation to the next step.
|
||||
func (a *Anim) Animate(msg StepMsg) tea.Cmd {
|
||||
if msg.ID != a.id {
|
||||
return nil
|
||||
}
|
||||
|
||||
step := a.step.Add(1)
|
||||
if int(step) >= len(a.cyclingFrames) {
|
||||
a.step.Store(0)
|
||||
}
|
||||
|
||||
a.mu.RLock()
|
||||
labelWidth := a.labelWidth
|
||||
a.mu.RUnlock()
|
||||
|
||||
if a.initialized.Load() && labelWidth > 0 {
|
||||
ellipsisStep := a.ellipsisStep.Add(1)
|
||||
if int(ellipsisStep) >= ellipsisAnimSpeed*len(ellipsisFrames) {
|
||||
a.ellipsisStep.Store(0)
|
||||
}
|
||||
} else if !a.initialized.Load() && time.Since(a.startTime) >= maxBirthOffset {
|
||||
a.initialized.Store(true)
|
||||
}
|
||||
return a.Step()
|
||||
}
|
||||
|
||||
// Render renders the current state of the animation.
|
||||
func (a *Anim) Render() string {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
|
||||
var b strings.Builder
|
||||
step := int(a.step.Load())
|
||||
for i := range a.width {
|
||||
switch {
|
||||
case !a.initialized.Load() && i < len(a.birthOffsets) && time.Since(a.startTime) < a.birthOffsets[i]:
|
||||
b.WriteString(a.initialFrames[step][i])
|
||||
case i < a.cyclingCharWidth:
|
||||
b.WriteString(a.cyclingFrames[step][i])
|
||||
case i == a.cyclingCharWidth:
|
||||
b.WriteString(labelGap)
|
||||
case i > a.cyclingCharWidth:
|
||||
labelIdx := i - a.cyclingCharWidth - labelGapWidth
|
||||
if labelIdx >= 0 && labelIdx < len(a.label) {
|
||||
b.WriteString(a.label[labelIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if a.initialized.Load() && a.labelWidth > 0 {
|
||||
ellipsisStep := int(a.ellipsisStep.Load())
|
||||
frameIdx := ellipsisStep / ellipsisAnimSpeed
|
||||
if frameIdx >= 0 && frameIdx < len(a.ellipsisFrames) {
|
||||
b.WriteString(a.ellipsisFrames[frameIdx])
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Step is a command that triggers the next step in the animation.
|
||||
func (a *Anim) Step() tea.Cmd {
|
||||
return tea.Tick(time.Second/time.Duration(fps), func(t time.Time) tea.Msg {
|
||||
return StepMsg{ID: a.id}
|
||||
})
|
||||
}
|
||||
|
||||
// makeGradientRamp returns a slice of colors blended between the given keys.
|
||||
// Blending is done as Hcl to stay in gamut.
|
||||
func makeGradientRamp(size int, stops ...color.Color) []color.Color {
|
||||
if len(stops) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
points := make([]colorful.Color, len(stops))
|
||||
for i, stop := range stops {
|
||||
points[i], _ = colorful.MakeColor(stop)
|
||||
}
|
||||
|
||||
numSegments := len(stops) - 1
|
||||
if numSegments == 0 {
|
||||
return nil
|
||||
}
|
||||
blended := make([]color.Color, 0, size)
|
||||
|
||||
segmentSizes := make([]int, numSegments)
|
||||
baseSize := size / numSegments
|
||||
remainder := size % numSegments
|
||||
for i := range numSegments {
|
||||
segmentSizes[i] = baseSize
|
||||
if i < remainder {
|
||||
segmentSizes[i]++
|
||||
}
|
||||
}
|
||||
|
||||
for i := range numSegments {
|
||||
c1 := points[i]
|
||||
c2 := points[i+1]
|
||||
segmentSize := segmentSizes[i]
|
||||
|
||||
for j := range segmentSize {
|
||||
if segmentSize == 0 {
|
||||
continue
|
||||
}
|
||||
t := float64(j) / float64(segmentSize)
|
||||
c := c1.BlendHcl(c2, t)
|
||||
blended = append(blended, c)
|
||||
}
|
||||
}
|
||||
|
||||
return blended
|
||||
}
|
||||
|
||||
func colorIsUnset(c color.Color) bool {
|
||||
if c == nil {
|
||||
return true
|
||||
}
|
||||
_, _, _, a := c.RGBA()
|
||||
return a == 0
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package termio
|
||||
|
||||
import "os"
|
||||
|
||||
var (
|
||||
interactiveInput = os.Stdin
|
||||
interactiveOutput = os.Stdout
|
||||
)
|
||||
|
||||
// InteractiveInput returns the process's original stdin handle before any
|
||||
// console logging hooks swapped stdio to pipes.
|
||||
func InteractiveInput() *os.File {
|
||||
return interactiveInput
|
||||
}
|
||||
|
||||
// InteractiveOutput returns the process's original stdout handle before any
|
||||
// console logging hooks swapped stdio to pipes.
|
||||
func InteractiveOutput() *os.File {
|
||||
return interactiveOutput
|
||||
}
|
||||
+56
-52
@@ -1,84 +1,88 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"image/color"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
// HuhTheme returns a huh Theme derived from the current Sliver client theme.
|
||||
//
|
||||
// This is used for all interactive TUI forms in the client so that colors are
|
||||
// consistent with the rest of the console styling (theme.yaml).
|
||||
func HuhTheme() *huh.Theme {
|
||||
t := huh.ThemeBase()
|
||||
func HuhTheme() huh.Theme {
|
||||
return huh.ThemeFunc(func(isDark bool) *huh.Styles {
|
||||
t := huh.ThemeBase(isDark)
|
||||
|
||||
current := Current()
|
||||
border := paletteModV1(current.Default, 300)
|
||||
text := paletteModV1(current.Default, 900)
|
||||
dim := paletteModV1(current.Default, 500)
|
||||
faint := paletteModV1(current.Default, 400)
|
||||
current := Current()
|
||||
border := paletteMod(current.Default, 300)
|
||||
text := paletteMod(current.Default, 900)
|
||||
dim := paletteMod(current.Default, 500)
|
||||
faint := paletteMod(current.Default, 400)
|
||||
|
||||
primary := paletteColorV1(current.Primary)
|
||||
accent := paletteColorV1(current.Secondary)
|
||||
primary := paletteColor(current.Primary)
|
||||
accent := paletteColor(current.Secondary)
|
||||
|
||||
success := paletteColorV1(current.Success)
|
||||
danger := paletteColorV1(current.Danger)
|
||||
success := paletteColor(current.Success)
|
||||
danger := paletteColor(current.Danger)
|
||||
|
||||
// Focused styles.
|
||||
t.Focused.Base = t.Focused.Base.BorderForeground(border)
|
||||
t.Focused.Card = t.Focused.Base
|
||||
t.Focused.Title = t.Focused.Title.Foreground(primary).Bold(true)
|
||||
t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(primary).Bold(true).MarginBottom(1)
|
||||
t.Focused.Description = t.Focused.Description.Foreground(dim)
|
||||
t.Focused.Directory = t.Focused.Directory.Foreground(primary)
|
||||
t.Focused.File = t.Focused.File.Foreground(text)
|
||||
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(danger)
|
||||
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(danger)
|
||||
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(accent)
|
||||
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(accent)
|
||||
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(accent)
|
||||
t.Focused.Option = t.Focused.Option.Foreground(text)
|
||||
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(accent)
|
||||
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(success)
|
||||
t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(success)
|
||||
t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(text)
|
||||
t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(faint)
|
||||
// Focused styles.
|
||||
t.Focused.Base = t.Focused.Base.BorderForeground(border)
|
||||
t.Focused.Card = t.Focused.Base
|
||||
t.Focused.Title = t.Focused.Title.Foreground(primary).Bold(true)
|
||||
t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(primary).Bold(true).MarginBottom(1)
|
||||
t.Focused.Description = t.Focused.Description.Foreground(dim)
|
||||
t.Focused.Directory = t.Focused.Directory.Foreground(primary)
|
||||
t.Focused.File = t.Focused.File.Foreground(text)
|
||||
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(danger)
|
||||
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(danger)
|
||||
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(accent)
|
||||
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(accent)
|
||||
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(accent)
|
||||
t.Focused.Option = t.Focused.Option.Foreground(text)
|
||||
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(accent)
|
||||
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(success)
|
||||
t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(success)
|
||||
t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(text)
|
||||
t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(faint)
|
||||
|
||||
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(text).Background(primary)
|
||||
t.Focused.Next = t.Focused.FocusedButton
|
||||
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(paletteModV1(current.Default, 100))
|
||||
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(text).Background(primary)
|
||||
t.Focused.Next = t.Focused.FocusedButton
|
||||
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(paletteMod(current.Default, 100))
|
||||
|
||||
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primary)
|
||||
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(faint)
|
||||
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(accent)
|
||||
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primary)
|
||||
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(faint)
|
||||
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(accent)
|
||||
|
||||
// Blurred styles (same palette, but without the focused border/indicators).
|
||||
t.Blurred = t.Focused
|
||||
t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||
t.Blurred.Card = t.Blurred.Base
|
||||
t.Blurred.MultiSelectSelector = lipgloss.NewStyle().SetString(" ")
|
||||
t.Blurred.NextIndicator = lipgloss.NewStyle()
|
||||
t.Blurred.PrevIndicator = lipgloss.NewStyle()
|
||||
// Blurred styles (same palette, but without the focused border/indicators).
|
||||
t.Blurred = t.Focused
|
||||
t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||
t.Blurred.Card = t.Blurred.Base
|
||||
t.Blurred.MultiSelectSelector = lipgloss.NewStyle().SetString(" ")
|
||||
t.Blurred.NextIndicator = lipgloss.NewStyle()
|
||||
t.Blurred.PrevIndicator = lipgloss.NewStyle()
|
||||
|
||||
// Group styles.
|
||||
t.Group.Title = t.Focused.Title
|
||||
t.Group.Description = t.Focused.Description
|
||||
// Group styles.
|
||||
t.Group.Title = t.Focused.Title
|
||||
t.Group.Description = t.Focused.Description
|
||||
|
||||
return t
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
func paletteColorV1(p Palette) lipgloss.TerminalColor {
|
||||
func paletteColor(p Palette) color.Color {
|
||||
if p.Default != "" {
|
||||
return lipgloss.Color(p.Default)
|
||||
}
|
||||
return lipgloss.Color("#ffffff")
|
||||
}
|
||||
|
||||
func paletteModV1(p Palette, mod int) lipgloss.TerminalColor {
|
||||
func paletteMod(p Palette, mod int) color.Color {
|
||||
if mod != 0 {
|
||||
if v, ok := p.Mods[mod]; ok && v != "" {
|
||||
return lipgloss.Color(v)
|
||||
}
|
||||
}
|
||||
return paletteColorV1(p)
|
||||
return paletteColor(p)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// MultiplayerConnectMode controls how the client reaches multiplayer.
|
||||
type MultiplayerConnectMode int
|
||||
|
||||
const (
|
||||
MultiplayerConnectAuto MultiplayerConnectMode = iota
|
||||
MultiplayerConnectDisableWG
|
||||
MultiplayerConnectRequireWG
|
||||
)
|
||||
|
||||
type connectionCloser interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
var (
|
||||
multiplayerConnectModeMu sync.RWMutex
|
||||
multiplayerConnectMode = MultiplayerConnectAuto
|
||||
connClosers sync.Map // *grpc.ClientConn -> connectionCloser
|
||||
)
|
||||
|
||||
// SetMultiplayerConnectMode configures future multiplayer client dials.
|
||||
func SetMultiplayerConnectMode(mode MultiplayerConnectMode) {
|
||||
multiplayerConnectModeMu.Lock()
|
||||
multiplayerConnectMode = mode
|
||||
multiplayerConnectModeMu.Unlock()
|
||||
}
|
||||
|
||||
func getMultiplayerConnectMode() MultiplayerConnectMode {
|
||||
multiplayerConnectModeMu.RLock()
|
||||
mode := multiplayerConnectMode
|
||||
multiplayerConnectModeMu.RUnlock()
|
||||
return mode
|
||||
}
|
||||
|
||||
func registerConnCloser(conn *grpc.ClientConn, closer connectionCloser) {
|
||||
if conn == nil || closer == nil {
|
||||
return
|
||||
}
|
||||
connClosers.Store(conn, closer)
|
||||
}
|
||||
|
||||
func unregisterConnCloser(conn *grpc.ClientConn) connectionCloser {
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
if closer, ok := connClosers.LoadAndDelete(conn); ok {
|
||||
return closer.(connectionCloser)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseGRPCConnection closes a client connection and any transport-specific
|
||||
// resources attached to it, such as the multiplayer WireGuard tunnel.
|
||||
func CloseGRPCConnection(conn *grpc.ClientConn) error {
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
if closer := unregisterConnCloser(conn); closer != nil {
|
||||
errs = append(errs, closer.Close())
|
||||
}
|
||||
errs = append(errs, conn.Close())
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -50,6 +51,13 @@ type TokenAuth struct {
|
||||
token string
|
||||
}
|
||||
|
||||
type multiplayerDialStrategy int
|
||||
|
||||
const (
|
||||
multiplayerDialDirect multiplayerDialStrategy = iota
|
||||
multiplayerDialWireGuard
|
||||
)
|
||||
|
||||
// Return value is mapped to request headers.
|
||||
func (t TokenAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
@@ -63,10 +71,56 @@ func (TokenAuth) RequireTransportSecurity() bool {
|
||||
|
||||
// MTLSConnect - Connect to the sliver server
|
||||
func MTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
tlsConfig, err := getTLSConfig(config.CACertificate, config.Certificate, config.PrivateKey)
|
||||
strategy, err := selectMultiplayerDialStrategy(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch strategy {
|
||||
case multiplayerDialWireGuard:
|
||||
return wireGuardMTLSConnect(config)
|
||||
default:
|
||||
return directMTLSConnect(config)
|
||||
}
|
||||
}
|
||||
|
||||
func selectMultiplayerDialStrategy(config *assets.ClientConfig) (multiplayerDialStrategy, error) {
|
||||
if config == nil {
|
||||
return multiplayerDialDirect, errors.New("client config is required")
|
||||
}
|
||||
|
||||
switch getMultiplayerConnectMode() {
|
||||
case MultiplayerConnectDisableWG:
|
||||
return multiplayerDialDirect, nil
|
||||
case MultiplayerConnectRequireWG:
|
||||
if err := validateWireGuardConfig(config); err != nil {
|
||||
return multiplayerDialDirect, err
|
||||
}
|
||||
return multiplayerDialWireGuard, nil
|
||||
default:
|
||||
if config.WG == nil {
|
||||
return multiplayerDialDirect, nil
|
||||
}
|
||||
if err := validateWireGuardConfig(config); err != nil {
|
||||
return multiplayerDialDirect, err
|
||||
}
|
||||
return multiplayerDialWireGuard, nil
|
||||
}
|
||||
}
|
||||
|
||||
func directMTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
options, err := newMTLSDialOptions(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return dialRPCClient(fmt.Sprintf("%s:%d", config.LHost, config.LPort), options, nil)
|
||||
}
|
||||
|
||||
func newMTLSDialOptions(config *assets.ClientConfig) ([]grpc.DialOption, error) {
|
||||
tlsConfig, err := getTLSConfig(config.CACertificate, config.Certificate, config.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transportCreds := credentials.NewTLS(tlsConfig)
|
||||
callCreds := credentials.PerRPCCredentials(TokenAuth{token: config.Token})
|
||||
options := []grpc.DialOption{
|
||||
@@ -78,11 +132,18 @@ func MTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.Clie
|
||||
if kp, ok := getKeepaliveParams(); ok {
|
||||
options = append(options, grpc.WithKeepaliveParams(kp))
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
connection, err := grpc.NewClient(fmt.Sprintf("%s:%d", config.LHost, config.LPort), options...)
|
||||
func dialRPCClient(target string, options []grpc.DialOption, closer connectionCloser) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
connection, err := grpc.NewClient(target, options...)
|
||||
if err != nil {
|
||||
if closer != nil {
|
||||
_ = closer.Close()
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
registerConnCloser(connection, closer)
|
||||
|
||||
// Preserve the legacy grpc.DialContext + grpc.WithBlock behavior: block
|
||||
// until the channel becomes READY or the timeout expires.
|
||||
@@ -97,7 +158,7 @@ func MTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.Clie
|
||||
break
|
||||
}
|
||||
if !connection.WaitForStateChange(ctx, state) {
|
||||
_ = connection.Close()
|
||||
_ = CloseGRPCConnection(connection)
|
||||
return nil, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
)
|
||||
|
||||
func TestSelectMultiplayerDialStrategyLegacyConfigUsesDirectMTLS(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 TestSelectMultiplayerDialStrategyRequireWGRejectsMissingWGConfig(t *testing.T) {
|
||||
setTestMultiplayerConnectMode(t, MultiplayerConnectRequireWG)
|
||||
|
||||
_, err := selectMultiplayerDialStrategy(&assets.ClientConfig{})
|
||||
if !errors.Is(err, ErrMissingWireGuardConfig) {
|
||||
t.Fatalf("expected missing WG config error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectMultiplayerDialStrategyRejectsIncompleteWGConfig(t *testing.T) {
|
||||
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
|
||||
|
||||
_, err := selectMultiplayerDialStrategy(&assets.ClientConfig{
|
||||
WG: &assets.ClientWGConfig{
|
||||
ServerPubKey: "server-pub",
|
||||
ClientIP: "100.64.0.2",
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, ErrIncompleteWireGuardConfig) {
|
||||
t.Fatalf("expected incomplete WG config error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectMultiplayerDialStrategyUsesWireGuardWhenConfigComplete(t *testing.T) {
|
||||
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
|
||||
|
||||
strategy, err := selectMultiplayerDialStrategy(&assets.ClientConfig{
|
||||
WG: &assets.ClientWGConfig{
|
||||
ServerPubKey: "server-pub",
|
||||
ClientPrivateKey: "client-priv",
|
||||
ClientIP: "100.64.0.2",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("select dial strategy: %v", err)
|
||||
}
|
||||
if strategy != multiplayerDialWireGuard {
|
||||
t.Fatalf("expected WireGuard strategy, got %v", strategy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectMultiplayerDialStrategyDisableWGOverridesCompleteConfig(t *testing.T) {
|
||||
setTestMultiplayerConnectMode(t, MultiplayerConnectDisableWG)
|
||||
|
||||
strategy, err := selectMultiplayerDialStrategy(&assets.ClientConfig{
|
||||
WG: &assets.ClientWGConfig{
|
||||
ServerPubKey: "server-pub",
|
||||
ClientPrivateKey: "client-priv",
|
||||
ClientIP: "100.64.0.2",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("select dial strategy: %v", err)
|
||||
}
|
||||
if strategy != multiplayerDialDirect {
|
||||
t.Fatalf("expected direct strategy when WG is disabled, got %v", strategy)
|
||||
}
|
||||
}
|
||||
|
||||
func setTestMultiplayerConnectMode(t *testing.T, mode MultiplayerConnectMode) {
|
||||
t.Helper()
|
||||
|
||||
previous := getMultiplayerConnectMode()
|
||||
SetMultiplayerConnectMode(mode)
|
||||
t.Cleanup(func() {
|
||||
SetMultiplayerConnectMode(previous)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"golang.zx2c4.com/wireguard/conn"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
multiplayerWireGuardDefaultServerIP = "100.65.0.1"
|
||||
multiplayerWireGuardMTU = 1420
|
||||
multiplayerWireGuardKeepalive = 25
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingWireGuardConfig = errors.New("operator config has no wg block")
|
||||
ErrIncompleteWireGuardConfig = errors.New("operator config has incomplete wg block")
|
||||
)
|
||||
|
||||
type wireGuardTunnel struct {
|
||||
dev *device.Device
|
||||
net *transportNet
|
||||
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (t *wireGuardTunnel) Close() error {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
t.closeOnce.Do(func() {
|
||||
if t.dev != nil {
|
||||
t.dev.Close()
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *wireGuardTunnel) DialContext(ctx context.Context, address string) (net.Conn, error) {
|
||||
if t == nil || t.net == nil {
|
||||
return nil, errors.New("wireguard tunnel is not initialized")
|
||||
}
|
||||
return t.net.DialContext(ctx, "tcp", address)
|
||||
}
|
||||
|
||||
func wireGuardMTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
tunnel, target, err := newWireGuardTunnel(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
options, err := newMTLSDialOptions(config)
|
||||
if err != nil {
|
||||
_ = tunnel.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
options = append(options, grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
return tunnel.DialContext(ctx, addr)
|
||||
}))
|
||||
|
||||
return dialRPCClient(target, options, tunnel)
|
||||
}
|
||||
|
||||
func newWireGuardTunnel(config *assets.ClientConfig) (*wireGuardTunnel, string, error) {
|
||||
if err := validateWireGuardConfig(config); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if config.LPort <= 0 || 65535 < config.LPort {
|
||||
return nil, "", fmt.Errorf("invalid multiplayer port %d", config.LPort)
|
||||
}
|
||||
|
||||
clientIP, err := netip.ParseAddr(strings.TrimSpace(config.WG.ClientIP))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("invalid wireguard client IP %q: %w", config.WG.ClientIP, err)
|
||||
}
|
||||
|
||||
serverIPValue := strings.TrimSpace(config.WG.ServerIP)
|
||||
if serverIPValue == "" {
|
||||
serverIPValue = multiplayerWireGuardDefaultServerIP
|
||||
}
|
||||
serverIP, err := netip.ParseAddr(serverIPValue)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("invalid wireguard server IP %q: %w", serverIPValue, err)
|
||||
}
|
||||
|
||||
endpoint, err := resolveWireGuardEndpoint(config.LHost, config.LPort)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
tun, tNet, err := createTransportNetTUN([]netip.Addr{clientIP}, multiplayerWireGuardMTU)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, "[client/wg] "))
|
||||
wgConf := bytes.NewBuffer(nil)
|
||||
fmt.Fprintf(wgConf, "private_key=%s\n", strings.TrimSpace(config.WG.ClientPrivateKey))
|
||||
fmt.Fprintf(wgConf, "public_key=%s\n", strings.TrimSpace(config.WG.ServerPubKey))
|
||||
fmt.Fprintf(wgConf, "endpoint=%s\n", endpoint.String())
|
||||
fmt.Fprintf(wgConf, "allowed_ip=%s\n", multiplayerWireGuardAllowedIP(serverIP))
|
||||
fmt.Fprintf(wgConf, "persistent_keepalive_interval=%d\n", multiplayerWireGuardKeepalive)
|
||||
|
||||
if err := dev.IpcSetOperation(bufio.NewReader(wgConf)); err != nil {
|
||||
dev.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
if err := dev.Up(); err != nil {
|
||||
dev.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
target := net.JoinHostPort(serverIP.String(), strconv.Itoa(config.LPort))
|
||||
return &wireGuardTunnel{dev: dev, net: tNet}, target, nil
|
||||
}
|
||||
|
||||
func validateWireGuardConfig(config *assets.ClientConfig) error {
|
||||
if config == nil {
|
||||
return errors.New("client config is required")
|
||||
}
|
||||
if config.WG == nil {
|
||||
return ErrMissingWireGuardConfig
|
||||
}
|
||||
|
||||
missing := make([]string, 0, 3)
|
||||
if strings.TrimSpace(config.WG.ServerPubKey) == "" {
|
||||
missing = append(missing, "server_pub_key")
|
||||
}
|
||||
if strings.TrimSpace(config.WG.ClientPrivateKey) == "" {
|
||||
missing = append(missing, "client_private_key")
|
||||
}
|
||||
if strings.TrimSpace(config.WG.ClientIP) == "" {
|
||||
missing = append(missing, "client_ip")
|
||||
}
|
||||
if len(missing) != 0 {
|
||||
return fmt.Errorf("%w: missing %s", ErrIncompleteWireGuardConfig, strings.Join(missing, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveWireGuardEndpoint(host string, port int) (netip.AddrPort, error) {
|
||||
host = strings.Trim(strings.TrimSpace(host), "[]")
|
||||
if host == "" {
|
||||
return netip.AddrPort{}, errors.New("wireguard endpoint host is required")
|
||||
}
|
||||
|
||||
if addr, err := netip.ParseAddr(host); err == nil {
|
||||
return netip.AddrPortFrom(addr, uint16(port)), nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, fmt.Errorf("failed to resolve wireguard endpoint %q: %w", host, err)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if ip.IsValid() {
|
||||
return netip.AddrPortFrom(ip, uint16(port)), nil
|
||||
}
|
||||
}
|
||||
return netip.AddrPort{}, fmt.Errorf("wireguard endpoint %q did not resolve to a usable IP", host)
|
||||
}
|
||||
|
||||
func multiplayerWireGuardAllowedIP(addr netip.Addr) string {
|
||||
if addr.Is6() {
|
||||
return fmt.Sprintf("%s/128", addr)
|
||||
}
|
||||
return fmt.Sprintf("%s/32", addr)
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
insecurerand "math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
)
|
||||
|
||||
type transportTun struct {
|
||||
ep *channel.Endpoint
|
||||
stack *stack.Stack
|
||||
events chan tun.Event
|
||||
notifyHandle *channel.NotificationHandle
|
||||
incomingPacket chan *buffer.View
|
||||
mtu int
|
||||
hasV4 bool
|
||||
hasV6 bool
|
||||
}
|
||||
|
||||
type transportNet transportTun
|
||||
|
||||
func createTransportNetTUN(localAddresses []netip.Addr, mtu int) (tun.Device, *transportNet, error) {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(0xFFFFFFFF))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create random number: %w", err)
|
||||
}
|
||||
clock := tcpip.NewStdClock()
|
||||
rng := insecurerand.New(insecurerand.NewSource(n.Int64()))
|
||||
|
||||
opts := stack.Options{
|
||||
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
|
||||
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4},
|
||||
HandleLocal: true,
|
||||
IPTables: stack.DefaultTables(clock, rng),
|
||||
}
|
||||
dev := &transportTun{
|
||||
ep: channel.New(1024, uint32(mtu), ""),
|
||||
stack: stack.New(opts),
|
||||
events: make(chan tun.Event, 10),
|
||||
incomingPacket: make(chan *buffer.View),
|
||||
mtu: mtu,
|
||||
}
|
||||
|
||||
sackEnabledOpt := tcpip.TCPSACKEnabled(true)
|
||||
if tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt); tcpipErr != nil {
|
||||
return nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr)
|
||||
}
|
||||
|
||||
dev.notifyHandle = dev.ep.AddNotify(dev)
|
||||
if tcpipErr := dev.stack.CreateNIC(1, dev.ep); tcpipErr != nil {
|
||||
return nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr)
|
||||
}
|
||||
|
||||
for _, ip := range localAddresses {
|
||||
var protoNumber tcpip.NetworkProtocolNumber
|
||||
if ip.Is4() {
|
||||
protoNumber = ipv4.ProtocolNumber
|
||||
} else if ip.Is6() {
|
||||
protoNumber = ipv6.ProtocolNumber
|
||||
}
|
||||
protoAddr := tcpip.ProtocolAddress{
|
||||
Protocol: protoNumber,
|
||||
AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(),
|
||||
}
|
||||
if tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{}); tcpipErr != nil {
|
||||
return nil, nil, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr)
|
||||
}
|
||||
if ip.Is4() {
|
||||
dev.hasV4 = true
|
||||
} else if ip.Is6() {
|
||||
dev.hasV6 = true
|
||||
}
|
||||
}
|
||||
|
||||
if dev.hasV4 {
|
||||
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1})
|
||||
}
|
||||
if dev.hasV6 {
|
||||
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1})
|
||||
}
|
||||
|
||||
dev.events <- tun.EventUp
|
||||
return dev, (*transportNet)(dev), nil
|
||||
}
|
||||
|
||||
func (tun *transportTun) Name() (string, error) {
|
||||
return "go", nil
|
||||
}
|
||||
|
||||
func (tun *transportTun) File() *os.File {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tun *transportTun) Events() <-chan tun.Event {
|
||||
return tun.events
|
||||
}
|
||||
|
||||
func (tun *transportTun) Read(buf [][]byte, sizes []int, offset int) (int, error) {
|
||||
view, ok := <-tun.incomingPacket
|
||||
if !ok {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
|
||||
n, err := view.Read(buf[0][offset:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sizes[0] = n
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (tun *transportTun) Write(buf [][]byte, offset int) (int, error) {
|
||||
for _, buf := range buf {
|
||||
packet := buf[offset:]
|
||||
if len(packet) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)})
|
||||
switch packet[0] >> 4 {
|
||||
case 4:
|
||||
tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb)
|
||||
case 6:
|
||||
tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb)
|
||||
default:
|
||||
return 0, syscall.EAFNOSUPPORT
|
||||
}
|
||||
}
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (tun *transportTun) WriteNotify() {
|
||||
pkt := tun.ep.Read()
|
||||
if pkt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
view := pkt.ToView()
|
||||
pkt.DecRef()
|
||||
|
||||
tun.incomingPacket <- view
|
||||
}
|
||||
|
||||
func (tun *transportTun) Close() error {
|
||||
tun.stack.RemoveNIC(1)
|
||||
tun.stack.Close()
|
||||
tun.ep.RemoveNotify(tun.notifyHandle)
|
||||
tun.ep.Close()
|
||||
|
||||
if tun.events != nil {
|
||||
close(tun.events)
|
||||
}
|
||||
if tun.incomingPacket != nil {
|
||||
close(tun.incomingPacket)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tun *transportTun) MTU() (int, error) {
|
||||
return tun.mtu, nil
|
||||
}
|
||||
|
||||
func (tun *transportTun) BatchSize() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func convertTransportFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) {
|
||||
var protoNumber tcpip.NetworkProtocolNumber
|
||||
if endpoint.Addr().Is4() {
|
||||
protoNumber = ipv4.ProtocolNumber
|
||||
} else {
|
||||
protoNumber = ipv6.ProtocolNumber
|
||||
}
|
||||
return tcpip.FullAddress{
|
||||
NIC: 1,
|
||||
Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()),
|
||||
Port: endpoint.Port(),
|
||||
}, protoNumber
|
||||
}
|
||||
|
||||
func (netstack *transportNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error) {
|
||||
fa, pn := convertTransportFullAddr(addr)
|
||||
return gonet.DialContextTCP(ctx, netstack.stack, fa, pn)
|
||||
}
|
||||
|
||||
func (netstack *transportNet) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported network %q", network)
|
||||
}
|
||||
|
||||
host, portString, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port, err := net.LookupPort("tcp", portString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return netstack.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(addr, uint16(port)))
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package docs
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
markdownDir = "sliver-docs/pages/docs/md"
|
||||
asciinemaDir = "sliver-docs/public/asciinema"
|
||||
)
|
||||
|
||||
var (
|
||||
// rawFS stores embedded docs content from the Sliver docs site.
|
||||
//go:embed sliver-docs/pages/docs/md/*.md sliver-docs/public/asciinema/*
|
||||
rawFS embed.FS
|
||||
|
||||
// FS provides access to the embedded markdown docs rooted at the md directory.
|
||||
FS = mustSubFS(rawFS, markdownDir)
|
||||
|
||||
// AsciinemaFS provides access to the embedded asciinema recordings.
|
||||
AsciinemaFS = mustSubFS(rawFS, asciinemaDir)
|
||||
)
|
||||
|
||||
// Doc is a single embedded markdown document.
|
||||
type Doc struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// Docs mirrors the JSON shape used by the docs site.
|
||||
type Docs struct {
|
||||
Docs []Doc `json:"docs"`
|
||||
}
|
||||
|
||||
// All returns every embedded markdown document.
|
||||
func All() (Docs, error) {
|
||||
entries, err := fs.ReadDir(FS, ".")
|
||||
if err != nil {
|
||||
return Docs{}, err
|
||||
}
|
||||
|
||||
docs := make([]Doc, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || path.Ext(entry.Name()) != ".md" {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := fs.ReadFile(FS, entry.Name())
|
||||
if err != nil {
|
||||
return Docs{}, err
|
||||
}
|
||||
|
||||
docs = append(docs, Doc{
|
||||
Name: strings.TrimSuffix(entry.Name(), path.Ext(entry.Name())),
|
||||
Content: string(content),
|
||||
})
|
||||
}
|
||||
return Docs{Docs: docs}, nil
|
||||
}
|
||||
|
||||
// Read returns the embedded markdown for a doc name, with or without the .md suffix.
|
||||
func Read(name string) (string, error) {
|
||||
if !strings.HasSuffix(name, ".md") {
|
||||
name += ".md"
|
||||
}
|
||||
|
||||
content, err := fs.ReadFile(FS, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// ReadAsciinema returns the embedded asciinema recording, with or without the .cast suffix.
|
||||
func ReadAsciinema(name string) ([]byte, error) {
|
||||
if !strings.HasSuffix(name, ".cast") {
|
||||
name += ".cast"
|
||||
}
|
||||
return fs.ReadFile(AsciinemaFS, name)
|
||||
}
|
||||
|
||||
func mustSubFS(embedded fs.FS, dir string) fs.FS {
|
||||
sub, err := fs.Sub(embedded, dir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllMatchesEmbeddedMarkdownSources(t *testing.T) {
|
||||
dirEntries, err := os.ReadDir("sliver-docs/pages/docs/md")
|
||||
if err != nil {
|
||||
t.Fatalf("read markdown source directory: %v", err)
|
||||
}
|
||||
|
||||
expected := make(map[string]string, len(dirEntries))
|
||||
for _, entry := range dirEntries {
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".md" {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join("sliver-docs/pages/docs/md", entry.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("read markdown source %q: %v", entry.Name(), err)
|
||||
}
|
||||
expected[entry.Name()] = string(content)
|
||||
}
|
||||
|
||||
embeddedEntries, err := fs.ReadDir(FS, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("read embedded directory: %v", err)
|
||||
}
|
||||
if len(embeddedEntries) != len(expected) {
|
||||
t.Fatalf("embedded entry count = %d, want %d", len(embeddedEntries), len(expected))
|
||||
}
|
||||
|
||||
docs, err := All()
|
||||
if err != nil {
|
||||
t.Fatalf("All(): %v", err)
|
||||
}
|
||||
if len(docs.Docs) != len(expected) {
|
||||
t.Fatalf("All() doc count = %d, want %d", len(docs.Docs), len(expected))
|
||||
}
|
||||
|
||||
gotNames := make([]string, 0, len(docs.Docs))
|
||||
for _, doc := range docs.Docs {
|
||||
filename := doc.Name + ".md"
|
||||
gotNames = append(gotNames, doc.Name)
|
||||
|
||||
expectedContent, ok := expected[filename]
|
||||
if !ok {
|
||||
t.Fatalf("unexpected embedded doc %q", filename)
|
||||
}
|
||||
if doc.Content != expectedContent {
|
||||
t.Fatalf("embedded content mismatch for %q", filename)
|
||||
}
|
||||
}
|
||||
|
||||
wantNames := make([]string, 0, len(expected))
|
||||
for filename := range expected {
|
||||
wantNames = append(wantNames, filename[:len(filename)-len(filepath.Ext(filename))])
|
||||
}
|
||||
sort.Strings(gotNames)
|
||||
sort.Strings(wantNames)
|
||||
for i := range gotNames {
|
||||
if gotNames[i] != wantNames[i] {
|
||||
t.Fatalf("embedded names mismatch at index %d: got %q want %q", i, gotNames[i], wantNames[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAcceptsDocNameWithoutSuffix(t *testing.T) {
|
||||
want, err := os.ReadFile(filepath.Join("sliver-docs/pages/docs/md", "Getting Started.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read source doc: %v", err)
|
||||
}
|
||||
|
||||
got, err := Read("Getting Started")
|
||||
if err != nil {
|
||||
t.Fatalf("Read(): %v", err)
|
||||
}
|
||||
if got != string(want) {
|
||||
t.Fatal("Read() returned unexpected content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsciinemaFSMatchesEmbeddedSources(t *testing.T) {
|
||||
dirEntries, err := os.ReadDir("sliver-docs/public/asciinema")
|
||||
if err != nil {
|
||||
t.Fatalf("read asciinema source directory: %v", err)
|
||||
}
|
||||
|
||||
expected := make(map[string][]byte, len(dirEntries))
|
||||
for _, entry := range dirEntries {
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".cast" {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join("sliver-docs/public/asciinema", entry.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("read asciinema source %q: %v", entry.Name(), err)
|
||||
}
|
||||
expected[entry.Name()] = content
|
||||
}
|
||||
|
||||
embeddedEntries, err := fs.ReadDir(AsciinemaFS, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("read embedded asciinema directory: %v", err)
|
||||
}
|
||||
if len(embeddedEntries) != len(expected) {
|
||||
t.Fatalf("embedded asciinema entry count = %d, want %d", len(embeddedEntries), len(expected))
|
||||
}
|
||||
|
||||
for filename, want := range expected {
|
||||
got, err := fs.ReadFile(AsciinemaFS, filename)
|
||||
if err != nil {
|
||||
t.Fatalf("read embedded asciinema %q: %v", filename, err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("embedded asciinema mismatch for %q", filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAsciinemaAcceptsNameWithoutSuffix(t *testing.T) {
|
||||
want, err := os.ReadFile(filepath.Join("sliver-docs/public/asciinema", "intro.cast"))
|
||||
if err != nil {
|
||||
t.Fatalf("read source asciinema: %v", err)
|
||||
}
|
||||
|
||||
got, err := ReadAsciinema("intro")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAsciinema(): %v", err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatal("ReadAsciinema() returned unexpected content")
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,16 @@ 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:
|
||||
@@ -51,6 +61,12 @@ cxx: {}
|
||||
- `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.
|
||||
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
# Sliver Implant Framework
|
||||
# Copyright (C) 2019 Bishop Fox
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$SkipGenerate,
|
||||
[switch]$UnitOnly,
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]]$RemainingArgs
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
foreach ($arg in $RemainingArgs) {
|
||||
switch ($arg) {
|
||||
"--skip-generate" {
|
||||
$SkipGenerate = $true
|
||||
continue
|
||||
}
|
||||
"--unit-only" {
|
||||
$UnitOnly = $true
|
||||
$SkipGenerate = $true
|
||||
continue
|
||||
}
|
||||
default {
|
||||
throw "Unknown argument: $arg"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($UnitOnly) {
|
||||
$SkipGenerate = $true
|
||||
}
|
||||
|
||||
$script:TestTmpRoot = $null
|
||||
$script:CreatedTestRoot = $false
|
||||
|
||||
function Invoke-ExternalCommand {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
[string[]]$Arguments = @(),
|
||||
[hashtable]$Environment = @{}
|
||||
)
|
||||
|
||||
$previous = @{}
|
||||
try {
|
||||
foreach ($name in $Environment.Keys) {
|
||||
$previous[$name] = [System.Environment]::GetEnvironmentVariable($name, "Process")
|
||||
[System.Environment]::SetEnvironmentVariable($name, $Environment[$name], "Process")
|
||||
}
|
||||
|
||||
& $FilePath @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Command exited with code ${LASTEXITCODE}: $FilePath $($Arguments -join ' ')"
|
||||
}
|
||||
} catch {
|
||||
Write-Host $_
|
||||
throw
|
||||
} finally {
|
||||
foreach ($name in $Environment.Keys) {
|
||||
[System.Environment]::SetEnvironmentVariable($name, $previous[$name], "Process")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Write-FailureLogs {
|
||||
$logPath = Join-Path $env:SLIVER_ROOT_DIR "logs\sliver.log"
|
||||
if (Test-Path -LiteralPath $logPath) {
|
||||
Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-TestStep {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[scriptblock]$Action
|
||||
)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==> $Name"
|
||||
try {
|
||||
& $Action
|
||||
return
|
||||
} catch {
|
||||
Write-FailureLogs
|
||||
throw "Test step failed: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-SkipPackage {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Package,
|
||||
[string]$Tags = ""
|
||||
)
|
||||
|
||||
$goListArgs = @("list", "-e", "-f", "{{if .Error}}{{.Error}}{{end}}")
|
||||
if ($Tags) {
|
||||
$goListArgs += "-tags=$Tags"
|
||||
}
|
||||
$goListArgs += $Package
|
||||
|
||||
$goListError = & go @goListArgs 2>$null
|
||||
$goListText = ($goListError | Out-String)
|
||||
if ($goListText -like "*build constraints exclude all Go files*") {
|
||||
Write-Host ""
|
||||
Write-Host "==> Skipping $Package (unsupported on current platform)"
|
||||
return $true
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-TestDirectories {
|
||||
Get-ChildItem -Path @("client", "implant", "server", "util") -Recurse -Filter "*_test.go" -File |
|
||||
ForEach-Object {
|
||||
[string](Resolve-Path -LiteralPath $_.DirectoryName -Relative).Replace("\", "/")
|
||||
} |
|
||||
Sort-Object -Unique
|
||||
}
|
||||
|
||||
function Get-ServerBinaryPath {
|
||||
$candidates = @(
|
||||
(Join-Path $PWD.Path "sliver-server.exe"),
|
||||
(Join-Path $PWD.Path "sliver-server")
|
||||
)
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-UnpackServerAssets {
|
||||
$serverBinary = Get-ServerBinaryPath
|
||||
if ($serverBinary) {
|
||||
try {
|
||||
Invoke-ExternalCommand -FilePath $serverBinary -Arguments @("unpack", "--force")
|
||||
return
|
||||
} catch {
|
||||
Write-Host "sliver-server unpack failed, falling back to go run ./server unpack --force"
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("run", "-tags=server,go_sqlite", "./server", "unpack", "--force")
|
||||
}
|
||||
|
||||
function Stop-TestProcesses {
|
||||
if (-not $script:CreatedTestRoot -or -not $script:TestTmpRoot) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
$escapedRoot = [Regex]::Escape($script:TestTmpRoot)
|
||||
$relatedProcesses = Get-CimInstance Win32_Process -ErrorAction Stop |
|
||||
Where-Object {
|
||||
$_.ParentProcessId -eq $PID -or
|
||||
($_.CommandLine -and $_.CommandLine -match $escapedRoot)
|
||||
} |
|
||||
Sort-Object ProcessId -Unique
|
||||
|
||||
foreach ($proc in $relatedProcesses) {
|
||||
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {
|
||||
# Best-effort cleanup only.
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-TestRoot {
|
||||
if (-not $script:CreatedTestRoot -or -not $script:TestTmpRoot) {
|
||||
return
|
||||
}
|
||||
|
||||
for ($attempt = 0; $attempt -lt 5; $attempt++) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $script:TestTmpRoot -Recurse -Force -ErrorAction Stop
|
||||
} catch {
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $script:TestTmpRoot)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $script:TestTmpRoot) {
|
||||
Write-Warning "Failed to fully clean temp dir: $script:TestTmpRoot"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host "----------------------------------------------------------------"
|
||||
Write-Host "WARNING: Running unit tests on slow systems can take a LONG time"
|
||||
Write-Host " Recommended to only run on 16+ CPU cores and 32Gb+ RAM"
|
||||
Write-Host "----------------------------------------------------------------"
|
||||
|
||||
$tags = "osusergo,netgo,go_sqlite"
|
||||
|
||||
$script:TestTmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sliver-go-tests-" + [System.Guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $script:TestTmpRoot -Force | Out-Null
|
||||
$script:CreatedTestRoot = $true
|
||||
|
||||
$env:SLIVER_ROOT_DIR = Join-Path $script:TestTmpRoot "sliver"
|
||||
$env:SLIVER_CLIENT_DIR = Join-Path $script:TestTmpRoot "sliver-client"
|
||||
$env:SLIVER_CLIENT_ROOT_DIR = $env:SLIVER_CLIENT_DIR
|
||||
$env:HOME = Join-Path $script:TestTmpRoot "home"
|
||||
$env:XDG_CONFIG_HOME = Join-Path $script:TestTmpRoot "xdg-config"
|
||||
$env:XDG_CACHE_HOME = Join-Path $script:TestTmpRoot "xdg-cache"
|
||||
$env:XDG_DATA_HOME = Join-Path $script:TestTmpRoot "xdg-data"
|
||||
$env:TMPDIR = Join-Path $script:TestTmpRoot "tmp"
|
||||
$env:TMP = $env:TMPDIR
|
||||
$env:TEMP = $env:TMPDIR
|
||||
$env:GOCACHE = Join-Path $script:TestTmpRoot "go-cache"
|
||||
$env:GOTMPDIR = Join-Path $script:TestTmpRoot "go-tmp"
|
||||
if ($env:GOFLAGS) {
|
||||
$env:GOFLAGS = ($env:GOFLAGS.Trim() + " -mod=vendor").Trim()
|
||||
} else {
|
||||
$env:GOFLAGS = "-mod=vendor"
|
||||
}
|
||||
|
||||
@(
|
||||
$env:SLIVER_ROOT_DIR,
|
||||
$env:SLIVER_CLIENT_DIR,
|
||||
$env:HOME,
|
||||
$env:XDG_CONFIG_HOME,
|
||||
$env:XDG_CACHE_HOME,
|
||||
$env:XDG_DATA_HOME,
|
||||
$env:TMPDIR,
|
||||
$env:GOCACHE,
|
||||
$env:GOTMPDIR
|
||||
) | ForEach-Object {
|
||||
New-Item -ItemType Directory -Path $_ -Force | Out-Null
|
||||
}
|
||||
|
||||
$isWindowsPlatform = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
|
||||
[System.Runtime.InteropServices.OSPlatform]::Windows
|
||||
)
|
||||
$sliverGoBin = Join-Path $env:SLIVER_ROOT_DIR "go\bin"
|
||||
if (-not $isWindowsPlatform) {
|
||||
$env:PATH = [string]::Join([System.IO.Path]::PathSeparator, @($sliverGoBin, $env:PATH))
|
||||
}
|
||||
|
||||
Write-Host "Using isolated temp directories:"
|
||||
Write-Host " SLIVER_ROOT_DIR=$env:SLIVER_ROOT_DIR"
|
||||
Write-Host " SLIVER_CLIENT_DIR=$env:SLIVER_CLIENT_DIR"
|
||||
|
||||
Invoke-TestStep -Name "unpack server assets" -Action { Invoke-UnpackServerAssets }
|
||||
|
||||
$testDirs = @(Get-TestDirectories)
|
||||
$clientTestPkgs = @()
|
||||
$implantTestPkgs = @()
|
||||
$serverUtilTestPkgs = @()
|
||||
|
||||
foreach ($testDir in $testDirs) {
|
||||
$pkg = if ($testDir.StartsWith(".")) { $testDir } else { "./$testDir" }
|
||||
switch -Wildcard ($pkg) {
|
||||
"./server/c2" { continue }
|
||||
"./server/generate" { continue }
|
||||
"./client/*" {
|
||||
$clientTestPkgs += $pkg
|
||||
continue
|
||||
}
|
||||
"./implant/*" {
|
||||
$implantTestPkgs += $pkg
|
||||
continue
|
||||
}
|
||||
"./server/*" {
|
||||
$serverUtilTestPkgs += $pkg
|
||||
continue
|
||||
}
|
||||
"./util*" {
|
||||
$serverUtilTestPkgs += $pkg
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pkg in $clientTestPkgs) {
|
||||
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
|
||||
}
|
||||
|
||||
Invoke-TestStep -Name $pkg -Action {
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", $pkg)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pkg in $serverUtilTestPkgs) {
|
||||
if (Test-SkipPackage -Package $pkg -Tags "server,$tags") {
|
||||
continue
|
||||
}
|
||||
|
||||
switch ($pkg) {
|
||||
"./server/assets/traffic-encoders" {
|
||||
Invoke-TestStep -Name $pkg -Action {
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", "-timeout", "10m", "-tags=server,$tags", $pkg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
"./server/encoders" {
|
||||
Invoke-TestStep -Name $pkg -Action {
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", "-timeout", "10m", "-tags=server,$tags", $pkg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
"./server/rpc" {
|
||||
Invoke-TestStep -Name $pkg -Action {
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", "-vet=off", "-timeout", "30m", "-tags=server,$tags", $pkg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
default {
|
||||
Invoke-TestStep -Name $pkg -Action {
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", "-tags=server,$tags", $pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-TestStep -Name "./server/c2" -Action {
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", "-tags=server,$tags", "./server/c2")
|
||||
}
|
||||
|
||||
if ($UnitOnly) {
|
||||
Write-Host ""
|
||||
Write-Host "Skipping ./server/c2 e2e tests (--unit-only)"
|
||||
} else {
|
||||
Invoke-TestStep -Name "./server/c2 (e2e yamux)" -Action {
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", "-tags=server,$tags,sliver_e2e", "./server/c2", "-run", "Test(MTLS|WG)Yamux_", "-count=1")
|
||||
}
|
||||
Invoke-TestStep -Name "./server/c2 (e2e dns)" -Action {
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("test", "-tags=server,$tags,sliver_e2e", "./server/c2", "-run", "TestDNS_", "-count=1")
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipGenerate) {
|
||||
$generateGoMaxProcs = if ($env:SLIVER_GENERATE_GOMAXPROCS) { $env:SLIVER_GENERATE_GOMAXPROCS } else { "2" }
|
||||
$generateGoP = if ($env:SLIVER_GENERATE_GO_P) { $env:SLIVER_GENERATE_GO_P } else { "1" }
|
||||
$generateTestParallel = if ($env:SLIVER_GENERATE_TEST_PARALLEL) { $env:SLIVER_GENERATE_TEST_PARALLEL } else { "1" }
|
||||
|
||||
Invoke-TestStep -Name "./server/generate" -Action {
|
||||
Invoke-ExternalCommand -FilePath "go" `
|
||||
-Arguments @("test", "-timeout", "6h", "-p", $generateGoP, "-parallel", $generateTestParallel, "-tags=server,$tags", "./server/generate") `
|
||||
-Environment @{
|
||||
GOMAXPROCS = $generateGoMaxProcs
|
||||
GOPROXY = "off"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host ""
|
||||
if ($UnitOnly) {
|
||||
Write-Host "Skipping ./server/generate tests (--unit-only)"
|
||||
} else {
|
||||
Write-Host "Skipping ./server/generate tests (--skip-generate)"
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Stop-TestProcesses
|
||||
Remove-TestRoot
|
||||
}
|
||||
+17
-3
@@ -20,10 +20,15 @@ set -u
|
||||
set -o pipefail
|
||||
|
||||
SKIP_GENERATE=0
|
||||
UNIT_ONLY=0
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--skip-generate" ]; then
|
||||
SKIP_GENERATE=1
|
||||
fi
|
||||
if [ "$arg" = "--unit-only" ]; then
|
||||
UNIT_ONLY=1
|
||||
SKIP_GENERATE=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "----------------------------------------------------------------"
|
||||
@@ -219,8 +224,13 @@ done
|
||||
|
||||
## Server c2 (unit + e2e)
|
||||
run_test_cmd "./server/c2" go test -tags="server,$TAGS" ./server/c2 || exit 1
|
||||
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
|
||||
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
|
||||
if [ "$UNIT_ONLY" -eq 1 ]; then
|
||||
echo
|
||||
echo "Skipping ./server/c2 e2e tests (--unit-only)"
|
||||
else
|
||||
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
|
||||
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
|
||||
fi
|
||||
|
||||
## Server generate
|
||||
if [ "$SKIP_GENERATE" -eq 0 ]; then
|
||||
@@ -235,5 +245,9 @@ if [ "$SKIP_GENERATE" -eq 0 ]; then
|
||||
go test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
|
||||
else
|
||||
echo
|
||||
echo "Skipping ./server/generate tests (--skip-generate)"
|
||||
if [ "$UNIT_ONLY" -eq 1 ]; then
|
||||
echo "Skipping ./server/generate tests (--unit-only)"
|
||||
else
|
||||
echo "Skipping ./server/generate tests (--skip-generate)"
|
||||
fi
|
||||
fi
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/bishopfox/sliver
|
||||
|
||||
go 1.25.6
|
||||
go 1.25.8
|
||||
|
||||
replace github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.46.3-0.20231214181515-27e49f3c3b69
|
||||
|
||||
@@ -9,7 +9,11 @@ replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-2
|
||||
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701
|
||||
|
||||
require (
|
||||
charm.land/lipgloss/v2 v2.0.0
|
||||
charm.land/bubbles/v2 v2.0.0
|
||||
charm.land/fantasy v0.8.1
|
||||
charm.land/glamour/v2 v2.0.0
|
||||
charm.land/huh/v2 v2.0.3
|
||||
charm.land/lipgloss/v2 v2.0.2
|
||||
filippo.io/age v1.3.1
|
||||
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb
|
||||
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a
|
||||
@@ -18,11 +22,6 @@ require (
|
||||
github.com/SherClockHolmes/webpush-go v1.4.0
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/huh v0.8.0
|
||||
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/cheggaaa/pb/v3 v3.1.7
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
|
||||
github.com/chromedp/chromedp v0.14.1
|
||||
@@ -35,7 +34,7 @@ require (
|
||||
github.com/hashicorp/yamux v0.1.2
|
||||
github.com/jedib0t/go-pretty/v6 v6.7.8
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
|
||||
github.com/klauspost/compress v1.18.1
|
||||
github.com/klauspost/compress v1.18.4
|
||||
github.com/lesnuages/go-winio v0.4.19
|
||||
github.com/lesnuages/snitch v0.6.0
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
@@ -48,7 +47,7 @@ require (
|
||||
github.com/reeflective/console v0.1.25
|
||||
github.com/reeflective/readline v1.1.4
|
||||
github.com/rsteube/carapace v0.50.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/sliverarmory/beignet v0.0.3
|
||||
github.com/sliverarmory/malasada v0.0.3
|
||||
github.com/sliverarmory/wasm-donut v0.0.3
|
||||
@@ -60,17 +59,17 @@ require (
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/xlab/treeprint v1.2.0
|
||||
github.com/yiya1989/sshkrb5 v0.0.1
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9
|
||||
golang.org/x/mod v0.31.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/term v0.38.0
|
||||
golang.org/x/text v0.32.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
|
||||
golang.org/x/mod v0.33.0
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/term v0.40.0
|
||||
golang.org/x/text v0.34.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.257.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/api v0.269.0
|
||||
google.golang.org/grpc v1.79.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
@@ -85,37 +84,39 @@ require (
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/firestore v1.20.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.7.0 // 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.58.0 // indirect
|
||||
cloud.google.com/go/storage v1.60.0 // indirect
|
||||
filippo.io/hpke v0.4.0 // indirect
|
||||
firebase.google.com/go/v4 v4.18.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.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/MicahParks/keyfunc v1.9.0 // indirect
|
||||
github.com/PagerDuty/go-pagerduty v1.8.0 // indirect
|
||||
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20250718055228-285ecf400b48 // indirect
|
||||
github.com/appleboy/go-fcm v1.2.6 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // 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.4 // 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/service/sso v1.30.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // 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/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
|
||||
@@ -124,22 +125,27 @@ require (
|
||||
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // 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/json v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/creachadair/msync v0.7.1 // indirect
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||
github.com/dghubble/oauth1 v0.7.3 // indirect
|
||||
github.com/dghubble/sling v1.4.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-lark/lark v1.16.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -148,29 +154,39 @@ require (
|
||||
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-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.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
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kaptinlin/go-i18n v0.2.3 // indirect
|
||||
github.com/kaptinlin/jsonpointer v0.4.9 // indirect
|
||||
github.com/kaptinlin/jsonschema v0.6.10 // indirect
|
||||
github.com/kaptinlin/messageformat-go v0.4.9 // 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/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/mailgun/errors v0.4.0 // indirect
|
||||
github.com/mailgun/mailgun-go/v5 v5.8.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/mileusna/viber v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/oapi-codegen/runtime v1.1.2 // indirect
|
||||
github.com/openai/openai-go/v2 v2.7.1 // 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
|
||||
@@ -194,25 +210,27 @@ require (
|
||||
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
go.mau.fi/util v0.9.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.40.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.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.6 // indirect
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genai v1.49.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.0.0
|
||||
charm.land/bubbletea/v2 v2.0.0
|
||||
charm.land/bubbletea/v2 v2.0.2
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563 // indirect
|
||||
@@ -224,16 +242,15 @@ require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0 // indirect
|
||||
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.5 // 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/service/ssm v1.65.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/carapace-sh/carapace v1.11.0
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2
|
||||
github.com/charmbracelet/x/ansi v0.11.6
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
@@ -245,12 +262,11 @@ require (
|
||||
github.com/demisto/goxforce v0.0.0-20160322194047-db8357535b1d // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gaissmai/bart v0.25.0 // indirect
|
||||
github.com/gen2brain/shm v0.1.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
@@ -273,21 +289,18 @@ require (
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/mark3labs/mcp-go v0.43.2
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/mdlayher/netlink v1.8.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/moloch--/go-keystone v0.0.2
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
@@ -307,15 +320,16 @@ require (
|
||||
github.com/thedevsaddam/gojsonq/v2 v2.5.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/time v0.14.0
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect
|
||||
|
||||
@@ -6,31 +6,37 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/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.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
|
||||
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
|
||||
charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
|
||||
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/fantasy v0.8.1 h1:2zQovDrd26g+/X2+/6bMomTX54HmZWNNHWeorDO0WRo=
|
||||
charm.land/fantasy v0.8.1/go.mod h1:KJ8vjy9FH7G2aeR/fL+os2uFHkQ4js2+UJVbsUKCXYM=
|
||||
charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=
|
||||
charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w=
|
||||
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
||||
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
|
||||
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
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.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
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/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.20.0 h1:JLlT12QP0fM2SJirKVyu2spBCO8leElaW0OOtPm6HEo=
|
||||
cloud.google.com/go/firestore v1.20.0/go.mod h1:jqu4yKdBmDN5srneWzx3HlKrHFWFdlkgjgQ6BKIOFQo=
|
||||
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.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
|
||||
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
|
||||
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
|
||||
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.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
|
||||
cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
||||
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=
|
||||
filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0=
|
||||
@@ -56,14 +62,14 @@ github.com/Binject/universal v0.0.0-20220519011857-bea739e758c0/go.mod h1:J3XDRl
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
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/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=
|
||||
@@ -114,44 +120,46 @@ github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0 h1:JjwxKkxzcBk4k8147g0e
|
||||
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0/go.mod h1:IbERvuyb387Hppp8hX0SQTFt/mkej8+OhuS8L0nC2CI=
|
||||
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 h1:cIAK2NNf2yafdgpFRNJrgZMwvy61BEVpGoHc2n4/yWs=
|
||||
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4/go.mod h1:SalMPBCab3yuID8nIhLfzwoBV+lBRyaC7NhuN8qL8xE=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
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/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
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.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
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/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.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
|
||||
github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||
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/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=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
@@ -178,42 +186,38 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab h1:J7XQLgl9sefgTnTGrmX3xqvp5o6MCiBzEjGv5igAlc4=
|
||||
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab/go.mod h1:hqlYqR7uPKOKfnNeicUbZp0Ps0GeYFlKYtwh5HGDCx8=
|
||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
|
||||
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3 h1:KUeWGoKnmyrLaDIa0smE6pK5eFMZWNIxPGweQR12iLg=
|
||||
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3/go.mod h1:OMqKat/mm9a/qOnpuNOPyYO9bPzRNnmzLnRZT5KYltg=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||
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=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f h1:OKFNbG2sSmgpQW9EC3gYNG+QrcQ4+wWYjzfmJvWkkDo=
|
||||
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 h1:DTSZxdV9qQagD4iGcAt9RgaRBZtJl01bfKgdLzUzUPI=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ=
|
||||
github.com/charmbracelet/x/json v0.2.0/go.mod h1:opFIflx2YgXgi49xVUu8gEQ21teFAxyMwvOiZhIvWNM=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||
github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
|
||||
github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ=
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
|
||||
@@ -233,8 +237,8 @@ github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
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/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
|
||||
@@ -272,6 +276,8 @@ github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYC
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 h1:uh1GSejOhVPRQmoXZxY82TiewZB8QXiaP1skL7Nun3Y=
|
||||
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=
|
||||
@@ -279,17 +285,15 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
|
||||
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=
|
||||
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM=
|
||||
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
@@ -301,8 +305,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gaissmai/bart v0.25.0 h1:eqiokVPqM3F94vJ0bTHXHtH91S8zkKL+bKh+BsGOsJM=
|
||||
@@ -319,8 +323,8 @@ 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-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0=
|
||||
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
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=
|
||||
@@ -350,6 +354,8 @@ github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaEL
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
@@ -358,6 +364,8 @@ 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/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
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=
|
||||
@@ -417,12 +425,14 @@ 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.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
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/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=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
@@ -474,8 +484,9 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
@@ -483,6 +494,14 @@ github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5G
|
||||
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/kaptinlin/go-i18n v0.2.3 h1:jyN/YOXXLcnGRBLdU+a8+6782B97fWE5aQqAHtvvk8Q=
|
||||
github.com/kaptinlin/go-i18n v0.2.3/go.mod h1:O+Ax4HkMO0Jt4OaP4E4WCx0PAADeWkwk8Jgt9bjAU1w=
|
||||
github.com/kaptinlin/jsonpointer v0.4.9 h1:o//bYf4PCvnMJIIX8bIg77KB6DO3wBPAabRyPRKh680=
|
||||
github.com/kaptinlin/jsonpointer v0.4.9/go.mod h1:9y0LgXavlmVE5FSHShY5LRlURJJVhbyVJSRWkilrTqA=
|
||||
github.com/kaptinlin/jsonschema v0.6.10 h1:CYded7nrwVu7pU1GaIjtd9dSzgqZjh7+LTKFaWqS08I=
|
||||
github.com/kaptinlin/jsonschema v0.6.10/go.mod h1:ZXZ4K5KrRmCCF1i6dgvBsQifl+WTb8XShKj0NpQNrz8=
|
||||
github.com/kaptinlin/messageformat-go v0.4.9 h1:FR5j5n4aL4nG0afKn9vvANrKxLu7HjmbhJnw5ogIwAQ=
|
||||
github.com/kaptinlin/messageformat-go v0.4.9/go.mod h1:qZzrGrlvWDz2KyyvN3dOWcK9PVSRV1BnfnNU+zB/RWc=
|
||||
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=
|
||||
@@ -495,8 +514,10 @@ github.com/kevinburke/twilio-go v0.0.0-20250718182727-5fe7adc01f29 h1:AZ5D/KUEro
|
||||
github.com/kevinburke/twilio-go v0.0.0-20250718182727-5fe7adc01f29/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.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
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/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=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||
@@ -541,8 +562,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
@@ -556,6 +575,8 @@ github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ
|
||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mileusna/viber v1.0.1 h1:gWB6/lKoWYVxkH0Jb8jRnGIRZ/9DEM7RBZRJHRfdYWs=
|
||||
@@ -581,12 +602,8 @@ github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a h1:Q69DxLWF+Z8oI
|
||||
github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
|
||||
github.com/moloch--/sgn v0.0.5 h1:92GFaRpe+353uhqzFiNKXKgmTiD4dBxnvb8gd4MaV/g=
|
||||
github.com/moloch--/sgn v0.0.5/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
@@ -621,9 +638,13 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/openai/openai-go/v2 v2.7.1 h1:/tfvTJhfv7hTSL8mWwc5VL4WLLSDL5yn9VqVykdu9r8=
|
||||
github.com/openai/openai-go/v2 v2.7.1/go.mod h1:jrJs23apqJKKbT+pqtFgNKpRju/KP9zpUTZhz3GElQE=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
@@ -673,8 +694,8 @@ github.com/silenceper/wechat/v2 v2.1.11/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7a
|
||||
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.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/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.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
|
||||
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
|
||||
github.com/sliverarmory/beignet v0.0.3 h1:ssR6AK9lirVulHZhQMyMmVAi0FcFwWxlh9LwKCJQ4X0=
|
||||
@@ -792,30 +813,39 @@ github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT0
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
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-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=
|
||||
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
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.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.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4=
|
||||
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.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
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.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
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=
|
||||
@@ -836,11 +866,11 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
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/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=
|
||||
@@ -856,8 +886,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.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
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=
|
||||
@@ -879,11 +909,11 @@ 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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20171101214715-fd80eb99c8f6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -922,7 +952,6 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -938,8 +967,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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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=
|
||||
@@ -949,8 +978,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -964,8 +993,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -982,8 +1011,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.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
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=
|
||||
@@ -998,28 +1027,30 @@ 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.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
|
||||
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
|
||||
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/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=
|
||||
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||
google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0=
|
||||
google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
|
||||
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-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
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-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
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=
|
||||
|
||||
@@ -97,11 +97,20 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
|
||||
}
|
||||
}
|
||||
tcpProxy := &tcpproxy.Proxy{}
|
||||
var keepAlivePeriod time.Duration
|
||||
if req.KeepAlive > 0 {
|
||||
keepAlivePeriod = time.Duration(req.KeepAlive) * time.Second
|
||||
} else if req.KeepAlive < 0 {
|
||||
keepAlivePeriod = -1 * time.Second
|
||||
} else {
|
||||
keepAlivePeriod = 30 * time.Second
|
||||
}
|
||||
|
||||
channelProxy := &rportfwd.ChannelProxy{
|
||||
Conn: connection,
|
||||
RemoteAddr: req.ForwardAddress,
|
||||
BindAddr: req.BindAddress,
|
||||
KeepAlivePeriod: 1000 * time.Second,
|
||||
KeepAlivePeriod: keepAlivePeriod,
|
||||
DialTimeout: 30 * time.Second,
|
||||
}
|
||||
tcpProxy.AddRoute(req.BindAddress, channelProxy)
|
||||
|
||||
@@ -81,9 +81,16 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[portfwd] Configuring keep alive")
|
||||
// {{end}}
|
||||
conn.SetKeepAlive(true)
|
||||
// TODO: Make KeepAlive configurable
|
||||
conn.SetKeepAlivePeriod(30 * time.Second)
|
||||
if portfwdReq.KeepAlive < 0 {
|
||||
conn.SetKeepAlive(false)
|
||||
} else {
|
||||
conn.SetKeepAlive(true)
|
||||
if portfwdReq.KeepAlive > 0 {
|
||||
conn.SetKeepAlivePeriod(time.Duration(portfwdReq.KeepAlive) * time.Second)
|
||||
} else {
|
||||
conn.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add tunnel
|
||||
|
||||
@@ -140,9 +140,16 @@ func (p *ChannelProxy) HandleConn(src net.Conn) {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[portfwd] Configuring keep alive")
|
||||
// {{end}}
|
||||
conn.SetKeepAlive(true)
|
||||
// TODO: Make KeepAlive configurable
|
||||
conn.SetKeepAlivePeriod(30 * time.Second)
|
||||
if p.KeepAlivePeriod < 0 {
|
||||
conn.SetKeepAlive(false)
|
||||
} else {
|
||||
conn.SetKeepAlive(true)
|
||||
if p.KeepAlivePeriod > 0 {
|
||||
conn.SetKeepAlivePeriod(p.KeepAlivePeriod)
|
||||
} else {
|
||||
conn.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add tunnel
|
||||
// {{if .Config.Debug}}
|
||||
|
||||
@@ -58,6 +58,7 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {
|
||||
var buf []byte
|
||||
var err error
|
||||
var reqHandle uintptr
|
||||
var connHandle uintptr
|
||||
var resp *Response
|
||||
|
||||
var rawBody []byte
|
||||
@@ -81,9 +82,17 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {
|
||||
Body: rawBody,
|
||||
}
|
||||
|
||||
if reqHandle, err = buildRequest(c.handle, req); err != nil {
|
||||
if connHandle, reqHandle, err = buildRequest(c.handle, req); err != nil {
|
||||
if reqHandle != 0 {
|
||||
InternetCloseHandle(reqHandle)
|
||||
}
|
||||
if connHandle != 0 {
|
||||
InternetCloseHandle(connHandle)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer InternetCloseHandle(reqHandle)
|
||||
defer InternetCloseHandle(connHandle)
|
||||
|
||||
if c.Timeout > 0 {
|
||||
buf = make([]byte, 4)
|
||||
@@ -232,3 +241,11 @@ func (jar *Jar) SetCookies(u *url.URL, cookies []*Cookie) {
|
||||
func (jar *Jar) Cookies(u *url.URL) []*Cookie {
|
||||
return jar.cookies
|
||||
}
|
||||
|
||||
// Close will close the client handle
|
||||
func (c *Client) Close() {
|
||||
if c.handle != 0 {
|
||||
InternetCloseHandle(c.handle)
|
||||
c.handle = 0
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ func convertFail(str string, err error) error {
|
||||
)
|
||||
}
|
||||
|
||||
func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
|
||||
func buildRequest(sessionHndl uintptr, r *Request) (uintptr, uintptr, error) {
|
||||
var connHndl uintptr
|
||||
var err error
|
||||
var flags uintptr
|
||||
@@ -29,7 +29,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
|
||||
|
||||
// Parse URL
|
||||
if uri, err = url.Parse(r.URL); err != nil {
|
||||
return 0, fmt.Errorf("failed to parse url %s: %w", r.URL, err)
|
||||
return 0, 0, fmt.Errorf("failed to parse url %s: %w", r.URL, err)
|
||||
}
|
||||
|
||||
passwd, _ = uri.User.Password()
|
||||
@@ -37,7 +37,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
|
||||
if uri.Port() != "" {
|
||||
if port, err = strconv.ParseInt(uri.Port(), 10, 64); err != nil {
|
||||
err = fmt.Errorf("port %s invalid: %w", uri.Port(), err)
|
||||
return 0, err
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create connection: %w", err)
|
||||
return 0, 0, fmt.Errorf("failed to create connection: %w", err)
|
||||
}
|
||||
|
||||
// Send query string too
|
||||
@@ -82,10 +82,10 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to open request: %w", err)
|
||||
return connHndl, 0, fmt.Errorf("failed to open request: %w", err)
|
||||
}
|
||||
|
||||
return reqHndl, nil
|
||||
return connHndl, reqHndl, nil
|
||||
}
|
||||
|
||||
var cookies []*Cookie
|
||||
|
||||
@@ -213,6 +213,20 @@ func HTTPSendRequestW(
|
||||
return nil
|
||||
}
|
||||
|
||||
// InternetCloseHandle is from wininet.h
|
||||
func InternetCloseHandle(hndl uintptr) error {
|
||||
var err error
|
||||
var proc string = "InternetCloseHandle"
|
||||
var success uintptr
|
||||
|
||||
success, _, err = wininet.NewProc(proc).Call(hndl)
|
||||
if success == 0 {
|
||||
return fmt.Errorf("%s: %w", proc, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InternetConnectW is from wininet.h
|
||||
func InternetConnectW(
|
||||
sessionHndl uintptr,
|
||||
@@ -437,7 +451,7 @@ func InternetErrorDlg(
|
||||
)
|
||||
// we expect 12032 if user hits okay, otherwise we return the response code as an error
|
||||
if uint32(success) != ERROR_INTERNET_FORCE_RETRY {
|
||||
return success, fmt.Errorf("%s: %s", proc, success)
|
||||
return success, fmt.Errorf("%s: %v", proc, success)
|
||||
}
|
||||
|
||||
*lppvData = buf
|
||||
|
||||
+3374
-5501
File diff suppressed because it is too large.
Load diff
@@ -456,6 +456,7 @@ message ListenerJob {
|
||||
message MultiplayerListenerReq {
|
||||
string Host = 1;
|
||||
uint32 Port = 2;
|
||||
bool WireGuard = 3;
|
||||
}
|
||||
|
||||
message MTLSListenerReq {
|
||||
@@ -1407,6 +1408,68 @@ message CrackFileChunk {
|
||||
bytes Data = 9;
|
||||
}
|
||||
|
||||
// [ AI ] ----------------------------------------
|
||||
|
||||
message AIProviderConfigs {
|
||||
repeated AIProviderConfig Providers = 1;
|
||||
AIConfigSummary Config = 2;
|
||||
}
|
||||
|
||||
message AIProviderConfig {
|
||||
string Name = 1;
|
||||
bool Configured = 2;
|
||||
}
|
||||
|
||||
message AIConfigSummary {
|
||||
string Provider = 1;
|
||||
string Model = 2;
|
||||
string ThinkingLevel = 3;
|
||||
bool Valid = 4;
|
||||
string Error = 5;
|
||||
}
|
||||
|
||||
message AIConversationReq {
|
||||
string ID = 1;
|
||||
bool IncludeMessages = 2;
|
||||
}
|
||||
|
||||
message AIConversations {
|
||||
repeated AIConversation Conversations = 1;
|
||||
}
|
||||
|
||||
message AIConversation {
|
||||
string ID = 1;
|
||||
int64 CreatedAt = 2;
|
||||
int64 UpdatedAt = 3;
|
||||
string OperatorName = 4;
|
||||
string Provider = 5;
|
||||
string Model = 6;
|
||||
string Title = 7;
|
||||
string Summary = 8;
|
||||
string SystemPrompt = 9;
|
||||
repeated AIConversationMessage Messages = 10;
|
||||
}
|
||||
|
||||
message AIConversationMessages {
|
||||
string ConversationID = 1;
|
||||
repeated AIConversationMessage Messages = 2;
|
||||
}
|
||||
|
||||
message AIConversationMessage {
|
||||
string ID = 1;
|
||||
string ConversationID = 2;
|
||||
int64 CreatedAt = 3;
|
||||
int64 UpdatedAt = 4;
|
||||
string OperatorName = 5;
|
||||
string Provider = 6;
|
||||
string Model = 7;
|
||||
uint32 Sequence = 8;
|
||||
string Role = 9;
|
||||
string Content = 10;
|
||||
string ProviderMessageID = 11;
|
||||
string FinishReason = 12;
|
||||
}
|
||||
|
||||
// watchtower
|
||||
message MonitoringProviders {
|
||||
repeated MonitoringProvider providers = 1;
|
||||
|
||||
+95
-196
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v6.33.4
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.34.0
|
||||
// source: commonpb/common.proto
|
||||
|
||||
package commonpb
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,18 +22,16 @@ const (
|
||||
)
|
||||
|
||||
type Empty struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Empty) Reset() {
|
||||
*x = Empty{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_commonpb_common_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_commonpb_common_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Empty) String() string {
|
||||
@@ -43,7 +42,7 @@ func (*Empty) ProtoMessage() {}
|
||||
|
||||
func (x *Empty) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_commonpb_common_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -60,23 +59,20 @@ func (*Empty) Descriptor() ([]byte, []int) {
|
||||
|
||||
// Request - Common fields used in all gRPC requests
|
||||
type Request struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Async bool `protobuf:"varint,1,opt,name=Async,proto3" json:"Async,omitempty"`
|
||||
Timeout int64 `protobuf:"varint,2,opt,name=Timeout,proto3" json:"Timeout,omitempty"`
|
||||
BeaconID string `protobuf:"bytes,8,opt,name=BeaconID,proto3" json:"BeaconID,omitempty"`
|
||||
SessionID string `protobuf:"bytes,9,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Async bool `protobuf:"varint,1,opt,name=Async,proto3" json:"Async,omitempty"`
|
||||
Timeout int64 `protobuf:"varint,2,opt,name=Timeout,proto3" json:"Timeout,omitempty"`
|
||||
BeaconID string `protobuf:"bytes,8,opt,name=BeaconID,proto3" json:"BeaconID,omitempty"`
|
||||
SessionID string `protobuf:"bytes,9,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Request) Reset() {
|
||||
*x = Request{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_commonpb_common_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_commonpb_common_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Request) String() string {
|
||||
@@ -87,7 +83,7 @@ func (*Request) ProtoMessage() {}
|
||||
|
||||
func (x *Request) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_commonpb_common_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -131,26 +127,24 @@ func (x *Request) GetSessionID() string {
|
||||
}
|
||||
|
||||
// Response - Common fields used in all gRPC responses. Note that the Err field
|
||||
// only used when the implant needs to return an error to the server.
|
||||
// Client<->Server comms should use normal gRPC error handling.
|
||||
//
|
||||
// only used when the implant needs to return an error to the server.
|
||||
// Client<->Server comms should use normal gRPC error handling.
|
||||
type Response struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Err string `protobuf:"bytes,1,opt,name=Err,proto3" json:"Err,omitempty"`
|
||||
Async bool `protobuf:"varint,2,opt,name=Async,proto3" json:"Async,omitempty"`
|
||||
BeaconID string `protobuf:"bytes,8,opt,name=BeaconID,proto3" json:"BeaconID,omitempty"`
|
||||
TaskID string `protobuf:"bytes,9,opt,name=TaskID,proto3" json:"TaskID,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Err string `protobuf:"bytes,1,opt,name=Err,proto3" json:"Err,omitempty"`
|
||||
Async bool `protobuf:"varint,2,opt,name=Async,proto3" json:"Async,omitempty"`
|
||||
BeaconID string `protobuf:"bytes,8,opt,name=BeaconID,proto3" json:"BeaconID,omitempty"`
|
||||
TaskID string `protobuf:"bytes,9,opt,name=TaskID,proto3" json:"TaskID,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Response) Reset() {
|
||||
*x = Response{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_commonpb_common_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_commonpb_common_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Response) String() string {
|
||||
@@ -161,7 +155,7 @@ func (*Response) ProtoMessage() {}
|
||||
|
||||
func (x *Response) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_commonpb_common_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -206,21 +200,18 @@ func (x *Response) GetTaskID() string {
|
||||
|
||||
// File - A basic file data type
|
||||
type File struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *File) Reset() {
|
||||
*x = File{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_commonpb_common_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_commonpb_common_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *File) String() string {
|
||||
@@ -231,7 +222,7 @@ func (*File) ProtoMessage() {}
|
||||
|
||||
func (x *File) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_commonpb_common_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -262,26 +253,23 @@ func (x *File) GetData() []byte {
|
||||
|
||||
// Process - A basic process data type
|
||||
type Process struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Pid int32 `protobuf:"varint,1,opt,name=Pid,proto3" json:"Pid,omitempty"`
|
||||
Ppid int32 `protobuf:"varint,2,opt,name=Ppid,proto3" json:"Ppid,omitempty"`
|
||||
Executable string `protobuf:"bytes,3,opt,name=Executable,proto3" json:"Executable,omitempty"`
|
||||
Owner string `protobuf:"bytes,4,opt,name=Owner,proto3" json:"Owner,omitempty"`
|
||||
Architecture string `protobuf:"bytes,7,opt,name=Architecture,proto3" json:"Architecture,omitempty"`
|
||||
SessionID int32 `protobuf:"varint,5,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
|
||||
CmdLine []string `protobuf:"bytes,6,rep,name=CmdLine,proto3" json:"CmdLine,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Pid int32 `protobuf:"varint,1,opt,name=Pid,proto3" json:"Pid,omitempty"`
|
||||
Ppid int32 `protobuf:"varint,2,opt,name=Ppid,proto3" json:"Ppid,omitempty"`
|
||||
Executable string `protobuf:"bytes,3,opt,name=Executable,proto3" json:"Executable,omitempty"`
|
||||
Owner string `protobuf:"bytes,4,opt,name=Owner,proto3" json:"Owner,omitempty"`
|
||||
Architecture string `protobuf:"bytes,7,opt,name=Architecture,proto3" json:"Architecture,omitempty"`
|
||||
SessionID int32 `protobuf:"varint,5,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
|
||||
CmdLine []string `protobuf:"bytes,6,rep,name=CmdLine,proto3" json:"CmdLine,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Process) Reset() {
|
||||
*x = Process{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_commonpb_common_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_commonpb_common_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Process) String() string {
|
||||
@@ -292,7 +280,7 @@ func (*Process) ProtoMessage() {}
|
||||
|
||||
func (x *Process) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_commonpb_common_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -358,21 +346,18 @@ func (x *Process) GetCmdLine() []string {
|
||||
|
||||
// EnvVar - Environment variable K/V
|
||||
type EnvVar struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Key string `protobuf:"bytes,1,opt,name=Key,proto3" json:"Key,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Key string `protobuf:"bytes,1,opt,name=Key,proto3" json:"Key,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EnvVar) Reset() {
|
||||
*x = EnvVar{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_commonpb_common_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_commonpb_common_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EnvVar) String() string {
|
||||
@@ -383,7 +368,7 @@ func (*EnvVar) ProtoMessage() {}
|
||||
|
||||
func (x *EnvVar) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_commonpb_common_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -414,62 +399,51 @@ func (x *EnvVar) GetValue() string {
|
||||
|
||||
var File_commonpb_common_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_commonpb_common_proto_rawDesc = []byte{
|
||||
0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f,
|
||||
0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x70,
|
||||
0x62, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x73, 0x0a, 0x07, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x41, 0x73, 0x79, 0x6e, 0x63, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x41, 0x73, 0x79, 0x6e, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x54,
|
||||
0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x42, 0x65, 0x61, 0x63, 0x6f, 0x6e, 0x49,
|
||||
0x44, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x42, 0x65, 0x61, 0x63, 0x6f, 0x6e, 0x49,
|
||||
0x44, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x09,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x22,
|
||||
0x66, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x45,
|
||||
0x72, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x45, 0x72, 0x72, 0x12, 0x14, 0x0a,
|
||||
0x05, 0x41, 0x73, 0x79, 0x6e, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x41, 0x73,
|
||||
0x79, 0x6e, 0x63, 0x12, 0x1a, 0x0a, 0x08, 0x42, 0x65, 0x61, 0x63, 0x6f, 0x6e, 0x49, 0x44, 0x18,
|
||||
0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x42, 0x65, 0x61, 0x63, 0x6f, 0x6e, 0x49, 0x44, 0x12,
|
||||
0x16, 0x0a, 0x06, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x06, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x44, 0x22, 0x2e, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e,
|
||||
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x0c, 0x52, 0x04, 0x44, 0x61, 0x74, 0x61, 0x22, 0xc1, 0x01, 0x0a, 0x07, 0x50, 0x72, 0x6f, 0x63,
|
||||
0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x50, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05,
|
||||
0x52, 0x03, 0x50, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x70, 0x69, 0x64, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x70, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x45, 0x78, 0x65,
|
||||
0x63, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x45,
|
||||
0x78, 0x65, 0x63, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4f, 0x77, 0x6e,
|
||||
0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12,
|
||||
0x22, 0x0a, 0x0c, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18,
|
||||
0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74,
|
||||
0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44,
|
||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49,
|
||||
0x44, 0x12, 0x18, 0x0a, 0x07, 0x43, 0x6d, 0x64, 0x4c, 0x69, 0x6e, 0x65, 0x18, 0x06, 0x20, 0x03,
|
||||
0x28, 0x09, 0x52, 0x07, 0x43, 0x6d, 0x64, 0x4c, 0x69, 0x6e, 0x65, 0x22, 0x30, 0x0a, 0x06, 0x45,
|
||||
0x6e, 0x76, 0x56, 0x61, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2f, 0x5a,
|
||||
0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x69, 0x73, 0x68,
|
||||
0x6f, 0x70, 0x66, 0x6f, 0x78, 0x2f, 0x73, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x70, 0x62, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
const file_commonpb_common_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x15commonpb/common.proto\x12\bcommonpb\"\a\n" +
|
||||
"\x05Empty\"s\n" +
|
||||
"\aRequest\x12\x14\n" +
|
||||
"\x05Async\x18\x01 \x01(\bR\x05Async\x12\x18\n" +
|
||||
"\aTimeout\x18\x02 \x01(\x03R\aTimeout\x12\x1a\n" +
|
||||
"\bBeaconID\x18\b \x01(\tR\bBeaconID\x12\x1c\n" +
|
||||
"\tSessionID\x18\t \x01(\tR\tSessionID\"f\n" +
|
||||
"\bResponse\x12\x10\n" +
|
||||
"\x03Err\x18\x01 \x01(\tR\x03Err\x12\x14\n" +
|
||||
"\x05Async\x18\x02 \x01(\bR\x05Async\x12\x1a\n" +
|
||||
"\bBeaconID\x18\b \x01(\tR\bBeaconID\x12\x16\n" +
|
||||
"\x06TaskID\x18\t \x01(\tR\x06TaskID\".\n" +
|
||||
"\x04File\x12\x12\n" +
|
||||
"\x04Name\x18\x01 \x01(\tR\x04Name\x12\x12\n" +
|
||||
"\x04Data\x18\x02 \x01(\fR\x04Data\"\xc1\x01\n" +
|
||||
"\aProcess\x12\x10\n" +
|
||||
"\x03Pid\x18\x01 \x01(\x05R\x03Pid\x12\x12\n" +
|
||||
"\x04Ppid\x18\x02 \x01(\x05R\x04Ppid\x12\x1e\n" +
|
||||
"\n" +
|
||||
"Executable\x18\x03 \x01(\tR\n" +
|
||||
"Executable\x12\x14\n" +
|
||||
"\x05Owner\x18\x04 \x01(\tR\x05Owner\x12\"\n" +
|
||||
"\fArchitecture\x18\a \x01(\tR\fArchitecture\x12\x1c\n" +
|
||||
"\tSessionID\x18\x05 \x01(\x05R\tSessionID\x12\x18\n" +
|
||||
"\aCmdLine\x18\x06 \x03(\tR\aCmdLine\"0\n" +
|
||||
"\x06EnvVar\x12\x10\n" +
|
||||
"\x03Key\x18\x01 \x01(\tR\x03Key\x12\x14\n" +
|
||||
"\x05Value\x18\x02 \x01(\tR\x05ValueB/Z-github.com/bishopfox/sliver/protobuf/commonpbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_commonpb_common_proto_rawDescOnce sync.Once
|
||||
file_commonpb_common_proto_rawDescData = file_commonpb_common_proto_rawDesc
|
||||
file_commonpb_common_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_commonpb_common_proto_rawDescGZIP() []byte {
|
||||
file_commonpb_common_proto_rawDescOnce.Do(func() {
|
||||
file_commonpb_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_commonpb_common_proto_rawDescData)
|
||||
file_commonpb_common_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_commonpb_common_proto_rawDesc), len(file_commonpb_common_proto_rawDesc)))
|
||||
})
|
||||
return file_commonpb_common_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_commonpb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_commonpb_common_proto_goTypes = []interface{}{
|
||||
var file_commonpb_common_proto_goTypes = []any{
|
||||
(*Empty)(nil), // 0: commonpb.Empty
|
||||
(*Request)(nil), // 1: commonpb.Request
|
||||
(*Response)(nil), // 2: commonpb.Response
|
||||
@@ -490,85 +464,11 @@ func file_commonpb_common_proto_init() {
|
||||
if File_commonpb_common_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_commonpb_common_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Empty); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_commonpb_common_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Request); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_commonpb_common_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Response); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_commonpb_common_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*File); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_commonpb_common_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Process); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_commonpb_common_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*EnvVar); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_commonpb_common_proto_rawDesc,
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_commonpb_common_proto_rawDesc), len(file_commonpb_common_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
@@ -579,7 +479,6 @@ func file_commonpb_common_proto_init() {
|
||||
MessageInfos: file_commonpb_common_proto_msgTypes,
|
||||
}.Build()
|
||||
File_commonpb_common_proto = out.File
|
||||
file_commonpb_common_proto_rawDesc = nil
|
||||
file_commonpb_common_proto_goTypes = nil
|
||||
file_commonpb_common_proto_depIdxs = nil
|
||||
}
|
||||
+41
-64
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v6.33.4
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.34.0
|
||||
// source: dnspb/dns.proto
|
||||
|
||||
package dnspb
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -84,29 +85,25 @@ func (DNSMessageType) EnumDescriptor() ([]byte, []int) {
|
||||
return file_dnspb_dns_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
//
|
||||
//NOTE: DNS is very space sensitive so certain fields are re-purposed
|
||||
//depending on the DNSMessageType.
|
||||
// NOTE: DNS is very space sensitive so certain fields are re-purposed
|
||||
// depending on the DNSMessageType.
|
||||
type DNSMessage struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type DNSMessageType `protobuf:"varint,1,opt,name=Type,proto3,enum=dnspb.DNSMessageType" json:"Type,omitempty"`
|
||||
ID uint32 `protobuf:"varint,2,opt,name=ID,proto3" json:"ID,omitempty"` // 8 bit message id + 24 bit dns session ID
|
||||
Start uint32 `protobuf:"varint,3,opt,name=Start,proto3" json:"Start,omitempty"` // Bytes start at
|
||||
Stop uint32 `protobuf:"varint,4,opt,name=Stop,proto3" json:"Stop,omitempty"` // Bytes stop at
|
||||
Size uint32 `protobuf:"varint,5,opt,name=Size,proto3" json:"Size,omitempty"` // Total size
|
||||
Data []byte `protobuf:"bytes,6,opt,name=Data,proto3" json:"Data,omitempty"` // Actual data
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Type DNSMessageType `protobuf:"varint,1,opt,name=Type,proto3,enum=dnspb.DNSMessageType" json:"Type,omitempty"`
|
||||
ID uint32 `protobuf:"varint,2,opt,name=ID,proto3" json:"ID,omitempty"` // 8 bit message id + 24 bit dns session ID
|
||||
Start uint32 `protobuf:"varint,3,opt,name=Start,proto3" json:"Start,omitempty"` // Bytes start at
|
||||
Stop uint32 `protobuf:"varint,4,opt,name=Stop,proto3" json:"Stop,omitempty"` // Bytes stop at
|
||||
Size uint32 `protobuf:"varint,5,opt,name=Size,proto3" json:"Size,omitempty"` // Total size
|
||||
Data []byte `protobuf:"bytes,6,opt,name=Data,proto3" json:"Data,omitempty"` // Actual data
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DNSMessage) Reset() {
|
||||
*x = DNSMessage{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_dnspb_dns_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_dnspb_dns_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DNSMessage) String() string {
|
||||
@@ -117,7 +114,7 @@ func (*DNSMessage) ProtoMessage() {}
|
||||
|
||||
func (x *DNSMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_dnspb_dns_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@@ -176,47 +173,42 @@ func (x *DNSMessage) GetData() []byte {
|
||||
|
||||
var File_dnspb_dns_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_dnspb_dns_proto_rawDesc = []byte{
|
||||
0x0a, 0x0f, 0x64, 0x6e, 0x73, 0x70, 0x62, 0x2f, 0x64, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x12, 0x05, 0x64, 0x6e, 0x73, 0x70, 0x62, 0x22, 0x99, 0x01, 0x0a, 0x0a, 0x44, 0x4e, 0x53,
|
||||
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x64, 0x6e, 0x73, 0x70, 0x62, 0x2e, 0x44, 0x4e,
|
||||
0x53, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79,
|
||||
0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02,
|
||||
0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0d, 0x52, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x74, 0x6f, 0x70,
|
||||
0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x53, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04,
|
||||
0x44, 0x61, 0x74, 0x61, 0x2a, 0x83, 0x01, 0x0a, 0x0e, 0x44, 0x4e, 0x53, 0x4d, 0x65, 0x73, 0x73,
|
||||
0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x4e, 0x4f, 0x50, 0x10, 0x00,
|
||||
0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x49, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f,
|
||||
0x4c, 0x4c, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x04, 0x12,
|
||||
0x0c, 0x0a, 0x08, 0x4d, 0x41, 0x4e, 0x49, 0x46, 0x45, 0x53, 0x54, 0x10, 0x06, 0x12, 0x13, 0x0a,
|
||||
0x0f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x4f, 0x5f, 0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e, 0x54,
|
||||
0x10, 0x07, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x52, 0x4f, 0x4d, 0x5f,
|
||||
0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e, 0x54, 0x10, 0x08, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x45,
|
||||
0x41, 0x52, 0x10, 0x09, 0x22, 0x04, 0x08, 0x01, 0x10, 0x01, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69,
|
||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x69, 0x73, 0x68, 0x6f, 0x70, 0x66,
|
||||
0x6f, 0x78, 0x2f, 0x73, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
||||
0x75, 0x66, 0x2f, 0x64, 0x6e, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
const file_dnspb_dns_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x0fdnspb/dns.proto\x12\x05dnspb\"\x99\x01\n" +
|
||||
"\n" +
|
||||
"DNSMessage\x12)\n" +
|
||||
"\x04Type\x18\x01 \x01(\x0e2\x15.dnspb.DNSMessageTypeR\x04Type\x12\x0e\n" +
|
||||
"\x02ID\x18\x02 \x01(\rR\x02ID\x12\x14\n" +
|
||||
"\x05Start\x18\x03 \x01(\rR\x05Start\x12\x12\n" +
|
||||
"\x04Stop\x18\x04 \x01(\rR\x04Stop\x12\x12\n" +
|
||||
"\x04Size\x18\x05 \x01(\rR\x04Size\x12\x12\n" +
|
||||
"\x04Data\x18\x06 \x01(\fR\x04Data*\x83\x01\n" +
|
||||
"\x0eDNSMessageType\x12\a\n" +
|
||||
"\x03NOP\x10\x00\x12\b\n" +
|
||||
"\x04INIT\x10\x02\x12\b\n" +
|
||||
"\x04POLL\x10\x03\x12\t\n" +
|
||||
"\x05CLOSE\x10\x04\x12\f\n" +
|
||||
"\bMANIFEST\x10\x06\x12\x13\n" +
|
||||
"\x0fDATA_TO_IMPLANT\x10\a\x12\x15\n" +
|
||||
"\x11DATA_FROM_IMPLANT\x10\b\x12\t\n" +
|
||||
"\x05CLEAR\x10\t\"\x04\b\x01\x10\x01B,Z*github.com/bishopfox/sliver/protobuf/dnspbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_dnspb_dns_proto_rawDescOnce sync.Once
|
||||
file_dnspb_dns_proto_rawDescData = file_dnspb_dns_proto_rawDesc
|
||||
file_dnspb_dns_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_dnspb_dns_proto_rawDescGZIP() []byte {
|
||||
file_dnspb_dns_proto_rawDescOnce.Do(func() {
|
||||
file_dnspb_dns_proto_rawDescData = protoimpl.X.CompressGZIP(file_dnspb_dns_proto_rawDescData)
|
||||
file_dnspb_dns_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_dnspb_dns_proto_rawDesc), len(file_dnspb_dns_proto_rawDesc)))
|
||||
})
|
||||
return file_dnspb_dns_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_dnspb_dns_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_dnspb_dns_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_dnspb_dns_proto_goTypes = []interface{}{
|
||||
var file_dnspb_dns_proto_goTypes = []any{
|
||||
(DNSMessageType)(0), // 0: dnspb.DNSMessageType
|
||||
(*DNSMessage)(nil), // 1: dnspb.DNSMessage
|
||||
}
|
||||
@@ -234,25 +226,11 @@ func file_dnspb_dns_proto_init() {
|
||||
if File_dnspb_dns_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_dnspb_dns_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*DNSMessage); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_dnspb_dns_proto_rawDesc,
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_dnspb_dns_proto_rawDesc), len(file_dnspb_dns_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
@@ -264,7 +242,6 @@ func file_dnspb_dns_proto_init() {
|
||||
MessageInfos: file_dnspb_dns_proto_msgTypes,
|
||||
}.Build()
|
||||
File_dnspb_dns_proto = out.File
|
||||
file_dnspb_dns_proto_rawDesc = nil
|
||||
file_dnspb_dns_proto_goTypes = nil
|
||||
file_dnspb_dns_proto_depIdxs = nil
|
||||
}
|
||||
+827
-1321
File diff suppressed because it is too large.
Load diff
@@ -31,6 +31,17 @@ service SliverRPC {
|
||||
rpc MonitorListConfig(commonpb.Empty) returns (clientpb.MonitoringProviders);
|
||||
rpc MonitorAddConfig(clientpb.MonitoringProvider) returns (commonpb.Response);
|
||||
rpc MonitorDelConfig(clientpb.MonitoringProvider) returns (commonpb.Response);
|
||||
|
||||
// *** AI ***
|
||||
rpc GetAIProviders(commonpb.Empty) returns (clientpb.AIProviderConfigs);
|
||||
rpc GetAIConversations(commonpb.Empty) returns (clientpb.AIConversations);
|
||||
rpc GetAIConversation(clientpb.AIConversationReq) returns (clientpb.AIConversation);
|
||||
rpc SaveAIConversation(clientpb.AIConversation) returns (clientpb.AIConversation);
|
||||
rpc DeleteAIConversation(clientpb.AIConversationReq) returns (commonpb.Empty);
|
||||
rpc GetAIConversationMessages(clientpb.AIConversationReq)
|
||||
returns (clientpb.AIConversationMessages);
|
||||
rpc SaveAIConversationMessage(clientpb.AIConversationMessage)
|
||||
returns (clientpb.AIConversationMessage);
|
||||
|
||||
// *** Listeners ***
|
||||
rpc StartMTLSListener(clientpb.MTLSListenerReq) returns (clientpb.ListenerJob);
|
||||
|
||||
+1276
-807
File diff suppressed because it is too large.
Load diff
+2724
-6088
File diff suppressed because it is too large.
Load diff
@@ -883,6 +883,7 @@ message PortfwdReq {
|
||||
int32 Protocol = 2;
|
||||
string Host = 3;
|
||||
|
||||
int32 KeepAlive = 4;
|
||||
uint64 TunnelID = 8 [jstype = JS_STRING]; // Bind to this tunnel
|
||||
commonpb.Request Request = 9;
|
||||
}
|
||||
@@ -1185,6 +1186,7 @@ message RportFwdStartListenerReq {
|
||||
uint32 BindPort = 2;
|
||||
uint32 ForwardPort = 3;
|
||||
string ForwardAddress = 4;
|
||||
int32 KeepAlive = 5;
|
||||
|
||||
commonpb.Request Request = 9;
|
||||
}
|
||||
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package ai
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/configs"
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderAnthropic = "anthropic"
|
||||
ProviderGoogle = "google"
|
||||
ProviderOpenAI = "openai"
|
||||
ProviderOpenAICompat = "openai-compat"
|
||||
ProviderOpenRouter = "openrouter"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnsupportedProvider - Returned when an AI provider is unknown.
|
||||
ErrUnsupportedProvider = errors.New("unsupported AI provider")
|
||||
)
|
||||
|
||||
// Provider - Provider metadata derived from server configuration.
|
||||
type Provider struct {
|
||||
Name string
|
||||
Config *configs.AIProviderConfig
|
||||
}
|
||||
|
||||
// SupportedProviders - Returns the currently supported server-side AI providers.
|
||||
func SupportedProviders() []string {
|
||||
return []string{
|
||||
ProviderAnthropic,
|
||||
ProviderGoogle,
|
||||
ProviderOpenAI,
|
||||
ProviderOpenAICompat,
|
||||
ProviderOpenRouter,
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeProviderName - Convert user supplied provider names to canonical identifiers.
|
||||
func NormalizeProviderName(name string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(name))
|
||||
switch normalized {
|
||||
case "gemini":
|
||||
return ProviderGoogle
|
||||
case "openai_compat", "openaicompat", "openai-compatible":
|
||||
return ProviderOpenAICompat
|
||||
default:
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
// IsSupportedProvider - Indicates whether the provider is supported by the server scaffolding.
|
||||
func IsSupportedProvider(name string) bool {
|
||||
switch NormalizeProviderName(name) {
|
||||
case ProviderAnthropic, ProviderGoogle, ProviderOpenAI, ProviderOpenAICompat, ProviderOpenRouter:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ProviderFromConfig - Resolve a configured provider by name.
|
||||
func ProviderFromConfig(name string) (*Provider, error) {
|
||||
cfg := configs.GetServerConfig()
|
||||
if cfg == nil || cfg.AI == nil {
|
||||
return nil, ErrUnsupportedProvider
|
||||
}
|
||||
|
||||
switch NormalizeProviderName(name) {
|
||||
case ProviderAnthropic:
|
||||
return &Provider{Name: ProviderAnthropic, Config: cfg.AI.Anthropic}, nil
|
||||
case ProviderGoogle:
|
||||
return &Provider{Name: ProviderGoogle, Config: cfg.AI.Google}, nil
|
||||
case ProviderOpenAI:
|
||||
return &Provider{Name: ProviderOpenAI, Config: cfg.AI.OpenAI}, nil
|
||||
case ProviderOpenAICompat:
|
||||
return &Provider{Name: ProviderOpenAICompat, Config: cfg.AI.OpenAICompat}, nil
|
||||
case ProviderOpenRouter:
|
||||
return &Provider{Name: ProviderOpenRouter, Config: cfg.AI.OpenRouter}, nil
|
||||
default:
|
||||
return nil, ErrUnsupportedProvider
|
||||
}
|
||||
}
|
||||
|
||||
// ConfiguredProviders - Return provider availability without exposing configured secrets.
|
||||
func ConfiguredProviders() []*clientpb.AIProviderConfig {
|
||||
return ConfiguredProvidersFromConfig(configs.GetServerConfig())
|
||||
}
|
||||
|
||||
// ConfiguredProvidersFromConfig - Return provider availability for a specific config.
|
||||
func ConfiguredProvidersFromConfig(cfg *configs.ServerConfig) []*clientpb.AIProviderConfig {
|
||||
providers := make([]*clientpb.AIProviderConfig, 0, len(SupportedProviders()))
|
||||
for _, name := range SupportedProviders() {
|
||||
providerConfig := aiProviderConfig(cfg, name)
|
||||
providers = append(providers, &clientpb.AIProviderConfig{
|
||||
Name: name,
|
||||
Configured: isProviderConfigured(name, providerConfig),
|
||||
})
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
// SafeConfigSummary - Return the client-safe AI configuration summary.
|
||||
func SafeConfigSummary() *clientpb.AIConfigSummary {
|
||||
return SafeConfigSummaryFromConfig(configs.GetServerConfig())
|
||||
}
|
||||
|
||||
// SafeConfigSummaryFromConfig - Return the client-safe AI configuration summary for a specific config.
|
||||
func SafeConfigSummaryFromConfig(cfg *configs.ServerConfig) *clientpb.AIConfigSummary {
|
||||
summary := &clientpb.AIConfigSummary{}
|
||||
if cfg != nil && cfg.AI != nil {
|
||||
summary.Model = strings.TrimSpace(cfg.AI.Model)
|
||||
summary.ThinkingLevel = strings.TrimSpace(cfg.AI.ThinkingLevel)
|
||||
}
|
||||
|
||||
provider, providerConfig := selectedProviderConfig(cfg)
|
||||
summary.Provider = provider
|
||||
|
||||
switch {
|
||||
case cfg == nil || cfg.AI == nil:
|
||||
summary.Error = "server AI is not configured; run `ai-config` on the server"
|
||||
case provider == "":
|
||||
summary.Error = "server AI is missing a configured provider; run `ai-config` on the server"
|
||||
case !isProviderConfigured(provider, providerConfig):
|
||||
summary.Error = missingProviderConfigError(provider)
|
||||
default:
|
||||
summary.Valid = true
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
func selectedProviderConfig(cfg *configs.ServerConfig) (string, *configs.AIProviderConfig) {
|
||||
if cfg == nil || cfg.AI == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
explicitProvider := NormalizeProviderName(cfg.AI.Provider)
|
||||
if IsSupportedProvider(explicitProvider) {
|
||||
return explicitProvider, aiProviderConfig(cfg, explicitProvider)
|
||||
}
|
||||
|
||||
for _, provider := range SupportedProviders() {
|
||||
providerConfig := aiProviderConfig(cfg, provider)
|
||||
if isProviderConfigured(provider, providerConfig) {
|
||||
return provider, providerConfig
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func aiProviderConfig(cfg *configs.ServerConfig, provider string) *configs.AIProviderConfig {
|
||||
if cfg == nil || cfg.AI == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch NormalizeProviderName(provider) {
|
||||
case ProviderAnthropic:
|
||||
return cfg.AI.Anthropic
|
||||
case ProviderGoogle:
|
||||
return cfg.AI.Google
|
||||
case ProviderOpenAI:
|
||||
return cfg.AI.OpenAI
|
||||
case ProviderOpenAICompat:
|
||||
return cfg.AI.OpenAICompat
|
||||
case ProviderOpenRouter:
|
||||
return cfg.AI.OpenRouter
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func isProviderConfigured(provider string, providerConfig *configs.AIProviderConfig) bool {
|
||||
if providerConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch NormalizeProviderName(provider) {
|
||||
case ProviderAnthropic:
|
||||
return strings.TrimSpace(providerConfig.APIKey) != "" ||
|
||||
providerConfig.UseBedrock ||
|
||||
(strings.TrimSpace(providerConfig.Project) != "" && strings.TrimSpace(providerConfig.Location) != "")
|
||||
case ProviderGoogle:
|
||||
return strings.TrimSpace(providerConfig.APIKey) != "" ||
|
||||
(strings.TrimSpace(providerConfig.Project) != "" && strings.TrimSpace(providerConfig.Location) != "")
|
||||
case ProviderOpenAI:
|
||||
return strings.TrimSpace(providerConfig.APIKey) != ""
|
||||
case ProviderOpenAICompat:
|
||||
return strings.TrimSpace(providerConfig.BaseURL) != ""
|
||||
case ProviderOpenRouter:
|
||||
return strings.TrimSpace(providerConfig.APIKey) != ""
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func missingProviderConfigError(provider string) string {
|
||||
switch NormalizeProviderName(provider) {
|
||||
case ProviderAnthropic:
|
||||
return "server AI provider \"anthropic\" needs an API key, Bedrock mode, or a Vertex project/location; run `ai-config` on the server"
|
||||
case ProviderGoogle:
|
||||
return "server AI provider \"google\" needs a Gemini API key or a Vertex project/location; run `ai-config` on the server"
|
||||
case ProviderOpenAI:
|
||||
return "server AI provider \"openai\" is missing an API key; run `ai-config` on the server"
|
||||
case ProviderOpenAICompat:
|
||||
return "server AI provider \"openai-compat\" is missing a base URL; run `ai-config` on the server"
|
||||
case ProviderOpenRouter:
|
||||
return "server AI provider \"openrouter\" is missing an API key; run `ai-config` on the server"
|
||||
default:
|
||||
return fmt.Sprintf("server AI provider %q is not fully configured; run `ai-config` on the server", provider)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/server/configs"
|
||||
)
|
||||
|
||||
func TestSafeConfigSummaryFromConfigUsesExplicitConfiguredProvider(t *testing.T) {
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
Provider: ProviderOpenAI,
|
||||
Model: "gpt-test",
|
||||
ThinkingLevel: "high",
|
||||
OpenAI: &configs.AIProviderConfig{APIKey: "openai-key"},
|
||||
Anthropic: &configs.AIProviderConfig{APIKey: "anthropic-key"},
|
||||
},
|
||||
}
|
||||
|
||||
summary := SafeConfigSummaryFromConfig(cfg)
|
||||
if !summary.GetValid() {
|
||||
t.Fatalf("expected config to be valid, got error %q", summary.GetError())
|
||||
}
|
||||
if summary.GetProvider() != ProviderOpenAI {
|
||||
t.Fatalf("expected provider %q, got %q", ProviderOpenAI, summary.GetProvider())
|
||||
}
|
||||
if summary.GetModel() != "gpt-test" {
|
||||
t.Fatalf("expected model %q, got %q", "gpt-test", summary.GetModel())
|
||||
}
|
||||
if summary.GetThinkingLevel() != "high" {
|
||||
t.Fatalf("expected thinking level %q, got %q", "high", summary.GetThinkingLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeConfigSummaryFromConfigFallsBackToConfiguredProvider(t *testing.T) {
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
OpenAI: &configs.AIProviderConfig{},
|
||||
OpenAICompat: &configs.AIProviderConfig{},
|
||||
OpenRouter: &configs.AIProviderConfig{},
|
||||
Google: &configs.AIProviderConfig{},
|
||||
Anthropic: &configs.AIProviderConfig{APIKey: "anthropic-key"},
|
||||
},
|
||||
}
|
||||
|
||||
summary := SafeConfigSummaryFromConfig(cfg)
|
||||
if !summary.GetValid() {
|
||||
t.Fatalf("expected fallback config to be valid, got error %q", summary.GetError())
|
||||
}
|
||||
if summary.GetProvider() != ProviderAnthropic {
|
||||
t.Fatalf("expected provider fallback %q, got %q", ProviderAnthropic, summary.GetProvider())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeConfigSummaryFromConfigRejectsExplicitProviderWithoutAPIKey(t *testing.T) {
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
Provider: ProviderOpenAI,
|
||||
OpenAI: &configs.AIProviderConfig{},
|
||||
Anthropic: &configs.AIProviderConfig{APIKey: "anthropic-key"},
|
||||
},
|
||||
}
|
||||
|
||||
summary := SafeConfigSummaryFromConfig(cfg)
|
||||
if summary.GetValid() {
|
||||
t.Fatal("expected config to be invalid")
|
||||
}
|
||||
if summary.GetProvider() != ProviderOpenAI {
|
||||
t.Fatalf("expected provider %q, got %q", ProviderOpenAI, summary.GetProvider())
|
||||
}
|
||||
expected := "server AI provider \"openai\" is missing an API key; run `ai-config` on the server"
|
||||
if summary.GetError() != expected {
|
||||
t.Fatalf("expected error %q, got %q", expected, summary.GetError())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeConfigSummaryFromConfigAcceptsGoogleVertexConfiguration(t *testing.T) {
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
Provider: ProviderGoogle,
|
||||
Google: &configs.AIProviderConfig{
|
||||
Project: "vertex-project",
|
||||
Location: "us-central1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
summary := SafeConfigSummaryFromConfig(cfg)
|
||||
if !summary.GetValid() {
|
||||
t.Fatalf("expected google vertex config to be valid, got error %q", summary.GetError())
|
||||
}
|
||||
if summary.GetProvider() != ProviderGoogle {
|
||||
t.Fatalf("expected provider %q, got %q", ProviderGoogle, summary.GetProvider())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeConfigSummaryFromConfigAcceptsOpenAICompatBaseURL(t *testing.T) {
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
Provider: ProviderOpenAICompat,
|
||||
OpenAICompat: &configs.AIProviderConfig{
|
||||
BaseURL: "http://127.0.0.1:11434/v1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
summary := SafeConfigSummaryFromConfig(cfg)
|
||||
if !summary.GetValid() {
|
||||
t.Fatalf("expected openai-compat config to be valid, got error %q", summary.GetError())
|
||||
}
|
||||
if summary.GetProvider() != ProviderOpenAICompat {
|
||||
t.Fatalf("expected provider %q, got %q", ProviderOpenAICompat, summary.GetProvider())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredProvidersFromConfigReportsAvailabilityOnly(t *testing.T) {
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
Anthropic: &configs.AIProviderConfig{},
|
||||
Google: &configs.AIProviderConfig{Project: "vertex-project", Location: "us-central1"},
|
||||
OpenAI: &configs.AIProviderConfig{APIKey: "openai-key"},
|
||||
OpenAICompat: &configs.AIProviderConfig{BaseURL: "http://127.0.0.1:8080/v1"},
|
||||
OpenRouter: &configs.AIProviderConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
providers := ConfiguredProvidersFromConfig(cfg)
|
||||
if len(providers) != len(SupportedProviders()) {
|
||||
t.Fatalf("expected %d providers, got %d", len(SupportedProviders()), len(providers))
|
||||
}
|
||||
if providers[0].GetName() != ProviderAnthropic || providers[0].GetConfigured() {
|
||||
t.Fatalf("expected anthropic to be present and unconfigured, got %+v", providers[0])
|
||||
}
|
||||
if providers[1].GetName() != ProviderGoogle || !providers[1].GetConfigured() {
|
||||
t.Fatalf("expected google to be present and configured, got %+v", providers[1])
|
||||
}
|
||||
if providers[2].GetName() != ProviderOpenAI || !providers[2].GetConfigured() {
|
||||
t.Fatalf("expected openai to be present and configured, got %+v", providers[2])
|
||||
}
|
||||
if providers[3].GetName() != ProviderOpenAICompat || !providers[3].GetConfigured() {
|
||||
t.Fatalf("expected openai-compat to be present and configured, got %+v", providers[3])
|
||||
}
|
||||
if providers[4].GetName() != ProviderOpenRouter || providers[4].GetConfigured() {
|
||||
t.Fatalf("expected openrouter to be present and unconfigured, got %+v", providers[4])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"charm.land/fantasy"
|
||||
fantasyanthropic "charm.land/fantasy/providers/anthropic"
|
||||
fantasygoogle "charm.land/fantasy/providers/google"
|
||||
fantasyopenai "charm.land/fantasy/providers/openai"
|
||||
fantasyopenaicompat "charm.land/fantasy/providers/openaicompat"
|
||||
fantasyopenrouter "charm.land/fantasy/providers/openrouter"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/configs"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultOpenAIModel = "gpt-5.2"
|
||||
defaultAnthropicModel = "claude-sonnet-4-0"
|
||||
defaultGoogleModel = "gemini-2.5-pro"
|
||||
defaultOpenRouterModel = "openai/gpt-5"
|
||||
defaultCompletionTimeout = 2 * time.Minute
|
||||
|
||||
anthropicThinkingBudgetLow = 1024
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{Timeout: defaultCompletionTimeout}
|
||||
|
||||
// SetHTTPClientForTests swaps the provider HTTP client and returns a restore function.
|
||||
func SetHTTPClientForTests(client *http.Client) func() {
|
||||
previous := httpClient
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: defaultCompletionTimeout}
|
||||
}
|
||||
httpClient = client
|
||||
return func() {
|
||||
httpClient = previous
|
||||
}
|
||||
}
|
||||
|
||||
// RuntimeConfig captures the provider settings used for a single completion run.
|
||||
type RuntimeConfig struct {
|
||||
Provider string
|
||||
Model string
|
||||
ThinkingLevel string
|
||||
MaxOutputTokens int64
|
||||
Temperature *float64
|
||||
TopP *float64
|
||||
TopK *int64
|
||||
PresencePenalty *float64
|
||||
FrequencyPenalty *float64
|
||||
|
||||
APIKey string
|
||||
BaseURL string
|
||||
Headers map[string]string
|
||||
UserAgent string
|
||||
Organization string
|
||||
Project string
|
||||
Location string
|
||||
SkipAuth bool
|
||||
UseResponsesAPI bool
|
||||
UseBedrock bool
|
||||
}
|
||||
|
||||
// Completion contains the persisted assistant response details.
|
||||
type Completion struct {
|
||||
Provider string
|
||||
Model string
|
||||
Content string
|
||||
ProviderMessageID string
|
||||
FinishReason string
|
||||
}
|
||||
|
||||
type providerMessage struct {
|
||||
Role string
|
||||
Content string
|
||||
}
|
||||
|
||||
// ResolveRuntimeConfig derives the provider settings for a stored conversation.
|
||||
func ResolveRuntimeConfig(cfg *configs.ServerConfig, conversation *clientpb.AIConversation) (*RuntimeConfig, error) {
|
||||
runtime := &RuntimeConfig{
|
||||
ThinkingLevel: normalizeThinkingLevel(cfg),
|
||||
}
|
||||
var providerConfig *configs.AIProviderConfig
|
||||
if cfg != nil && cfg.AI != nil {
|
||||
runtime.MaxOutputTokens = cfg.AI.MaxOutputTokens
|
||||
runtime.Temperature = copyOptionalFloat(cfg.AI.Temperature)
|
||||
runtime.TopP = copyOptionalFloat(cfg.AI.TopP)
|
||||
runtime.TopK = copyOptionalInt(cfg.AI.TopK)
|
||||
runtime.PresencePenalty = copyOptionalFloat(cfg.AI.PresencePenalty)
|
||||
runtime.FrequencyPenalty = copyOptionalFloat(cfg.AI.FrequencyPenalty)
|
||||
}
|
||||
|
||||
if conversation != nil {
|
||||
runtime.Provider = NormalizeProviderName(conversation.GetProvider())
|
||||
runtime.Model = strings.TrimSpace(conversation.GetModel())
|
||||
}
|
||||
|
||||
if runtime.Provider == "" {
|
||||
selectedProvider, selectedProviderConfig := selectedProviderConfig(cfg)
|
||||
runtime.Provider = selectedProvider
|
||||
providerConfig = selectedProviderConfig
|
||||
applyProviderRuntimeConfig(runtime, providerConfig)
|
||||
} else {
|
||||
if !IsSupportedProvider(runtime.Provider) {
|
||||
return runtime, fmt.Errorf("unsupported AI provider %q", runtime.Provider)
|
||||
}
|
||||
providerConfig = aiProviderConfig(cfg, runtime.Provider)
|
||||
applyProviderRuntimeConfig(runtime, providerConfig)
|
||||
}
|
||||
|
||||
if runtime.Model == "" && cfg != nil && cfg.AI != nil {
|
||||
runtime.Model = strings.TrimSpace(cfg.AI.Model)
|
||||
}
|
||||
if runtime.Model == "" {
|
||||
runtime.Model = defaultModelForProvider(runtime.Provider)
|
||||
}
|
||||
runtime.UseResponsesAPI = useResponsesAPI(runtime.Provider, providerConfig)
|
||||
|
||||
switch {
|
||||
case runtime.Provider == "":
|
||||
return runtime, fmt.Errorf("server AI provider is not configured; run `ai-config` on the server")
|
||||
case !IsSupportedProvider(runtime.Provider):
|
||||
return runtime, fmt.Errorf("unsupported AI provider %q", runtime.Provider)
|
||||
case !runtimeProviderConfigured(runtime):
|
||||
return runtime, errors.New(missingProviderConfigError(runtime.Provider))
|
||||
case runtime.Model == "":
|
||||
return runtime, fmt.Errorf("server AI provider %q is missing a model; update `ai.model` or choose a provider default", runtime.Provider)
|
||||
default:
|
||||
return runtime, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteConversation sends the stored conversation history to the provider and returns the assistant reply.
|
||||
func CompleteConversation(ctx context.Context, runtime *RuntimeConfig, conversation *clientpb.AIConversation) (*Completion, error) {
|
||||
if runtime == nil {
|
||||
return nil, fmt.Errorf("AI runtime config is required")
|
||||
}
|
||||
if conversation == nil {
|
||||
return nil, fmt.Errorf("AI conversation is required")
|
||||
}
|
||||
|
||||
systemPrompt, messages, err := conversationHistory(conversation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider, err := newFantasyProvider(runtime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
model, err := provider.LanguageModel(ctx, runtime.Model)
|
||||
if err != nil {
|
||||
return nil, formatFantasyError(runtime.Provider, err)
|
||||
}
|
||||
|
||||
response, err := model.Generate(ctx, buildFantasyCall(runtime, systemPrompt, messages))
|
||||
if err != nil {
|
||||
return nil, formatFantasyError(runtime.Provider, err)
|
||||
}
|
||||
|
||||
content := extractFantasyResponseText(response)
|
||||
if content == "" {
|
||||
return nil, fmt.Errorf("%s response did not include assistant text", runtime.Provider)
|
||||
}
|
||||
|
||||
finishReason := strings.TrimSpace(string(response.FinishReason))
|
||||
if finishReason == "" || finishReason == string(fantasy.FinishReasonUnknown) {
|
||||
finishReason = "completed"
|
||||
}
|
||||
|
||||
return &Completion{
|
||||
Provider: fallbackString(model.Provider(), runtime.Provider),
|
||||
Model: fallbackString(model.Model(), runtime.Model),
|
||||
Content: content,
|
||||
ProviderMessageID: extractProviderMessageID(response),
|
||||
FinishReason: finishReason,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newFantasyProvider(runtime *RuntimeConfig) (fantasy.Provider, error) {
|
||||
if runtime == nil {
|
||||
return nil, fmt.Errorf("AI runtime config is required")
|
||||
}
|
||||
|
||||
headers := providerHeaders(runtime)
|
||||
|
||||
switch runtime.Provider {
|
||||
case ProviderAnthropic:
|
||||
opts := []fantasyanthropic.Option{
|
||||
fantasyanthropic.WithName(ProviderAnthropic),
|
||||
fantasyanthropic.WithHTTPClient(httpClient),
|
||||
}
|
||||
if runtime.APIKey != "" {
|
||||
opts = append(opts, fantasyanthropic.WithAPIKey(runtime.APIKey))
|
||||
}
|
||||
if runtime.BaseURL != "" {
|
||||
opts = append(opts, fantasyanthropic.WithBaseURL(runtime.BaseURL))
|
||||
}
|
||||
if len(headers) > 0 {
|
||||
opts = append(opts, fantasyanthropic.WithHeaders(headers))
|
||||
}
|
||||
if runtime.Project != "" && runtime.Location != "" {
|
||||
opts = append(opts, fantasyanthropic.WithVertex(runtime.Project, runtime.Location))
|
||||
}
|
||||
if runtime.SkipAuth {
|
||||
opts = append(opts, fantasyanthropic.WithSkipAuth(true))
|
||||
}
|
||||
if runtime.UseBedrock {
|
||||
opts = append(opts, fantasyanthropic.WithBedrock())
|
||||
}
|
||||
return fantasyanthropic.New(opts...)
|
||||
case ProviderGoogle:
|
||||
opts := []fantasygoogle.Option{
|
||||
fantasygoogle.WithName(ProviderGoogle),
|
||||
fantasygoogle.WithHTTPClient(httpClient),
|
||||
}
|
||||
if runtime.BaseURL != "" {
|
||||
opts = append(opts, fantasygoogle.WithBaseURL(runtime.BaseURL))
|
||||
}
|
||||
if len(headers) > 0 {
|
||||
opts = append(opts, fantasygoogle.WithHeaders(headers))
|
||||
}
|
||||
if runtime.APIKey != "" {
|
||||
opts = append(opts, fantasygoogle.WithGeminiAPIKey(runtime.APIKey))
|
||||
} else if runtime.Project != "" && runtime.Location != "" {
|
||||
opts = append(opts, fantasygoogle.WithVertex(runtime.Project, runtime.Location))
|
||||
}
|
||||
if runtime.SkipAuth {
|
||||
opts = append(opts, fantasygoogle.WithSkipAuth(true))
|
||||
}
|
||||
return fantasygoogle.New(opts...)
|
||||
case ProviderOpenAI:
|
||||
opts := []fantasyopenai.Option{
|
||||
fantasyopenai.WithName(ProviderOpenAI),
|
||||
fantasyopenai.WithHTTPClient(httpClient),
|
||||
}
|
||||
if runtime.APIKey != "" {
|
||||
opts = append(opts, fantasyopenai.WithAPIKey(runtime.APIKey))
|
||||
}
|
||||
if runtime.BaseURL != "" {
|
||||
opts = append(opts, fantasyopenai.WithBaseURL(runtime.BaseURL))
|
||||
}
|
||||
if len(headers) > 0 {
|
||||
opts = append(opts, fantasyopenai.WithHeaders(headers))
|
||||
}
|
||||
if runtime.Organization != "" {
|
||||
opts = append(opts, fantasyopenai.WithOrganization(runtime.Organization))
|
||||
}
|
||||
if runtime.Project != "" {
|
||||
opts = append(opts, fantasyopenai.WithProject(runtime.Project))
|
||||
}
|
||||
if runtime.UseResponsesAPI {
|
||||
opts = append(opts, fantasyopenai.WithUseResponsesAPI())
|
||||
}
|
||||
return fantasyopenai.New(opts...)
|
||||
case ProviderOpenAICompat:
|
||||
opts := []fantasyopenaicompat.Option{
|
||||
fantasyopenaicompat.WithName(ProviderOpenAICompat),
|
||||
fantasyopenaicompat.WithHTTPClient(httpClient),
|
||||
}
|
||||
if runtime.APIKey != "" {
|
||||
opts = append(opts, fantasyopenaicompat.WithAPIKey(runtime.APIKey))
|
||||
}
|
||||
if runtime.BaseURL != "" {
|
||||
opts = append(opts, fantasyopenaicompat.WithBaseURL(runtime.BaseURL))
|
||||
}
|
||||
if len(headers) > 0 {
|
||||
opts = append(opts, fantasyopenaicompat.WithHeaders(headers))
|
||||
}
|
||||
if runtime.UseResponsesAPI {
|
||||
opts = append(opts, fantasyopenaicompat.WithUseResponsesAPI())
|
||||
}
|
||||
return fantasyopenaicompat.New(opts...)
|
||||
case ProviderOpenRouter:
|
||||
opts := []fantasyopenrouter.Option{
|
||||
fantasyopenrouter.WithName(ProviderOpenRouter),
|
||||
fantasyopenrouter.WithHTTPClient(httpClient),
|
||||
}
|
||||
if runtime.APIKey != "" {
|
||||
opts = append(opts, fantasyopenrouter.WithAPIKey(runtime.APIKey))
|
||||
}
|
||||
if len(headers) > 0 {
|
||||
opts = append(opts, fantasyopenrouter.WithHeaders(headers))
|
||||
}
|
||||
return fantasyopenrouter.New(opts...)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported AI provider %q", runtime.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
func buildFantasyCall(runtime *RuntimeConfig, systemPrompt string, messages []providerMessage) fantasy.Call {
|
||||
prompt := make(fantasy.Prompt, 0, len(messages)+1)
|
||||
if strings.TrimSpace(systemPrompt) != "" {
|
||||
prompt = append(prompt, fantasy.Message{
|
||||
Role: fantasy.MessageRoleSystem,
|
||||
Content: []fantasy.MessagePart{
|
||||
fantasy.TextPart{Text: strings.TrimSpace(systemPrompt)},
|
||||
},
|
||||
})
|
||||
}
|
||||
for _, message := range messages {
|
||||
prompt = append(prompt, fantasy.Message{
|
||||
Role: toFantasyMessageRole(message.Role),
|
||||
Content: []fantasy.MessagePart{
|
||||
fantasy.TextPart{Text: message.Content},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
call := fantasy.Call{
|
||||
Prompt: prompt,
|
||||
ProviderOptions: fantasyProviderOptions(runtime),
|
||||
MaxOutputTokens: optionalRuntimeInt(runtime.MaxOutputTokens),
|
||||
Temperature: copyOptionalFloat(runtime.Temperature),
|
||||
TopP: copyOptionalFloat(runtime.TopP),
|
||||
TopK: copyOptionalInt(runtime.TopK),
|
||||
PresencePenalty: copyOptionalFloat(runtime.PresencePenalty),
|
||||
FrequencyPenalty: copyOptionalFloat(runtime.FrequencyPenalty),
|
||||
}
|
||||
return call
|
||||
}
|
||||
|
||||
func fantasyProviderOptions(runtime *RuntimeConfig) fantasy.ProviderOptions {
|
||||
if runtime == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch runtime.Provider {
|
||||
case ProviderAnthropic:
|
||||
if budget := anthropicThinkingBudget(runtime.ThinkingLevel); budget > 0 {
|
||||
return fantasyanthropic.NewProviderOptions(&fantasyanthropic.ProviderOptions{
|
||||
Thinking: &fantasyanthropic.ThinkingProviderOption{BudgetTokens: budget},
|
||||
})
|
||||
}
|
||||
case ProviderGoogle:
|
||||
if thinkingConfig := googleThinkingConfig(runtime.ThinkingLevel); thinkingConfig != nil {
|
||||
return fantasy.ProviderOptions{
|
||||
fantasygoogle.Name: &fantasygoogle.ProviderOptions{
|
||||
ThinkingConfig: thinkingConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
case ProviderOpenAI:
|
||||
if effort := openAIReasoningEffort(runtime.ThinkingLevel); effort != nil {
|
||||
if runtime.UseResponsesAPI && fantasyopenai.IsResponsesModel(runtime.Model) {
|
||||
return fantasyopenai.NewResponsesProviderOptions(&fantasyopenai.ResponsesProviderOptions{
|
||||
ReasoningEffort: effort,
|
||||
})
|
||||
}
|
||||
return fantasyopenai.NewProviderOptions(&fantasyopenai.ProviderOptions{
|
||||
ReasoningEffort: effort,
|
||||
})
|
||||
}
|
||||
case ProviderOpenAICompat:
|
||||
if effort := openAIReasoningEffort(runtime.ThinkingLevel); effort != nil {
|
||||
return fantasyopenaicompat.NewProviderOptions(&fantasyopenaicompat.ProviderOptions{
|
||||
ReasoningEffort: effort,
|
||||
})
|
||||
}
|
||||
case ProviderOpenRouter:
|
||||
if reasoning := openRouterReasoningOptions(runtime.ThinkingLevel); reasoning != nil {
|
||||
return fantasyopenrouter.NewProviderOptions(&fantasyopenrouter.ProviderOptions{
|
||||
Reasoning: reasoning,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func conversationHistory(conversation *clientpb.AIConversation) (string, []providerMessage, error) {
|
||||
if conversation == nil {
|
||||
return "", nil, fmt.Errorf("AI conversation is required")
|
||||
}
|
||||
|
||||
systemPrompt := strings.TrimSpace(conversation.GetSystemPrompt())
|
||||
messages := make([]providerMessage, 0, len(conversation.GetMessages()))
|
||||
for _, message := range conversation.GetMessages() {
|
||||
if message == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
role := strings.ToLower(strings.TrimSpace(message.GetRole()))
|
||||
content := strings.TrimSpace(message.GetContent())
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch role {
|
||||
case "user", "assistant":
|
||||
messages = append(messages, providerMessage{
|
||||
Role: role,
|
||||
Content: content,
|
||||
})
|
||||
case "system":
|
||||
if systemPrompt == "" {
|
||||
systemPrompt = content
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(messages) == 0 {
|
||||
return systemPrompt, nil, fmt.Errorf("AI conversation has no user or assistant messages")
|
||||
}
|
||||
if messages[len(messages)-1].Role != "user" {
|
||||
return systemPrompt, nil, fmt.Errorf("AI conversation is not awaiting an assistant response")
|
||||
}
|
||||
|
||||
return systemPrompt, messages, nil
|
||||
}
|
||||
|
||||
func defaultModelForProvider(provider string) string {
|
||||
switch NormalizeProviderName(provider) {
|
||||
case ProviderOpenAI:
|
||||
return defaultOpenAIModel
|
||||
case ProviderAnthropic:
|
||||
return defaultAnthropicModel
|
||||
case ProviderGoogle:
|
||||
return defaultGoogleModel
|
||||
case ProviderOpenRouter:
|
||||
return defaultOpenRouterModel
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeThinkingLevel(cfg *configs.ServerConfig) string {
|
||||
if cfg == nil || cfg.AI == nil {
|
||||
return ""
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.AI.ThinkingLevel)) {
|
||||
case "low", "medium", "high", "disabled":
|
||||
return strings.ToLower(strings.TrimSpace(cfg.AI.ThinkingLevel))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func openAIReasoningEffort(thinkingLevel string) *fantasyopenai.ReasoningEffort {
|
||||
switch strings.ToLower(strings.TrimSpace(thinkingLevel)) {
|
||||
case "low":
|
||||
return fantasyopenai.ReasoningEffortOption(fantasyopenai.ReasoningEffortLow)
|
||||
case "medium":
|
||||
return fantasyopenai.ReasoningEffortOption(fantasyopenai.ReasoningEffortMedium)
|
||||
case "high":
|
||||
return fantasyopenai.ReasoningEffortOption(fantasyopenai.ReasoningEffortHigh)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func anthropicThinkingBudget(thinkingLevel string) int64 {
|
||||
switch strings.ToLower(strings.TrimSpace(thinkingLevel)) {
|
||||
case "low":
|
||||
return anthropicThinkingBudgetLow
|
||||
case "medium":
|
||||
return anthropicThinkingBudgetLow * 2
|
||||
case "high":
|
||||
return anthropicThinkingBudgetLow * 4
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func googleThinkingConfig(thinkingLevel string) *fantasygoogle.ThinkingConfig {
|
||||
var budget int64
|
||||
switch strings.ToLower(strings.TrimSpace(thinkingLevel)) {
|
||||
case "low":
|
||||
budget = 1024
|
||||
case "medium":
|
||||
budget = 2048
|
||||
case "high":
|
||||
budget = 4096
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return &fantasygoogle.ThinkingConfig{
|
||||
ThinkingBudget: fantasy.Opt(budget),
|
||||
}
|
||||
}
|
||||
|
||||
func openRouterReasoningOptions(thinkingLevel string) *fantasyopenrouter.ReasoningOptions {
|
||||
switch strings.ToLower(strings.TrimSpace(thinkingLevel)) {
|
||||
case "low":
|
||||
return &fantasyopenrouter.ReasoningOptions{
|
||||
Enabled: fantasy.Opt(true),
|
||||
Effort: fantasyopenrouter.ReasoningEffortOption(fantasyopenrouter.ReasoningEffortLow),
|
||||
}
|
||||
case "medium":
|
||||
return &fantasyopenrouter.ReasoningOptions{
|
||||
Enabled: fantasy.Opt(true),
|
||||
Effort: fantasyopenrouter.ReasoningEffortOption(fantasyopenrouter.ReasoningEffortMedium),
|
||||
}
|
||||
case "high":
|
||||
return &fantasyopenrouter.ReasoningOptions{
|
||||
Enabled: fantasy.Opt(true),
|
||||
Effort: fantasyopenrouter.ReasoningEffortOption(fantasyopenrouter.ReasoningEffortHigh),
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func applyProviderRuntimeConfig(runtime *RuntimeConfig, providerConfig *configs.AIProviderConfig) {
|
||||
if runtime == nil || providerConfig == nil {
|
||||
return
|
||||
}
|
||||
runtime.APIKey = strings.TrimSpace(providerConfig.APIKey)
|
||||
runtime.BaseURL = strings.TrimSpace(providerConfig.BaseURL)
|
||||
runtime.Headers = copyStringMap(providerConfig.Headers)
|
||||
runtime.UserAgent = strings.TrimSpace(providerConfig.UserAgent)
|
||||
runtime.Organization = strings.TrimSpace(providerConfig.Organization)
|
||||
runtime.Project = strings.TrimSpace(providerConfig.Project)
|
||||
runtime.Location = strings.TrimSpace(providerConfig.Location)
|
||||
runtime.SkipAuth = providerConfig.SkipAuth
|
||||
if providerConfig.UseResponsesAPI != nil {
|
||||
runtime.UseResponsesAPI = *providerConfig.UseResponsesAPI
|
||||
}
|
||||
runtime.UseBedrock = providerConfig.UseBedrock
|
||||
}
|
||||
|
||||
func useResponsesAPI(provider string, providerConfig *configs.AIProviderConfig) bool {
|
||||
if providerConfig != nil && providerConfig.UseResponsesAPI != nil {
|
||||
return *providerConfig.UseResponsesAPI
|
||||
}
|
||||
return NormalizeProviderName(provider) == ProviderOpenAI
|
||||
}
|
||||
|
||||
func providerHeaders(runtime *RuntimeConfig) map[string]string {
|
||||
if runtime == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(runtime.Headers) == 0 && runtime.UserAgent == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
headers := make(map[string]string, len(runtime.Headers)+1)
|
||||
for key, value := range runtime.Headers {
|
||||
if strings.TrimSpace(key) == "" || strings.TrimSpace(value) == "" {
|
||||
continue
|
||||
}
|
||||
headers[key] = value
|
||||
}
|
||||
if runtime.UserAgent != "" {
|
||||
headers["User-Agent"] = runtime.UserAgent
|
||||
}
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func runtimeProviderConfigured(runtime *RuntimeConfig) bool {
|
||||
if runtime == nil {
|
||||
return false
|
||||
}
|
||||
switch runtime.Provider {
|
||||
case ProviderAnthropic:
|
||||
return runtime.APIKey != "" ||
|
||||
runtime.UseBedrock ||
|
||||
(runtime.Project != "" && runtime.Location != "")
|
||||
case ProviderGoogle:
|
||||
return runtime.APIKey != "" ||
|
||||
(runtime.Project != "" && runtime.Location != "")
|
||||
case ProviderOpenAI:
|
||||
return runtime.APIKey != ""
|
||||
case ProviderOpenAICompat:
|
||||
return runtime.BaseURL != ""
|
||||
case ProviderOpenRouter:
|
||||
return runtime.APIKey != ""
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func extractFantasyResponseText(response *fantasy.Response) string {
|
||||
if response == nil {
|
||||
return ""
|
||||
}
|
||||
blocks := make([]string, 0, len(response.Content))
|
||||
for _, content := range response.Content {
|
||||
if content == nil || content.GetType() != fantasy.ContentTypeText {
|
||||
continue
|
||||
}
|
||||
textContent, ok := fantasy.AsContentType[fantasy.TextContent](content)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(textContent.Text) == "" {
|
||||
continue
|
||||
}
|
||||
blocks = append(blocks, textContent.Text)
|
||||
}
|
||||
return joinTextBlocks(blocks)
|
||||
}
|
||||
|
||||
func extractProviderMessageID(_ *fantasy.Response) string {
|
||||
// Fantasy's unified response model does not currently expose a stable top-level
|
||||
// provider message ID for plain text generations.
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatFantasyError(provider string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var providerErr *fantasy.ProviderError
|
||||
if errors.As(err, &providerErr) {
|
||||
message := strings.TrimSpace(providerErr.Error())
|
||||
if providerErr.StatusCode > 0 {
|
||||
if message == "" {
|
||||
message = http.StatusText(providerErr.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("%s API request failed with HTTP %d: %s", provider, providerErr.StatusCode, truncateForError(message))
|
||||
}
|
||||
if message != "" {
|
||||
return fmt.Errorf("%s API request failed: %s", provider, truncateForError(message))
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func toFantasyMessageRole(role string) fantasy.MessageRole {
|
||||
switch strings.ToLower(strings.TrimSpace(role)) {
|
||||
case "assistant":
|
||||
return fantasy.MessageRoleAssistant
|
||||
default:
|
||||
return fantasy.MessageRoleUser
|
||||
}
|
||||
}
|
||||
|
||||
func optionalRuntimeInt(value int64) *int64 {
|
||||
if value <= 0 {
|
||||
return nil
|
||||
}
|
||||
return fantasy.Opt(value)
|
||||
}
|
||||
|
||||
func copyOptionalFloat(value *float64) *float64 {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
normalized := *value
|
||||
return &normalized
|
||||
}
|
||||
|
||||
func copyOptionalInt(value *int64) *int64 {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
normalized := *value
|
||||
return &normalized
|
||||
}
|
||||
|
||||
func copyStringMap(values map[string]string) map[string]string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
copied := make(map[string]string, len(values))
|
||||
for key, value := range values {
|
||||
key = strings.TrimSpace(key)
|
||||
value = strings.TrimSpace(value)
|
||||
if key == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
copied[key] = value
|
||||
}
|
||||
if len(copied) == 0 {
|
||||
return nil
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
func joinTextBlocks(blocks []string) string {
|
||||
var filtered []string
|
||||
for _, block := range blocks {
|
||||
block = strings.TrimSpace(block)
|
||||
if block == "" {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, block)
|
||||
}
|
||||
return strings.Join(filtered, "\n\n")
|
||||
}
|
||||
|
||||
func truncateForError(value string) string {
|
||||
const maxLen = 512
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) <= maxLen {
|
||||
return value
|
||||
}
|
||||
return strings.TrimSpace(value[:maxLen]) + "..."
|
||||
}
|
||||
|
||||
func fallbackString(value string, fallback string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/configs"
|
||||
)
|
||||
|
||||
func TestCompleteConversationOpenAIUsesConfiguredCredentialsAndSettings(t *testing.T) {
|
||||
type capturedRequest struct {
|
||||
Path string
|
||||
Authorization string
|
||||
Body string
|
||||
}
|
||||
|
||||
requests := make(chan capturedRequest, 1)
|
||||
restoreClient := SetHTTPClientForTests(&http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
defer r.Body.Close()
|
||||
|
||||
payload, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read request body: %v", err)
|
||||
}
|
||||
|
||||
requests <- capturedRequest{
|
||||
Path: r.URL.Path,
|
||||
Authorization: r.Header.Get("Authorization"),
|
||||
Body: string(payload),
|
||||
}
|
||||
|
||||
return jsonResponse(http.StatusOK, `{
|
||||
"id": "resp_123",
|
||||
"model": "gpt-5.2",
|
||||
"status": "completed",
|
||||
"output": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "output_text", "text": "OpenAI assistant reply"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`), nil
|
||||
}),
|
||||
})
|
||||
defer restoreClient()
|
||||
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
Provider: ProviderOpenAI,
|
||||
ThinkingLevel: "high",
|
||||
OpenAI: &configs.AIProviderConfig{
|
||||
APIKey: "openai-key",
|
||||
BaseURL: "https://openai.example/proxy/v1",
|
||||
UseResponsesAPI: boolPtr(true),
|
||||
},
|
||||
Anthropic: &configs.AIProviderConfig{},
|
||||
},
|
||||
}
|
||||
conversation := &clientpb.AIConversation{
|
||||
Provider: ProviderOpenAI,
|
||||
SystemPrompt: "Keep it brief.",
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "assistant", Content: "Earlier answer."},
|
||||
{Role: "user", Content: "Explain the workflow."},
|
||||
},
|
||||
}
|
||||
|
||||
runtime, err := ResolveRuntimeConfig(cfg, conversation)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve runtime config: %v", err)
|
||||
}
|
||||
if runtime.Model != defaultOpenAIModel {
|
||||
t.Fatalf("unexpected default openai model: got=%q want=%q", runtime.Model, defaultOpenAIModel)
|
||||
}
|
||||
|
||||
completion, err := CompleteConversation(context.Background(), runtime, conversation)
|
||||
if err != nil {
|
||||
t.Fatalf("complete conversation: %v", err)
|
||||
}
|
||||
|
||||
request := <-requests
|
||||
if request.Path != "/proxy/v1/responses" {
|
||||
t.Fatalf("unexpected openai request path: got=%q want=%q", request.Path, "/proxy/v1/responses")
|
||||
}
|
||||
if request.Authorization != "Bearer openai-key" {
|
||||
t.Fatalf("unexpected authorization header: %q", request.Authorization)
|
||||
}
|
||||
for _, fragment := range []string{
|
||||
`"model":"gpt-5.2"`,
|
||||
`"effort":"high"`,
|
||||
`"Keep it brief."`,
|
||||
`"Earlier answer."`,
|
||||
`"Explain the workflow."`,
|
||||
} {
|
||||
if !strings.Contains(request.Body, fragment) {
|
||||
t.Fatalf("expected openai request body to contain %q, got %s", fragment, request.Body)
|
||||
}
|
||||
}
|
||||
|
||||
if completion.Content != "OpenAI assistant reply" {
|
||||
t.Fatalf("unexpected completion content: %q", completion.Content)
|
||||
}
|
||||
if completion.ProviderMessageID != "" {
|
||||
t.Fatalf("expected fantasy-backed completion to leave provider message id empty, got %q", completion.ProviderMessageID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteConversationAnthropicUsesConfiguredCredentialsAndSettings(t *testing.T) {
|
||||
type capturedRequest struct {
|
||||
Path string
|
||||
APIKey string
|
||||
AnthropicVersion string
|
||||
Body string
|
||||
}
|
||||
|
||||
requests := make(chan capturedRequest, 1)
|
||||
restoreClient := SetHTTPClientForTests(&http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
defer r.Body.Close()
|
||||
|
||||
payload, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read request body: %v", err)
|
||||
}
|
||||
|
||||
requests <- capturedRequest{
|
||||
Path: r.URL.Path,
|
||||
APIKey: r.Header.Get("x-api-key"),
|
||||
AnthropicVersion: r.Header.Get("anthropic-version"),
|
||||
Body: string(payload),
|
||||
}
|
||||
|
||||
return jsonResponse(http.StatusOK, `{
|
||||
"id": "msg_123",
|
||||
"model": "claude-sonnet-4-0",
|
||||
"stop_reason": "end_turn",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "internal reasoning"},
|
||||
{"type": "text", "text": "Anthropic assistant reply"}
|
||||
]
|
||||
}`), nil
|
||||
}),
|
||||
})
|
||||
defer restoreClient()
|
||||
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
Provider: ProviderAnthropic,
|
||||
ThinkingLevel: "medium",
|
||||
Anthropic: &configs.AIProviderConfig{
|
||||
APIKey: "anthropic-key",
|
||||
BaseURL: "https://anthropic.example/edge",
|
||||
},
|
||||
OpenAI: &configs.AIProviderConfig{},
|
||||
},
|
||||
}
|
||||
conversation := &clientpb.AIConversation{
|
||||
Provider: ProviderAnthropic,
|
||||
SystemPrompt: "Use short answers.",
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "user", Content: "Hello"},
|
||||
{Role: "assistant", Content: "Hi there"},
|
||||
{Role: "user", Content: "What changed?"},
|
||||
},
|
||||
}
|
||||
|
||||
runtime, err := ResolveRuntimeConfig(cfg, conversation)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve runtime config: %v", err)
|
||||
}
|
||||
if runtime.Model != defaultAnthropicModel {
|
||||
t.Fatalf("unexpected default anthropic model: got=%q want=%q", runtime.Model, defaultAnthropicModel)
|
||||
}
|
||||
|
||||
completion, err := CompleteConversation(context.Background(), runtime, conversation)
|
||||
if err != nil {
|
||||
t.Fatalf("complete conversation: %v", err)
|
||||
}
|
||||
|
||||
request := <-requests
|
||||
if request.Path != "/edge/v1/messages" {
|
||||
t.Fatalf("unexpected anthropic request path: got=%q want=%q", request.Path, "/edge/v1/messages")
|
||||
}
|
||||
if request.APIKey != "anthropic-key" {
|
||||
t.Fatalf("unexpected anthropic api key header: %q", request.APIKey)
|
||||
}
|
||||
if request.AnthropicVersion == "" {
|
||||
t.Fatal("expected anthropic version header to be set")
|
||||
}
|
||||
for _, fragment := range []string{
|
||||
`"model":"claude-sonnet-4-0"`,
|
||||
`"Use short answers."`,
|
||||
`"budget_tokens":2048`,
|
||||
`"Hello"`,
|
||||
`"Hi there"`,
|
||||
`"What changed?"`,
|
||||
} {
|
||||
if !strings.Contains(request.Body, fragment) {
|
||||
t.Fatalf("expected anthropic request body to contain %q, got %s", fragment, request.Body)
|
||||
}
|
||||
}
|
||||
|
||||
if completion.Content != "Anthropic assistant reply" {
|
||||
t.Fatalf("unexpected completion content: %q", completion.Content)
|
||||
}
|
||||
if completion.FinishReason != "stop" {
|
||||
t.Fatalf("unexpected finish reason: %q", completion.FinishReason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRuntimeConfigOpenAICompatAllowsBaseURLWithoutAPIKey(t *testing.T) {
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
Provider: ProviderOpenAICompat,
|
||||
Model: "gpt-oss-120b",
|
||||
OpenAICompat: &configs.AIProviderConfig{
|
||||
BaseURL: "http://127.0.0.1:8080/v1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runtime, err := ResolveRuntimeConfig(cfg, &clientpb.AIConversation{
|
||||
Provider: ProviderOpenAICompat,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resolve runtime config: %v", err)
|
||||
}
|
||||
if runtime.Provider != ProviderOpenAICompat {
|
||||
t.Fatalf("expected provider %q, got %q", ProviderOpenAICompat, runtime.Provider)
|
||||
}
|
||||
if runtime.BaseURL != "http://127.0.0.1:8080/v1" {
|
||||
t.Fatalf("expected openai-compat base url, got %q", runtime.BaseURL)
|
||||
}
|
||||
if runtime.APIKey != "" {
|
||||
t.Fatalf("expected openai-compat api key to remain empty, got %q", runtime.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteConversationOpenAICompatUsesBaseURLWithoutAuth(t *testing.T) {
|
||||
type capturedRequest struct {
|
||||
Path string
|
||||
Authorization string
|
||||
Body string
|
||||
}
|
||||
|
||||
requests := make(chan capturedRequest, 1)
|
||||
restoreClient := SetHTTPClientForTests(&http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
defer r.Body.Close()
|
||||
|
||||
payload, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read request body: %v", err)
|
||||
}
|
||||
|
||||
requests <- capturedRequest{
|
||||
Path: r.URL.Path,
|
||||
Authorization: r.Header.Get("Authorization"),
|
||||
Body: string(payload),
|
||||
}
|
||||
|
||||
return jsonResponse(http.StatusOK, `{
|
||||
"id": "chatcmpl_123",
|
||||
"object": "chat.completion",
|
||||
"model": "gpt-oss-120b",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "OpenAI-compatible assistant reply"
|
||||
}
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 5,
|
||||
"total_tokens": 15
|
||||
}
|
||||
}`), nil
|
||||
}),
|
||||
})
|
||||
defer restoreClient()
|
||||
|
||||
cfg := &configs.ServerConfig{
|
||||
AI: &configs.AIConfig{
|
||||
Provider: ProviderOpenAICompat,
|
||||
Model: "gpt-oss-120b",
|
||||
OpenAICompat: &configs.AIProviderConfig{
|
||||
BaseURL: "http://127.0.0.1:8080/v1",
|
||||
},
|
||||
},
|
||||
}
|
||||
conversation := &clientpb.AIConversation{
|
||||
Provider: ProviderOpenAICompat,
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "user", Content: "Say hi."},
|
||||
},
|
||||
}
|
||||
|
||||
runtime, err := ResolveRuntimeConfig(cfg, conversation)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve runtime config: %v", err)
|
||||
}
|
||||
|
||||
completion, err := CompleteConversation(context.Background(), runtime, conversation)
|
||||
if err != nil {
|
||||
t.Fatalf("complete conversation: %v", err)
|
||||
}
|
||||
|
||||
request := <-requests
|
||||
if request.Path != "/v1/chat/completions" {
|
||||
t.Fatalf("unexpected openai-compatible request path: got=%q want=%q", request.Path, "/v1/chat/completions")
|
||||
}
|
||||
if request.Authorization != "" {
|
||||
t.Fatalf("expected no authorization header for unauthenticated openai-compatible endpoint, got %q", request.Authorization)
|
||||
}
|
||||
if !strings.Contains(request.Body, `"Say hi."`) {
|
||||
t.Fatalf("expected openai-compatible request body to contain the user prompt, got %s", request.Body)
|
||||
}
|
||||
if completion.Content != "OpenAI-compatible assistant reply" {
|
||||
t.Fatalf("unexpected completion content: %q", completion.Content)
|
||||
}
|
||||
}
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
func jsonResponse(statusCode int, body string) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: statusCode,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
}
|
||||
}
|
||||
|
||||
func boolPtr(value bool) *bool {
|
||||
return &value
|
||||
}
|
||||
Loaded 100 of 1712 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user