mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
95
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
593925b4a5 | ||
|
|
255477e572 | ||
|
|
0acc6911d8 | ||
|
|
4ca026906b | ||
|
|
97a249e28e | ||
|
|
9f331687d3 | ||
|
|
14d68d6fcd | ||
|
|
05c5334fea | ||
|
|
d277a14c57 | ||
|
|
a7b5dbe6ee | ||
|
|
c188dc1e69 | ||
|
|
9fc9d1762e | ||
|
|
4ef86448d0 | ||
|
|
02117a6072 | ||
|
|
c272c8abac | ||
|
|
cdf2412b39 | ||
|
|
40f52791d3 | ||
|
|
a5d8bd16b9 | ||
|
|
bf113f2151 | ||
|
|
4cd010afb9 | ||
|
|
04b0bdd199 | ||
|
|
9682db93b3 | ||
|
|
1d7ffb4ac2 | ||
|
|
c5e7cd6354 | ||
|
|
b20ba7284b | ||
|
|
49498210f8 | ||
|
|
14e9121302 | ||
|
|
8bd0257dd9 | ||
|
|
93fdfb3add | ||
|
|
e02b03cc70 | ||
|
|
6831fe50a6 | ||
|
|
405108866d | ||
|
|
4d551b71e7 | ||
|
|
a7edd9838d | ||
|
|
9ef54ca6b1 | ||
|
|
4622c50288 | ||
|
|
b1ee0355af | ||
|
|
01d9b213f2 | ||
|
|
f56886bce5 | ||
|
|
88176dfd00 | ||
|
|
45dc3ea09b | ||
|
|
682b3c2409 | ||
|
|
3a93f312b4 | ||
|
|
2ca95f7a7c | ||
|
|
d2319432ab | ||
|
|
253f1a8806 | ||
|
|
2c025d5970 | ||
|
|
f0034683f9 | ||
|
|
8e8398b654 | ||
|
|
aa02293f05 | ||
|
|
f967a32308 | ||
|
|
99ec812455 | ||
|
|
eeef542eeb | ||
|
|
6d7a10b392 | ||
|
|
ca40c0a1e7 | ||
|
|
4a5ad6ea92 | ||
|
|
266dabf6c3 | ||
|
|
a4c8968539 | ||
|
|
613c030ea3 | ||
|
|
9f6bfb6714 | ||
|
|
a764814a8e | ||
|
|
0c9ce138a0 | ||
|
|
6c1dcf6a06 | ||
|
|
fffe93819d | ||
|
|
635ee421e3 | ||
|
|
3ce6d69474 | ||
|
|
cd523c7f77 | ||
|
|
1f1b759914 | ||
|
|
b78915fcbd | ||
|
|
fa0e13a5cc | ||
|
|
924bcddfd2 | ||
|
|
affbf61452 | ||
|
|
7c6b2f70e4 | ||
|
|
c46f2938de | ||
|
|
b355fe70d6 | ||
|
|
10b1079286 | ||
|
|
9c042e6977 | ||
|
|
62583cac8f | ||
|
|
95764945b8 | ||
|
|
26abda1c15 | ||
|
|
01dd3cac72 | ||
|
|
f8faa1f767 | ||
|
|
a9e48e84cd | ||
|
|
9d6704d925 | ||
|
|
ce5157e2e9 | ||
|
|
7d19051df8 | ||
|
|
a0d3bd579f | ||
|
|
82c309632a | ||
|
|
5c0b8ec460 | ||
|
|
7791a86eb4 | ||
|
|
7ac4db3fa0 | ||
|
|
512bc822ef | ||
|
|
f2bb9c3733 | ||
|
|
0bd4f100fd | ||
|
|
3230322fd6 |
No files matched your search
@@ -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,8 @@
|
||||
/crush
|
||||
|
||||
/examples
|
||||
|
||||
/.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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
|
||||
GO ?= go
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ENV =
|
||||
TAGS ?= -tags go_sqlite
|
||||
CGO_ENABLED = 0
|
||||
@@ -12,19 +12,6 @@ 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)
|
||||
@@ -34,151 +21,294 @@ ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
|
||||
ifneq ($(OS),Windows_NT)
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_P := $(shell uname -p)
|
||||
|
||||
# Programs required for generating protobuf/grpc files
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
ifeq ($(MAKECMDGOALS), pb)
|
||||
K := $(foreach exec,$(PB_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
||||
endif
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
#
|
||||
# Targets
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
|
||||
|
||||
#
|
||||
# Targets
|
||||
# >>> WINDOWS <<<
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
else
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
SHELL := cmd.exe
|
||||
.SHELLFLAGS := /C
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
GO_VERSION := $(patsubst go%,%,$(strip $(shell $(GO) env GOVERSION)))
|
||||
GO_MAJOR_VERSION := $(word 1,$(subst ., ,$(GO_VERSION)))
|
||||
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
COMMA := ,
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
define windows_exec
|
||||
$(strip $(foreach envvar,$(1),set "$(envvar)" && ))$(2)
|
||||
endef
|
||||
|
||||
define windows_go_build
|
||||
$(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build $(4) $(TAGS)$(COMMA)$(5) $(6) -o $(7) ./$(5))
|
||||
endef
|
||||
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client)
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX).exe)
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-386$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,windows,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-386$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,freebsd,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,freebsd,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-amd64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: pb
|
||||
pb: validate-pb-compilers
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor,server,$(LDFLAGS_DEBUG),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor,client,$(LDFLAGS_DEBUG),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: validate-pb-compilers
|
||||
validate-pb-compilers:
|
||||
@for %%P in ($(PB_COMPILERS)) do @where.exe %%P >NUL || (echo Missing protobuf util %%P in PATH & exit /b 1)
|
||||
|
||||
validate-go-version:
|
||||
@if $(GO_MAJOR_VERSION) GTR $(MIN_SUPPORTED_GO_MAJOR_VERSION) (exit /b 0) else if $(GO_MAJOR_VERSION) LSS $(MIN_SUPPORTED_GO_MAJOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1) else if $(GO_MINOR_VERSION) LSS $(MIN_SUPPORTED_GO_MINOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1)
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
-rmdir /S /Q server\assets\fs\darwin\amd64
|
||||
-rmdir /S /Q server\assets\fs\darwin\arm64
|
||||
-rmdir /S /Q server\assets\fs\windows\amd64
|
||||
-rmdir /S /Q server\assets\fs\linux\amd64
|
||||
-del /Q /F server\assets\fs\*.zip 2>NUL
|
||||
-del /Q /F .downloaded_assets 2>NUL
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
|
||||
|
||||
.downloaded_assets:
|
||||
$(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,150 @@
|
||||
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)
|
||||
model.showExperimentalWarningModal()
|
||||
|
||||
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 {
|
||||
SessionID string
|
||||
BeaconID string
|
||||
Label string
|
||||
Host string
|
||||
OS string
|
||||
Arch string
|
||||
C2 string
|
||||
Mode string
|
||||
Details []string
|
||||
}
|
||||
|
||||
type aiConnectionSummary struct {
|
||||
Profile string
|
||||
Server string
|
||||
Operator string
|
||||
State string
|
||||
}
|
||||
|
||||
func 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{
|
||||
SessionID: session.ID,
|
||||
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
|
||||
Host: fallback(session.Hostname, "<unknown host>"),
|
||||
OS: fallback(session.OS, "unknown"),
|
||||
Arch: fallback(session.Arch, "unknown"),
|
||||
C2: fallback(session.ActiveC2, "unknown"),
|
||||
Mode: "interactive session",
|
||||
Details: []string{
|
||||
fmt.Sprintf("User: %s", fallback(session.Username, "<unknown>")),
|
||||
fmt.Sprintf("PID: %d", session.PID),
|
||||
fmt.Sprintf("Remote: %s", fallback(session.RemoteAddress, "<unknown>")),
|
||||
},
|
||||
}
|
||||
case beacon != nil:
|
||||
ctx.target = aiTargetSummary{
|
||||
BeaconID: beacon.ID,
|
||||
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
|
||||
Host: fallback(beacon.Hostname, "<unknown host>"),
|
||||
OS: fallback(beacon.OS, "unknown"),
|
||||
Arch: fallback(beacon.Arch, "unknown"),
|
||||
C2: fallback(beacon.ActiveC2, "unknown"),
|
||||
Mode: "asynchronous beacon",
|
||||
Details: []string{
|
||||
fmt.Sprintf("User: %s", fallback(beacon.Username, "<unknown>")),
|
||||
fmt.Sprintf("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,519 @@
|
||||
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}, aiTargetSummary{}, "")
|
||||
msg := cmd()
|
||||
|
||||
loaded, ok := msg.(aiLoadedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiLoadedMsg, got %T", msg)
|
||||
}
|
||||
if loaded.selectedID != "conv-1" {
|
||||
t.Fatalf("unexpected selected conversation id: got=%q want=%q", loaded.selectedID, "conv-1")
|
||||
}
|
||||
if loaded.conversation == nil || loaded.conversation.GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected loaded conversation: %+v", loaded.conversation)
|
||||
}
|
||||
if len(loaded.conversations) != 2 {
|
||||
t.Fatalf("unexpected conversation count: got=%d want=%d", len(loaded.conversations), 2)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if server.getProvidersCalls != 1 {
|
||||
t.Fatalf("expected GetAIProviders to be called once, got %d", server.getProvidersCalls)
|
||||
}
|
||||
if server.getConversationsCalls != 1 {
|
||||
t.Fatalf("expected GetAIConversations to be called once, got %d", server.getConversationsCalls)
|
||||
}
|
||||
if len(server.getConversationReqs) != 1 {
|
||||
t.Fatalf("expected GetAIConversation to be called once, got %d", len(server.getConversationReqs))
|
||||
}
|
||||
if server.getConversationReqs[0].GetID() != "conv-1" || !server.getConversationReqs[0].GetIncludeMessages() {
|
||||
t.Fatalf("unexpected GetAIConversation request: %+v", server.getConversationReqs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAIStateCmdCreatesConversationWhenNoneExist(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
providersResp: &clientpb.AIProviderConfigs{
|
||||
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
|
||||
Config: &clientpb.AIConfigSummary{
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
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}, aiTargetSummary{}, "")()
|
||||
|
||||
loaded, ok := msg.(aiLoadedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiLoadedMsg, got %T", msg)
|
||||
}
|
||||
if loaded.status != "Created a new AI conversation." {
|
||||
t.Fatalf("unexpected status: %q", loaded.status)
|
||||
}
|
||||
if loaded.selectedID != "conv-created" {
|
||||
t.Fatalf("unexpected selected conversation id: %q", loaded.selectedID)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
|
||||
}
|
||||
request := server.saveConversationReqs[0]
|
||||
if request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetTitle() != "New conversation" {
|
||||
t.Fatalf("unexpected SaveAIConversation request: %+v", request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitPromptCmdCreatesConversationAndSavesUserMessage(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
saveConversationResp: &clientpb.AIConversation{
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "Explain the workflow.",
|
||||
},
|
||||
saveMessageResp: &clientpb.AIConversationMessage{
|
||||
ID: "msg-1",
|
||||
ConversationID: "conv-created",
|
||||
Role: "user",
|
||||
Content: "Explain the workflow.\n\nWith details.",
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := submitPromptCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
aiTargetSummary{SessionID: "session-1"},
|
||||
nil,
|
||||
"openai",
|
||||
"gpt-test",
|
||||
"Explain the workflow.\n\nWith details.",
|
||||
)()
|
||||
|
||||
submitted, ok := msg.(aiPromptSubmittedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
|
||||
}
|
||||
if submitted.conversationID != "conv-created" {
|
||||
t.Fatalf("unexpected conversation id: %q", submitted.conversationID)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
|
||||
}
|
||||
if server.saveConversationReqs[0].GetTitle() != "Explain the workflow." {
|
||||
t.Fatalf("unexpected conversation title: %q", server.saveConversationReqs[0].GetTitle())
|
||||
}
|
||||
if server.saveConversationReqs[0].GetTargetSessionID() != "session-1" {
|
||||
t.Fatalf("expected target session to be forwarded, got %+v", server.saveConversationReqs[0])
|
||||
}
|
||||
if len(server.saveMessageReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
|
||||
}
|
||||
request := server.saveMessageReqs[0]
|
||||
if request.GetConversationID() != "conv-created" || request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetRole() != "user" {
|
||||
t.Fatalf("unexpected SaveAIConversationMessage request: %+v", request)
|
||||
}
|
||||
if request.GetContent() != "Explain the workflow.\n\nWith details." {
|
||||
t.Fatalf("unexpected prompt content: %q", request.GetContent())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitPromptCmdUsesExistingConversationSettings(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
saveMessageResp: &clientpb.AIConversationMessage{
|
||||
ID: "msg-1",
|
||||
ConversationID: "conv-1",
|
||||
Role: "user",
|
||||
Content: "hello",
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := submitPromptCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
aiTargetSummary{},
|
||||
&clientpb.AIConversation{ID: "conv-1", Provider: "anthropic", Model: "claude-test", Title: "Thread"},
|
||||
"openai",
|
||||
"gpt-test",
|
||||
"hello",
|
||||
)()
|
||||
|
||||
if _, ok := msg.(aiPromptSubmittedMsg); !ok {
|
||||
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 0 {
|
||||
t.Fatalf("did not expect SaveAIConversation to be called, got %d calls", len(server.saveConversationReqs))
|
||||
}
|
||||
if len(server.saveMessageReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
|
||||
}
|
||||
request := server.saveMessageReqs[0]
|
||||
if request.GetProvider() != "anthropic" || request.GetModel() != "claude-test" {
|
||||
t.Fatalf("unexpected conversation settings in message request: %+v", request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteConversationCmdSendsDeleteRequest(t *testing.T) {
|
||||
server := &aiRPCServer{}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := deleteConversationCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
"conv-1",
|
||||
"conv-2",
|
||||
`Deleted "Thread".`,
|
||||
)()
|
||||
|
||||
deleted, ok := msg.(aiConversationDeletedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiConversationDeletedMsg, got %T", msg)
|
||||
}
|
||||
if deleted.conversationID != "conv-1" || deleted.selectedID != "conv-2" {
|
||||
t.Fatalf("unexpected delete message: %+v", deleted)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.deleteConversationReqs) != 1 {
|
||||
t.Fatalf("expected DeleteAIConversation to be called once, got %d", len(server.deleteConversationReqs))
|
||||
}
|
||||
if server.deleteConversationReqs[0].GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected DeleteAIConversation request: %+v", server.deleteConversationReqs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdFiltersForAIEvents(t *testing.T) {
|
||||
eventPayload := &clientpb.AIConversationEvent{
|
||||
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
|
||||
Conversation: &clientpb.AIConversation{
|
||||
ID: "conv-1",
|
||||
OperatorName: "alice",
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(eventPayload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal conversation event: %v", err)
|
||||
}
|
||||
|
||||
listener := make(chan *clientpb.Event, 2)
|
||||
listener <- &clientpb.Event{EventType: consts.WatchtowerEvent}
|
||||
listener <- &clientpb.Event{EventType: consts.AIConversationEvent, Data: data}
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
|
||||
aiEvent, ok := msg.(aiConversationEventMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiConversationEventMsg, got %T", msg)
|
||||
}
|
||||
if aiEvent.event == nil || aiEvent.event.GetConversation() == nil || aiEvent.event.GetConversation().GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected AI conversation event payload: %+v", aiEvent.event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdReturnsClosedMessage(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
if _, ok := msg.(aiListenerClosedMsg); !ok {
|
||||
t.Fatalf("expected aiListenerClosedMsg, got %T", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIModelAppliesSharedConversationEventsWithoutReload(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
model := newAIModel(nil, aiContext{
|
||||
connection: aiConnectionSummary{Operator: "alice"},
|
||||
}, listener)
|
||||
model.loading = false
|
||||
model.conversations = []*clientpb.AIConversation{{ID: "conv-1", Title: "First"}}
|
||||
|
||||
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
|
||||
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
|
||||
Conversation: &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
OperatorName: "bob",
|
||||
Title: "Shared thread",
|
||||
},
|
||||
}})
|
||||
|
||||
nextModel := updated.(*aiModel)
|
||||
if nextModel.loading {
|
||||
t.Fatal("did not expect shared conversation event to trigger a reload")
|
||||
}
|
||||
if conversationIndexByID(nextModel.conversations, "conv-2") < 0 {
|
||||
t.Fatalf("expected shared conversation to be merged locally, got %+v", nextModel.conversations)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected shared conversation event to continue listening for events")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIModelAppliesDeleteEventsWhileAwaitingResponse(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
model := newAIModel(nil, aiContext{
|
||||
connection: aiConnectionSummary{Operator: "alice"},
|
||||
}, listener)
|
||||
model.loading = false
|
||||
model.awaitingResponse = true
|
||||
model.currentConversation = &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
OperatorName: "alice",
|
||||
UpdatedAt: 100,
|
||||
TurnState: clientpb.AIConversationTurnState_AI_TURN_STATE_IN_PROGRESS,
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "user", Content: "still waiting"},
|
||||
},
|
||||
}
|
||||
model.conversations = []*clientpb.AIConversation{
|
||||
{ID: "conv-2", Title: "Current"},
|
||||
{ID: "conv-1", Title: "Fallback"},
|
||||
}
|
||||
|
||||
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
|
||||
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_DELETED,
|
||||
ConversationID: "conv-2",
|
||||
Conversation: &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
},
|
||||
}})
|
||||
|
||||
nextModel := updated.(*aiModel)
|
||||
if nextModel.loading {
|
||||
t.Fatal("did not expect delete tombstone to trigger a reload")
|
||||
}
|
||||
if nextModel.currentConversation != nil {
|
||||
t.Fatalf("expected deleted conversation to be cleared, got %+v", nextModel.currentConversation)
|
||||
}
|
||||
if conversationIndexByID(nextModel.conversations, "conv-2") >= 0 {
|
||||
t.Fatalf("expected deleted conversation to be removed, got %+v", nextModel.conversations)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected delete tombstone to continue listening for events")
|
||||
}
|
||||
}
|
||||
|
||||
type aiRPCServer struct {
|
||||
rpcpb.UnimplementedSliverRPCServer
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
providersResp *clientpb.AIProviderConfigs
|
||||
conversationsResp *clientpb.AIConversations
|
||||
conversationByID map[string]*clientpb.AIConversation
|
||||
saveConversationResp *clientpb.AIConversation
|
||||
saveMessageResp *clientpb.AIConversationMessage
|
||||
|
||||
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
|
||||
@@ -400,7 +400,13 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
|
||||
var outFilePath *os.File
|
||||
if save, _ := cmd.Flags().GetBool("save"); save {
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(session.GetHostname())))
|
||||
hostname := ""
|
||||
if session != nil {
|
||||
hostname = session.GetHostname()
|
||||
} else if beacon != nil {
|
||||
hostname = beacon.GetHostname()
|
||||
}
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(hostname)))
|
||||
outFilePath, err = os.CreateTemp("", outFile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -66,6 +66,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.Bind("", false, consoleCmd, func(f *pflag.FlagSet) {
|
||||
f.String("transport", string(defaultConfig.Transport), "mcp transport (http or sse)")
|
||||
f.String("url", "", "mcp server URL (defaults to local server settings)")
|
||||
f.String("auth-token", "", "MCP auth token (defaults to the local mcp.yaml token)")
|
||||
})
|
||||
flags.BindFlagCompletions(consoleCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["transport"] = carapace.ActionValues("http", "sse")
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
clientmcp "github.com/bishopfox/sliver/client/mcp"
|
||||
"github.com/bishopfox/sliver/client/version"
|
||||
mcpclient "github.com/mark3labs/mcp-go/client"
|
||||
mcptransport "github.com/mark3labs/mcp-go/client/transport"
|
||||
mcpapi "github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -36,7 +38,23 @@ func McpConsoleCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
}
|
||||
}
|
||||
|
||||
client, err := newConsoleClient(transport, targetURL)
|
||||
authToken, _ := cmd.Flags().GetString("auth-token")
|
||||
if authToken == "" {
|
||||
status := clientmcp.GetStatus()
|
||||
if status.AuthToken != "" {
|
||||
authToken = status.AuthToken
|
||||
} else {
|
||||
authInfo, loadErr := clientmcp.LoadAuthInfo()
|
||||
if loadErr == nil {
|
||||
authToken = authInfo.Token
|
||||
} else if !errors.Is(loadErr, os.ErrNotExist) {
|
||||
con.PrintErrorf("%s\n", loadErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client, err := newConsoleClient(transport, targetURL, authToken)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
@@ -68,12 +86,19 @@ func McpConsoleCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
runConsoleREPL(ctx, client, cmd.InOrStdin(), cmd.OutOrStdout())
|
||||
}
|
||||
|
||||
func newConsoleClient(transport clientmcp.Transport, targetURL string) (*mcpclient.Client, error) {
|
||||
func newConsoleClient(transport clientmcp.Transport, targetURL string, authToken string) (*mcpclient.Client, error) {
|
||||
if strings.TrimSpace(authToken) == "" {
|
||||
return nil, fmt.Errorf("mcp auth token is required (use --auth-token or start the local MCP server first)")
|
||||
}
|
||||
headers := map[string]string{
|
||||
clientmcp.AuthHeaderName(): authToken,
|
||||
}
|
||||
|
||||
switch transport {
|
||||
case clientmcp.TransportHTTP:
|
||||
return mcpclient.NewStreamableHttpClient(targetURL)
|
||||
return mcpclient.NewStreamableHttpClient(targetURL, mcptransport.WithHTTPHeaders(headers))
|
||||
case clientmcp.TransportSSE:
|
||||
return mcpclient.NewSSEMCPClient(targetURL)
|
||||
return mcpclient.NewSSEMCPClient(targetURL, mcpclient.WithHeaders(headers))
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported transport %q", transport)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ func McpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
} else {
|
||||
con.PrintErrorf("Endpoint: %s\n", err)
|
||||
}
|
||||
if status.AuthHeader != "" {
|
||||
con.Printf("Auth Header: %s\n", status.AuthHeader)
|
||||
}
|
||||
if status.AuthToken != "" {
|
||||
con.Printf("Auth Token: %s\n", status.AuthToken)
|
||||
}
|
||||
|
||||
if status.Running && !status.StartedAt.IsZero() {
|
||||
uptime := time.Since(status.StartedAt).Truncate(time.Second)
|
||||
|
||||
@@ -43,4 +43,11 @@ func McpStartCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if err == nil {
|
||||
con.PrintInfof("Endpoint: %s\n", endpoint)
|
||||
}
|
||||
status := slivermcp.GetStatus()
|
||||
if status.AuthHeader != "" {
|
||||
con.PrintInfof("Auth Header: %s\n", status.AuthHeader)
|
||||
}
|
||||
if status.AuthToken != "" {
|
||||
con.PrintInfof("Auth Token: %s\n", status.AuthToken)
|
||||
}
|
||||
}
|
||||
@@ -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,191 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
authConfigFileName = "mcp.yaml"
|
||||
authHeaderName = "Authorization"
|
||||
minAuthTokenLength = 8
|
||||
authTokenBytes = 16
|
||||
sseMessagePath = "/message"
|
||||
)
|
||||
|
||||
// AuthInfo describes the persisted MCP HTTP/SSE authentication settings.
|
||||
type AuthInfo struct {
|
||||
Header string
|
||||
Token string
|
||||
ConfigPath string
|
||||
}
|
||||
|
||||
// AuthHeaderName returns the required header name for MCP HTTP/SSE requests.
|
||||
func AuthHeaderName() string {
|
||||
return authHeaderName
|
||||
}
|
||||
|
||||
// AuthConfigPath returns the MCP auth config path in the client app directory.
|
||||
func AuthConfigPath() string {
|
||||
rootDir, _ := filepath.Abs(assets.GetRootAppDir())
|
||||
return filepath.Join(rootDir, authConfigFileName)
|
||||
}
|
||||
|
||||
type authConfig struct {
|
||||
Token string `yaml:"token"`
|
||||
}
|
||||
|
||||
// LoadAuthInfo loads the persisted MCP auth configuration without creating it.
|
||||
func LoadAuthInfo() (AuthInfo, error) {
|
||||
cfg, exists, err := loadAuthConfig()
|
||||
if err != nil {
|
||||
return AuthInfo{}, err
|
||||
}
|
||||
if !exists {
|
||||
return AuthInfo{}, os.ErrNotExist
|
||||
}
|
||||
cfg.Token = strings.TrimSpace(cfg.Token)
|
||||
if err := validateAuthToken(cfg.Token); err != nil {
|
||||
return AuthInfo{}, fmt.Errorf("invalid mcp token in %s: %w", AuthConfigPath(), err)
|
||||
}
|
||||
return AuthInfo{
|
||||
Header: authHeaderName,
|
||||
Token: cfg.Token,
|
||||
ConfigPath: AuthConfigPath(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ResolveAuthInfo loads the persisted auth token or creates and saves one on first use.
|
||||
func ResolveAuthInfo() (AuthInfo, bool, error) {
|
||||
cfg, exists, err := loadAuthConfig()
|
||||
if err != nil {
|
||||
return AuthInfo{}, false, err
|
||||
}
|
||||
if exists {
|
||||
cfg.Token = strings.TrimSpace(cfg.Token)
|
||||
if err := validateAuthToken(cfg.Token); err != nil {
|
||||
return AuthInfo{}, false, fmt.Errorf("invalid mcp token in %s: %w", AuthConfigPath(), err)
|
||||
}
|
||||
return AuthInfo{
|
||||
Header: authHeaderName,
|
||||
Token: cfg.Token,
|
||||
ConfigPath: AuthConfigPath(),
|
||||
}, false, nil
|
||||
}
|
||||
|
||||
token, err := generateAuthToken()
|
||||
if err != nil {
|
||||
return AuthInfo{}, false, err
|
||||
}
|
||||
cfg = &authConfig{Token: token}
|
||||
if err := saveAuthConfig(cfg); err != nil {
|
||||
return AuthInfo{}, false, err
|
||||
}
|
||||
return AuthInfo{
|
||||
Header: authHeaderName,
|
||||
Token: token,
|
||||
ConfigPath: AuthConfigPath(),
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func loadAuthConfig() (*authConfig, bool, error) {
|
||||
data, err := os.ReadFile(AuthConfigPath())
|
||||
if os.IsNotExist(err) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
cfg := &authConfig{}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
return cfg, true, nil
|
||||
}
|
||||
|
||||
func saveAuthConfig(cfg *authConfig) error {
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(AuthConfigPath(), data, 0o600)
|
||||
}
|
||||
|
||||
func generateAuthToken() (string, error) {
|
||||
tokenBytes := make([]byte, authTokenBytes)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
return "", fmt.Errorf("generate mcp auth token: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(tokenBytes), nil
|
||||
}
|
||||
|
||||
func validateAuthToken(token string) error {
|
||||
if len(strings.TrimSpace(token)) < minAuthTokenLength {
|
||||
return fmt.Errorf("token must be at least %d characters", minAuthTokenLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func authMiddleware(token string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !authTokenMatches(r.Header.Get(authHeaderName), token) {
|
||||
http.Error(w, fmt.Sprintf("missing or invalid %s header", authHeaderName), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func authTokenMatches(actual string, expected string) bool {
|
||||
if len(expected) == 0 || len(actual) != len(expected) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(actual), []byte(expected)) == 1
|
||||
}
|
||||
|
||||
type authenticatedHTTPTransport struct {
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func newAuthenticatedTransport(cfg Config, handler http.Handler, token string) serverTransport {
|
||||
mux := http.NewServeMux()
|
||||
authenticatedHandler := authMiddleware(token, handler)
|
||||
|
||||
switch cfg.Transport {
|
||||
case TransportHTTP:
|
||||
mux.Handle("/mcp", authenticatedHandler)
|
||||
case TransportSSE:
|
||||
mux.Handle("/sse", authenticatedHandler)
|
||||
mux.Handle(sseMessagePath, authenticatedHandler)
|
||||
}
|
||||
|
||||
return &authenticatedHTTPTransport{
|
||||
server: &http.Server{
|
||||
Handler: mux,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *authenticatedHTTPTransport) Start(addr string) error {
|
||||
if t.server.Addr == "" {
|
||||
t.server.Addr = addr
|
||||
} else if t.server.Addr != addr {
|
||||
return fmt.Errorf("conflicting listen address: %q vs %q", t.server.Addr, addr)
|
||||
}
|
||||
return t.server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (t *authenticatedHTTPTransport) Shutdown(ctx context.Context) error {
|
||||
return t.server.Shutdown(ctx)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveAuthInfoCreatesAndPersistsToken(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
|
||||
authInfo, created, err := ResolveAuthInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveAuthInfo returned error: %v", err)
|
||||
}
|
||||
if !created {
|
||||
t.Fatalf("expected first ResolveAuthInfo call to create a token")
|
||||
}
|
||||
if authInfo.Header != AuthHeaderName() {
|
||||
t.Fatalf("expected header %q, got %q", AuthHeaderName(), authInfo.Header)
|
||||
}
|
||||
if len(authInfo.Token) != authTokenBytes*2 {
|
||||
t.Fatalf("expected %d hex characters, got %d", authTokenBytes*2, len(authInfo.Token))
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(AuthConfigPath())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read persisted auth config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "token: "+authInfo.Token) {
|
||||
t.Fatalf("persisted config did not contain generated token: %s", string(data))
|
||||
}
|
||||
|
||||
authInfoAgain, createdAgain, err := ResolveAuthInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("second ResolveAuthInfo returned error: %v", err)
|
||||
}
|
||||
if createdAgain {
|
||||
t.Fatalf("expected second ResolveAuthInfo call to reuse the existing token")
|
||||
}
|
||||
if authInfoAgain.Token != authInfo.Token {
|
||||
t.Fatalf("expected persisted token %q, got %q", authInfo.Token, authInfoAgain.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAuthInfoRejectsInvalidExistingToken(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
|
||||
if err := os.WriteFile(AuthConfigPath(), []byte("token: short\n"), 0o600); err != nil {
|
||||
t.Fatalf("failed to seed auth config: %v", err)
|
||||
}
|
||||
|
||||
_, _, err := ResolveAuthInfo()
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid token error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "at least 8 characters") {
|
||||
t.Fatalf("expected length validation error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddlewareRequiresExactToken(t *testing.T) {
|
||||
const token = "0123456789abcdef"
|
||||
|
||||
handler := authMiddleware(token, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
header string
|
||||
statusCode int
|
||||
}{
|
||||
{name: "missing", statusCode: http.StatusUnauthorized},
|
||||
{name: "wrong", header: "wrong-token", statusCode: http.StatusUnauthorized},
|
||||
{name: "prefixed", header: "Bearer " + token, statusCode: http.StatusUnauthorized},
|
||||
{name: "exact", header: token, statusCode: http.StatusNoContent},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/mcp", nil)
|
||||
if testCase.header != "" {
|
||||
req.Header.Set(AuthHeaderName(), testCase.header)
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
if recorder.Code != testCase.statusCode {
|
||||
t.Fatalf("expected status %d, got %d", testCase.statusCode, recorder.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+24
-11
@@ -20,10 +20,13 @@ var (
|
||||
|
||||
// Status describes the running state of the MCP server.
|
||||
type Status struct {
|
||||
Running bool
|
||||
StartedAt time.Time
|
||||
Config Config
|
||||
LastError string
|
||||
Running bool
|
||||
StartedAt time.Time
|
||||
Config Config
|
||||
LastError string
|
||||
AuthHeader string
|
||||
AuthToken string
|
||||
AuthConfigPath string
|
||||
}
|
||||
|
||||
type serverTransport interface {
|
||||
@@ -42,6 +45,7 @@ type mcpManager struct {
|
||||
done chan struct{}
|
||||
logger *log.Logger
|
||||
logFile *os.File
|
||||
authInfo AuthInfo
|
||||
}
|
||||
|
||||
func newManager() *mcpManager {
|
||||
@@ -93,10 +97,13 @@ func (m *mcpManager) status() Status {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return Status{
|
||||
Running: m.running,
|
||||
StartedAt: m.startedAt,
|
||||
Config: m.cfg,
|
||||
LastError: m.lastErr,
|
||||
Running: m.running,
|
||||
StartedAt: m.startedAt,
|
||||
Config: m.cfg,
|
||||
LastError: m.lastErr,
|
||||
AuthHeader: m.authInfo.Header,
|
||||
AuthToken: m.authInfo.Token,
|
||||
AuthConfigPath: m.authInfo.ConfigPath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +112,10 @@ func (m *mcpManager) start(cfg Config, rpc rpcpb.SliverRPCClient) error {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
authInfo, _, err := ResolveAuthInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
if m.running {
|
||||
@@ -120,9 +131,9 @@ func (m *mcpManager) start(cfg Config, rpc rpcpb.SliverRPCClient) error {
|
||||
var transport serverTransport
|
||||
switch cfg.Transport {
|
||||
case TransportHTTP:
|
||||
transport = mcpserver.NewStreamableHTTPServer(mcpServer.server)
|
||||
transport = newAuthenticatedTransport(cfg, mcpserver.NewStreamableHTTPServer(mcpServer.server), authInfo.Token)
|
||||
case TransportSSE:
|
||||
transport = mcpserver.NewSSEServer(mcpServer.server)
|
||||
transport = newAuthenticatedTransport(cfg, mcpserver.NewSSEServer(mcpServer.server), authInfo.Token)
|
||||
default:
|
||||
m.mu.Unlock()
|
||||
logFile.Close()
|
||||
@@ -138,11 +149,12 @@ func (m *mcpManager) start(cfg Config, rpc rpcpb.SliverRPCClient) error {
|
||||
m.done = make(chan struct{})
|
||||
m.logger = logger
|
||||
m.logFile = logFile
|
||||
m.authInfo = authInfo
|
||||
addr := cfg.ListenAddress
|
||||
done := m.done
|
||||
m.mu.Unlock()
|
||||
|
||||
logger.Printf("starting mcp server transport=%s listen=%s", cfg.Transport, cfg.ListenAddress)
|
||||
logger.Printf("starting mcp server transport=%s listen=%s auth_header=%s auth_config=%s", cfg.Transport, cfg.ListenAddress, authInfo.Header, authInfo.ConfigPath)
|
||||
go m.run(addr, transport, done)
|
||||
return nil
|
||||
}
|
||||
@@ -161,6 +173,7 @@ func (m *mcpManager) run(addr string, transport serverTransport, done chan struc
|
||||
m.running = false
|
||||
m.transport = nil
|
||||
m.server = nil
|
||||
m.authInfo = AuthInfo{}
|
||||
logFile := m.logFile
|
||||
m.logFile = nil
|
||||
m.logger = nil
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
# Sliver AI Agentic Loop
|
||||
|
||||
## Goal
|
||||
|
||||
Port the example agentic loop from `examples/codex/codex-rs` into Sliver's AI chat so the server can:
|
||||
|
||||
- run a multi-step Responses API turn
|
||||
- emit turn and item lifecycle events similar to Codex
|
||||
- persist reasoning and tool-call items for sync and restoration
|
||||
- keep those non-chat items out of the model context window
|
||||
- render the new items in the client TUI
|
||||
|
||||
## How The Codex Example Loop Works
|
||||
|
||||
The Codex example splits the loop into three layers.
|
||||
|
||||
### 1. Provider/Protocol Layer
|
||||
|
||||
The protocol layer receives raw model events such as:
|
||||
|
||||
- turn started / completed / failed
|
||||
- reasoning deltas and completed reasoning items
|
||||
- command/tool begin and end notifications
|
||||
- assistant messages
|
||||
|
||||
These provider events are fine-grained and often arrive as partial updates.
|
||||
|
||||
### 2. Event Processor Layer
|
||||
|
||||
`event_processor_with_jsonl_output.rs` converts the raw provider stream into a stable thread event model.
|
||||
|
||||
The important behaviors are:
|
||||
|
||||
- a turn gets a lifecycle: `turn.started`, then `turn.completed` or `turn.failed`
|
||||
- each logical item gets a stable id
|
||||
- begin/end pairs are folded into a single item lifecycle
|
||||
- reasoning is emitted as its own item type
|
||||
- tool calls and command executions are emitted as their own item types
|
||||
- active items can be updated in place before completion
|
||||
|
||||
This is the key abstraction that makes the TUI and persisted history simple.
|
||||
|
||||
### 3. TUI Layer
|
||||
|
||||
The Codex TUI consumes those stable events rather than reloading the whole thread.
|
||||
|
||||
That lets it:
|
||||
|
||||
- append new items incrementally
|
||||
- update an existing in-flight item in place
|
||||
- keep the transcript stable while a turn runs
|
||||
- restore the same item types from stored history later
|
||||
|
||||
## Sliver Port
|
||||
|
||||
Sliver ports the same shape, adapted to its existing RPC and database model.
|
||||
|
||||
### Conversation State
|
||||
|
||||
`AIConversation` now stores:
|
||||
|
||||
- `ActiveTurnID`
|
||||
- `TurnState`
|
||||
- `TargetSessionID`
|
||||
- `TargetBeaconID`
|
||||
|
||||
The turn fields let clients restore "waiting", "completed", and "failed" state without inferring it from the last chat message.
|
||||
|
||||
The target fields bind a conversation to the active session or beacon so server-side tools can default to the conversation target.
|
||||
|
||||
### Message State
|
||||
|
||||
`AIConversationMessage` now stores:
|
||||
|
||||
- `Kind`: chat, reasoning, or tool call
|
||||
- `Visibility`: context or UI-only
|
||||
- `State`: in progress, completed, or failed
|
||||
- `TurnID`
|
||||
- `ItemID`
|
||||
- tool call metadata: name, arguments, result, error text, tool call id
|
||||
|
||||
This keeps one ordered transcript table while still distinguishing chat context from UI-only telemetry.
|
||||
|
||||
### UI-Only Persistence
|
||||
|
||||
Reasoning and tool-call items are stored as normal conversation messages, but with `Visibility = UI_ONLY`.
|
||||
|
||||
That gives Sliver:
|
||||
|
||||
- cross-client sync through the existing conversation storage path
|
||||
- transcript restoration after reconnect or TUI restart
|
||||
- ordered replay of an entire turn, including intermediate items
|
||||
|
||||
At the same time, the model context builder filters out any non-context item, so the provider only receives actual user/assistant chat context.
|
||||
|
||||
### Event Model
|
||||
|
||||
Sliver now publishes `AIConversationEvent` payloads instead of raw `AIConversation` snapshots.
|
||||
|
||||
Supported event types:
|
||||
|
||||
- `CONVERSATION_UPDATED`
|
||||
- `CONVERSATION_DELETED`
|
||||
- `TURN_STARTED`
|
||||
- `TURN_COMPLETED`
|
||||
- `TURN_FAILED`
|
||||
- `MESSAGE_STARTED`
|
||||
- `MESSAGE_UPDATED`
|
||||
- `MESSAGE_COMPLETED`
|
||||
|
||||
Each event can carry:
|
||||
|
||||
- a conversation summary
|
||||
- a single message/item
|
||||
- the turn id
|
||||
- optional error text
|
||||
|
||||
This mirrors the Codex pattern of stable turn and item lifecycle events.
|
||||
|
||||
## Server Turn Flow
|
||||
|
||||
For each new user message:
|
||||
|
||||
1. The user chat message is saved as a normal context-visible chat item.
|
||||
2. The server resolves runtime provider/model settings.
|
||||
3. A new turn id is generated and persisted with `TURN_STARTED`.
|
||||
4. If the runtime supports the Responses API loop, Sliver runs the agentic loop.
|
||||
5. Reasoning items are persisted as completed UI-only reasoning blocks.
|
||||
6. Tool calls are persisted as in-progress UI-only tool blocks, executed locally, then updated in place to completed or failed.
|
||||
7. Tool outputs are fed back into the Responses API with `previous_response_id`.
|
||||
8. When the model returns final assistant text, that assistant message is persisted as a normal context-visible chat item.
|
||||
9. The conversation turn state is cleared with `TURN_COMPLETED`.
|
||||
|
||||
If anything fails:
|
||||
|
||||
- a UI-only failed system message is persisted
|
||||
- the conversation is marked `TURN_FAILED`
|
||||
- the failure remains visible in the transcript without polluting future model context
|
||||
|
||||
## Tool Surface
|
||||
|
||||
The first port exposes a safe read-only tool set:
|
||||
|
||||
- `list_sessions_and_beacons`
|
||||
- `fs_ls`
|
||||
- `fs_cat`
|
||||
- `fs_pwd`
|
||||
|
||||
Why read-only first:
|
||||
|
||||
- it proves the loop, storage, eventing, and TUI behavior
|
||||
- it avoids surprising destructive remote actions in the first server-side tool release
|
||||
|
||||
Filesystem tools accept explicit `session_id` or `beacon_id`, but when omitted they fall back to the conversation target.
|
||||
|
||||
Beacon-backed tools wait for task completion on the server before emitting the completed item update.
|
||||
|
||||
## Context Filtering
|
||||
|
||||
Only messages with:
|
||||
|
||||
- `Kind = CHAT`
|
||||
- `Visibility = CONTEXT`
|
||||
|
||||
are included in future provider requests.
|
||||
|
||||
That means these items are excluded from the model context window:
|
||||
|
||||
- reasoning blocks
|
||||
- tool call blocks
|
||||
- failed system status blocks
|
||||
|
||||
This is the main rule that preserves transcript richness without degrading prompt quality.
|
||||
|
||||
## TUI Behavior
|
||||
|
||||
The AI TUI now consumes `AIConversationEvent` incrementally.
|
||||
|
||||
Instead of reloading the whole conversation for every server event, it:
|
||||
|
||||
- merges conversation summaries into the sidebar
|
||||
- upserts messages in place by message id or item id
|
||||
- updates waiting state from `TurnState`
|
||||
- removes deleted conversations immediately
|
||||
|
||||
Reasoning and tool-call blocks render as dedicated dark-grey transcript boxes.
|
||||
|
||||
### Reasoning Blocks
|
||||
|
||||
Reasoning blocks show:
|
||||
|
||||
- the `Reasoning` label
|
||||
- the persisted reasoning text
|
||||
- item state metadata
|
||||
|
||||
### Tool Call Blocks
|
||||
|
||||
Tool blocks show:
|
||||
|
||||
- `Tool: <name>`
|
||||
- arguments
|
||||
- result
|
||||
- error text when present
|
||||
- item state metadata
|
||||
|
||||
These blocks are restored exactly like chat messages because they are stored in the same ordered transcript.
|
||||
|
||||
## Restore And Sync
|
||||
|
||||
Because UI-only items live in the database:
|
||||
|
||||
- another Sliver client can attach to the same conversation and see the full turn history
|
||||
- reconnecting the TUI restores reasoning and tool-call blocks
|
||||
- turn state survives restart, so "in progress" and "failed" status can be reconstructed from persisted conversation metadata
|
||||
|
||||
## Differences From Codex
|
||||
|
||||
This port intentionally differs from the example in a few places.
|
||||
|
||||
- Sliver currently uses non-streaming Responses API requests, so reasoning items arrive as completed items rather than token deltas.
|
||||
- Tool execution is currently a bounded read-only server-side function set, not the full Codex local tool surface.
|
||||
- Sliver persists intermediate items in its conversation DB rather than only in a transient thread runtime.
|
||||
|
||||
The important invariant is still the same:
|
||||
|
||||
- turn lifecycle is explicit
|
||||
- item lifecycle is explicit
|
||||
- UI-only items are persisted and replayable
|
||||
- only context-visible chat reaches the model
|
||||
Generated
+1764
-2661
File diff suppressed because it is too large.
Load diff
@@ -5,8 +5,8 @@ This document describes the technical design of Sliver.
|
||||
There are four major components to the Sliver ecosystem:
|
||||
|
||||
- **Server Console -** The server console is the main interface, which is started when you run the `sliver-server` executable. The server console is a superset of the client console. All code is shared between the client/server consoles except server-specific commands related to client (i.e., operator) management. The server console talks over an in-memory gRPC interface to the server.
|
||||
- **Sliver Server -** The Sliver server is also part of the `sliver-server` executable and manages the internal database, starts/stops network listeners (such as C2 listeners, though there are other types). The main interface used to interact with the server is the gRPC interface, thru which all functionality is implemented. By default the server will only start an in-memory gRPC listener that can only be communicated with from the server console. However, the gRPC interface can also be exposed to the network (i.e., multiplayer mode) over mutual TLS (mTLS).
|
||||
- **Sliver Client -** The client console is the primary user interface that is used to interact with the Sliver server. Note that most of the code in the server console is actually the client console. The client console can also be compiled into a separate client binary file `sliver-client`, which is generally used to connect to the "mutliplayer" gRPC network listener.
|
||||
- **Sliver Server -** The Sliver server is also part of the `sliver-server` executable and manages the internal database, starts/stops network listeners (such as C2 listeners, though there are other types). The main interface used to interact with the server is the gRPC interface, thru which all functionality is implemented. By default the server will only start an in-memory gRPC listener that can only be communicated with from the server console. However, the gRPC interface can also be exposed to the network in multiplayer mode, either directly over mutual TLS (mTLS) or, by default, through a WireGuard-protected wrapper that carries the same gRPC/mTLS service.
|
||||
- **Sliver Client -** The client console is the primary user interface that is used to interact with the Sliver server. Note that most of the code in the server console is actually the client console. The client console can also be compiled into a separate client binary file `sliver-client`, which is generally used to connect to the multiplayer gRPC network listener.
|
||||
- **Implant -** The implant is the actual malicious code run on the target system you want remote access to.
|
||||
|
||||
```
|
||||
@@ -17,7 +17,8 @@ There are four major components to the Sliver ecosystem:
|
||||
└─────────┘ └───────────────┘
|
||||
▲
|
||||
│
|
||||
│gRPC/mTLS
|
||||
│WireGuard (default)
|
||||
│or direct gRPC/mTLS
|
||||
│
|
||||
┌────┴────┐
|
||||
│ Sliver │
|
||||
@@ -25,4 +26,4 @@ There are four major components to the Sliver ecosystem:
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
By implementing all functionality over this gRPC interface, and only differing the in-memory/mTLS connection types the client code doesn't "know" if it's running in the server console or the client console. Due to this, a single command implementation will work in both the server console and over the network in multiplayer mode.
|
||||
By implementing all functionality over this gRPC interface, and only differing the in-memory or remote transport types, the client code doesn't "know" if it's running in the server console or the client console. Due to this, a single command implementation will work in both the server console and over the network in multiplayer mode.
|
||||
@@ -12,6 +12,7 @@ daemon:
|
||||
host: ""
|
||||
port: 31337
|
||||
tailscale: false
|
||||
disable_wg: false
|
||||
logs:
|
||||
level: 4
|
||||
grpc_unary_payloads: false
|
||||
@@ -21,6 +22,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:
|
||||
@@ -41,8 +52,9 @@ cxx: {}
|
||||
- `daemon_mode` - Enable [daemon mode](/docs?name=Daemon+Mode).
|
||||
- `daemon` - Options for daemon mode.
|
||||
- `host` - Interface to bind the daemon listener to. Empty string means all interfaces.
|
||||
- `port` - TCP port for the daemon listener.
|
||||
- `tailscale` - Enable Tailscale for daemon listener setup.
|
||||
- `port` - Listen port for the multiplayer listener.
|
||||
- `tailscale` - Enable Tailscale for daemon listener setup. When enabled, Sliver does not wrap multiplayer in its own WireGuard layer.
|
||||
- `disable_wg` - Expose multiplayer directly over mTLS instead of the default WireGuard-protected mode.
|
||||
- `logs` - Server logging options.
|
||||
- `level` - `logrus` level (`0`-`6`, clamped by Sliver). `4` is `INFO`, `5` is `DEBUG`, `6` is `TRACE`.
|
||||
- `grpc_unary_payloads` - Log gRPC unary payloads.
|
||||
@@ -51,6 +63,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.
|
||||
@@ -124,8 +142,21 @@ For details on generating them, see [multiplayer mode](/docs?name=Multi-player+M
|
||||
"lport": 31337,
|
||||
"ca_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
|
||||
"private_key": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n",
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
|
||||
"wg": {
|
||||
"server_pub_key": "<multiplayer-wg-server-pubkey>",
|
||||
"client_private_key": "<operator-wg-private-key>",
|
||||
"client_pub_key": "<operator-wg-public-key>",
|
||||
"client_ip": "100.65.0.10",
|
||||
"server_ip": "100.65.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `operator` field is mostly a local label in the client. Server-side identity is derived from the client certificate (`CommonName`) and token material generated by the server. If you need to rename an operator, generate a new client configuration.
|
||||
|
||||
The `wg` block is optional:
|
||||
|
||||
- When present, `sliver-client` automatically brings up the multiplayer WireGuard wrapper and then dials the in-tunnel gRPC/mTLS service.
|
||||
- When absent, the client connects directly to the multiplayer mTLS listener.
|
||||
- Generate a direct-only profile with `new-operator --disable-wg` or `sliver-server operator --disable-wg` when you need compatibility with external gRPC clients that do not implement the multiplayer WireGuard wrapper.
|
||||
@@ -1,9 +1,20 @@
|
||||
Although the `sliver-client` is the default way to interact with a `sliver-server` and with implant sessions, there might be a time where you would want to automate some tasks upon reception of certain events.
|
||||
|
||||
To do so, one case use [sliver-script](https://github.com/moloch--/sliver-script), [sliver-py](https://github.com/moloch--/sliver-py) or write a custom client in another language. As all the communications between the client and the server are based on gRPC, any language with gRPC support should in theory be used to create a custom client.
|
||||
To do so, you can use [sliver-script](https://github.com/moloch--/sliver-script), [sliver-py](https://github.com/moloch--/sliver-py), or write a custom client in another language.
|
||||
|
||||
Current Sliver releases generate multiplayer operator configs with a `wg` block by default. The official `sliver-client`, and Go clients that reuse Sliver's `client/assets` plus `client/transport` packages, will automatically bring up that WireGuard wrapper before dialing the in-tunnel gRPC/mTLS service.
|
||||
|
||||
If you are writing a custom client in another language, you have two options:
|
||||
|
||||
- Implement the multiplayer WireGuard wrapper described in [multiplayer mode](/docs?name=Multi-player+Mode), then connect to the in-tunnel gRPC/mTLS service.
|
||||
- Keep multiplayer in direct mode by starting the listener with `multiplayer --disable-wg` or `sliver-server daemon --disable-wg`, then generate operator profiles with `new-operator --disable-wg` or `sliver-server operator --disable-wg`.
|
||||
|
||||
Once connected, the client/server API is still gRPC, so any language with gRPC support can in theory be used to create a custom client.
|
||||
|
||||
## Writing a Go client
|
||||
|
||||
This example uses Sliver's Go client libraries, so it works with either a direct multiplayer profile or a WireGuard-enabled profile.
|
||||
|
||||
In this example, we will focus on writing a custom Go client that executes a new system command on every new implant that connects to the sliver server.
|
||||
|
||||
Create a new Go project somewhere on your file system:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Sliver supports running in daemon mode, which automatically starts the multiplayer client listener (but not an interactive console). To connect to a daemonized server, use [multiplayer mode](/docs?name=Multi-player+Mode) operator configs.
|
||||
Sliver supports running in daemon mode, which automatically starts the multiplayer listener but does not provide an interactive console. To connect to a daemonized server, use [multiplayer mode](/docs?name=Multi-player+Mode) operator configs.
|
||||
|
||||
By default, daemon mode starts the same WireGuard-protected multiplayer listener used by the interactive `multiplayer` command. That means the outer listener is UDP on `31337` by default, while the authenticated gRPC/mTLS service stays inside the WireGuard tunnel.
|
||||
|
||||
There are two ways to run in daemon mode:
|
||||
|
||||
@@ -13,9 +15,11 @@ If `server.yaml` does not exist, Sliver generates it. If a legacy `server.json`
|
||||
- `-l, --lhost` multiplayer listener host
|
||||
- `-p, --lport` multiplayer listener port
|
||||
- `-t, --tailscale` enable Tailscale listener
|
||||
- `--disable-wg` expose multiplayer directly over mTLS instead of using the default WireGuard wrapper
|
||||
- `-f, --force` force unpack static assets
|
||||
- For `sliver-server daemon`, `--lhost` and `--lport` override config values. If omitted, Sliver uses `daemon.host` and `daemon.port` from `server.yaml`.
|
||||
- For normal startup (`sliver-server`) with `daemon_mode: true`, Sliver uses `daemon.tailscale` from `server.yaml`.
|
||||
- For normal startup (`sliver-server`) with `daemon_mode: true`, Sliver uses `daemon.tailscale` and `daemon.disable_wg` from `server.yaml`.
|
||||
- With `--disable-wg` or `daemon.disable_wg: true`, multiplayer falls back to direct TCP mTLS on the configured port.
|
||||
|
||||
### Example Config
|
||||
|
||||
@@ -25,6 +29,7 @@ daemon:
|
||||
host: ""
|
||||
port: 31337
|
||||
tailscale: false
|
||||
disable_wg: false
|
||||
logs:
|
||||
level: 4
|
||||
grpc_unary_payloads: false
|
||||
@@ -44,6 +49,8 @@ Since daemon mode does not provide an interactive server console, generate opera
|
||||
./sliver-server operator --name zer0cool --lhost 1.2.3.4 --permissions all --save zer0cool.cfg
|
||||
```
|
||||
|
||||
The `operator` CLI matches the daemon's multiplayer exposure by default. If the daemon is running in direct mode or over Tailscale, the generated config omits the multiplayer `wg` block automatically. You can also force a direct-only profile with `--disable-wg`.
|
||||
|
||||
### Shutdown Behavior
|
||||
|
||||
On Unix-like systems, daemon mode handles `SIGTERM` and exits cleanly.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
This script will install the latest version of Sliver as a systemd service, installs Windows cross-compiler dependencies (mingw), and setup multiplayer for the all local users. After running the script connect locally by running `sliver`.
|
||||
This script installs the latest version of Sliver as a systemd service, installs Windows cross-compiler dependencies (mingw), and sets up multiplayer for all local users. After running the script, connect locally by running `sliver`.
|
||||
|
||||
https://sliver.sh/install
|
||||
|
||||
This script should work on Kali, Ubuntu, and RHEL (CentOS, etc) distributions of Linux.
|
||||
|
||||
**⚠️ OPSEC:** By default the Linux install script will bind the multiplayer listener to `:31337` i.e. all interfaces. Ensure your firewalls are properly configured if this is a concern, or reconfigure the server to bind to localhost if you only wish to allow local users. Multiplayer Mode is authenticated using mTLS/gRPC and per-operator bearer tokens, so this doesn't present a security problem (notwithstanding bugs in our implementation), but publicly exposing your multiplayer listener will make it trivial to identify what the server is running.
|
||||
**⚠️ OPSEC:** By default the Linux install script will bind the multiplayer listener to `:31337` on all interfaces. In current releases that is the WireGuard-protected multiplayer listener, so the outer service is UDP/31337 and the authenticated gRPC/mTLS server only exists inside the tunnel. Ensure your firewalls are properly configured if this is a concern, or reconfigure the server to bind to localhost if you only wish to allow local users. Publicly exposing the multiplayer listener still makes the server easier to discover and fingerprint.
|
||||
|
||||
### One Liner
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ sliver > mcp start --transport http
|
||||
|
||||
[*] Starting MCP server (http) on 127.0.0.1:8080
|
||||
[*] Endpoint: http://127.0.0.1:8080/mcp
|
||||
[*] Auth Header: Authorization
|
||||
[*] Auth Token: 6f90c3b3c6058fa59f570e281f3f8d39
|
||||
```
|
||||
|
||||
You can use `mcp stop` to stop the server, see `mcp start --help` for additional options when starting the MCP server. Authentication is not yet supported for MCP over HTTP/SSE.
|
||||
You can use `mcp stop` to stop the server, see `mcp start --help` for additional options when starting the MCP server.
|
||||
|
||||
On first HTTP/SSE use, Sliver generates a random 128-bit token and saves it to `~/.sliver-client/mcp.yaml`. Every MCP HTTP/SSE request must include that token exactly in the `Authorization` header. If `mcp.yaml` already exists, the listener will only start when the token is at least 8 characters long.
|
||||
@@ -1,4 +1,6 @@
|
||||
Multiplayer-mode allows multiple operators (players) to connect to the same Sliver server and collaborate on engagements. The easiest way to set up a server for multiplayer is to use the [Linux install script](/docs?name=Linux+Install+Script) which will configure the server as a systemd service. However, any Sliver server binary supports multiplayer mode.
|
||||
Multiplayer mode allows multiple operators to connect to the same Sliver server and collaborate on engagements. The easiest way to set up a server for multiplayer is to use the [Linux install script](/docs?name=Linux+Install+Script), which configures the server as a systemd service. However, any `sliver-server` binary supports multiplayer mode.
|
||||
|
||||
By default, Sliver now protects the operator-facing multiplayer listener with a dedicated WireGuard wrapper. The gRPC/mTLS authentication stack is still used inside the tunnel, but the server no longer exposes the multiplayer gRPC listener directly unless you explicitly opt out with `--disable-wg`.
|
||||
|
||||
```
|
||||
┌──────────────────┐ C2
|
||||
@@ -8,7 +10,8 @@ Multiplayer-mode allows multiple operators (players) to connect to the same Sliv
|
||||
└──────────────────┘
|
||||
▲
|
||||
│
|
||||
gRPC/mTLS │
|
||||
UDP/WireGuard (default) │
|
||||
gRPC/mTLS inside │
|
||||
┌────────────┬────────┴─────┬───────────┐
|
||||
│ │ │ │
|
||||
┌─────┴──┐ │ │ ┌──┴─────┐
|
||||
@@ -18,32 +21,39 @@ Multiplayer-mode allows multiple operators (players) to connect to the same Sliv
|
||||
└────────┘ └────────┘
|
||||
```
|
||||
|
||||
## New Operators
|
||||
## Default WireGuard-Protected Mode
|
||||
|
||||
Operators will need to download their preferred platform's client software from the releases page. Clients can connect to servers of differing platforms (e.g. a Windows client can connect to a Linux server). Operators and servers authenticate using Mutual TLS, however all the certificates are managed automatically for you.
|
||||
|
||||
- If you did not install the server as a systemd service, be sure to leave the server console running in screen or [tmux](https://github.com/tmux/tmux) after starting the multiplayer listener.
|
||||
- If you did install the server as systemd service see the CLI section below for an alternative to the console commands for generating operator configs.
|
||||
- If you did install the server as a systemd service, see the CLI section below for an alternative to the console commands for generating operator configs.
|
||||
|
||||
Operators need a configuration file generated by the Sliver server to connect in multiplayer mode. The Sliver client can support multiple configuration files, but you can only be connected to one server at a time (per process).
|
||||
|
||||
From the server use the `new-operator` command to generate a new operator configuration file. You'll need to specify the operator's `--name` and the `--lhost` of the server (e.g., its public IP). Then from the server start a multiplayer listener using the `multiplayer` command to allow operators to connect:
|
||||
In the default mode:
|
||||
|
||||
**Note:** The `new-operator` and `multiplayer` commands are only accessible from the server's console
|
||||
- `multiplayer` exposes a WireGuard listener on `--lport` (default `31337`) over UDP.
|
||||
- The authenticated gRPC/mTLS server only listens inside that WireGuard tunnel.
|
||||
- `new-operator` generates a per-operator WireGuard keypair and tunnel IP, then embeds that information in the operator config's `wg` block.
|
||||
- The multiplayer listener uses WireGuard keys that are separate from Sliver's implant `wg` C2 listener.
|
||||
- `sliver-client` automatically uses the WireGuard wrapper when the imported config contains that `wg` block.
|
||||
|
||||
From the server, start the multiplayer listener and generate an operator config with the server's public address or hostname in `--lhost`. Use `--permissions all` for a standard interactive operator profile:
|
||||
|
||||
**Note:** The `new-operator` and `multiplayer` commands are only available from the server console.
|
||||
|
||||
```
|
||||
[server] sliver > new-operator --name moloch --lhost 1.2.3.4
|
||||
|
||||
[*] Generating new client certificate, please wait ...
|
||||
[*] Saved new client config to: /Users/moloch/Desktop/moloch_example.com.cfg
|
||||
|
||||
[server] sliver > multiplayer
|
||||
|
||||
[*] Multiplayer mode enabled!
|
||||
|
||||
[server] sliver > new-operator --name moloch --lhost 1.2.3.4 --permissions all
|
||||
|
||||
[*] Generating new client certificate, please wait ...
|
||||
[*] Saved new client config to: /Users/moloch/Desktop/moloch_example.com.cfg
|
||||
```
|
||||
|
||||
**IMPORTANT:** Before clients can connect to a server you must start an RPC listener with the `multiplayer` command. The default port is TCP/31337.
|
||||
**IMPORTANT:** Before clients can connect to a server you must start a multiplayer listener with the `multiplayer` command. In the default WireGuard-protected mode the outer listener is UDP/31337. If you disable the wrapper, multiplayer is exposed directly on TCP/31337 instead.
|
||||
|
||||
You can now give this configuration file `moloch_example.com.cfg` to the operator and they can connect to the server using the `sliver-client` binary. The Sliver client will look for configuration files in `~/.sliver-client/configs/` or you can import configs using the `import` CLI. The configs directory can contain multiple configs for different servers.
|
||||
|
||||
@@ -56,19 +66,49 @@ $ ./sliver-client
|
||||
localhost
|
||||
```
|
||||
|
||||
## Direct mTLS Mode
|
||||
|
||||
If you want the old behavior, or you need compatibility with third-party gRPC clients that do not implement the multiplayer WireGuard wrapper, disable it on both the listener and the generated operator profile:
|
||||
|
||||
```
|
||||
[server] sliver > multiplayer --disable-wg
|
||||
|
||||
[*] Multiplayer mode enabled!
|
||||
|
||||
[server] sliver > new-operator --name tester --lhost 1.2.3.4 --permissions all --disable-wg
|
||||
|
||||
[*] Generating new client certificate, please wait ...
|
||||
[*] Saved new client config to: /Users/tester/Desktop/tester_example.com.cfg
|
||||
```
|
||||
|
||||
In direct mode:
|
||||
|
||||
- Multiplayer is exposed directly over TCP on `--lport` (default `31337`).
|
||||
- Generated operator configs omit the `wg` block.
|
||||
- `sliver-client --disable-wg` forces a direct connection even if the config includes a `wg` block.
|
||||
- `sliver-client --require-wg` fails fast if the config does not contain a valid `wg` block.
|
||||
|
||||
The listener mode and the operator config need to match. A WireGuard-enabled config cannot talk to a direct listener, and a direct-only client cannot talk to the default WireGuard-wrapped listener.
|
||||
|
||||
### Server CLI / Daemon Mode Multiplayer
|
||||
|
||||
If the server is running in daemon mode (as is the default with the Linux install script), that means the multiplayer listener is started for you without an interactive console. You can use the server binaries' CLI to generate operator configuration files to connect:
|
||||
If the server is running in daemon mode, the multiplayer listener is started for you without an interactive console. By default the daemon uses the same WireGuard-protected multiplayer mode described above. You can switch the daemon back to direct exposure with `sliver-server daemon --disable-wg` or `daemon.disable_wg: true` in `server.yaml`.
|
||||
|
||||
Use the server CLI to generate operator configuration files:
|
||||
|
||||
```
|
||||
./sliver-server operator --name zer0cool --lhost 1.2.3.4 --save zer0cool.cfg
|
||||
./sliver-server operator --name zer0cool --lhost 1.2.3.4 --permissions all --save zer0cool.cfg
|
||||
```
|
||||
|
||||
The installation scripts by default will place the `sliver-server` binary in `/root`.
|
||||
When daemon mode is configured for direct multiplayer or Tailscale, the CLI automatically omits the `wg` block by default. You can also force a direct-only profile with `--disable-wg`.
|
||||
|
||||
The installation script places the `sliver-server` binary in `/root` by default.
|
||||
|
||||
### Tailscale Integration
|
||||
|
||||
Sliver v1.6 supports multiplayer integration with [Tailscale](https://tailscale.com/), which is an excellent option when wanting to maintain operational security.
|
||||
Sliver supports multiplayer integration with [Tailscale](https://tailscale.com/), which is an excellent option when you want to keep the multiplayer listener off the public Internet.
|
||||
|
||||
Tailscale mode is separate from the default multiplayer WireGuard wrapper. When you use `multiplayer -T`, Sliver exposes the listener only through Tailscale instead of layering its own multiplayer WireGuard tunnel on top.
|
||||
|
||||
```
|
||||
$ export TS_AUTHKEY=<tailscale authkey>
|
||||
@@ -77,4 +117,4 @@ $ ./sliver-server
|
||||
sliver > multiplayer -T
|
||||
```
|
||||
|
||||
You should now see a new Tailscale host named `sliver-server-<machine>`, you can set this using the `--lhost` flag. Use this as the hostname when connecting to the Sliver server from any other host on the Tailscale network.
|
||||
You should now see a new Tailscale host named `sliver-server-<machine>`. Use that hostname as `--lhost` when generating operator configs for other hosts on the same tailnet. If you generate the operator config before enabling Tailscale multiplayer, add `--disable-wg` explicitly so the profile does not include the multiplayer `wg` block.
|
||||
@@ -59,14 +59,16 @@ First, install the sliver-py extension using pip.
|
||||
pip3 install sliver-py
|
||||
```
|
||||
|
||||
Since our extension is essentially going to be another client connection to the sliver server, you’ll also need to enable multiplayer mode and generate a new profile
|
||||
Since our extension is essentially going to be another client connection to the sliver server, you’ll also need to enable multiplayer mode and generate a new profile.
|
||||
|
||||
Current Sliver releases default to a WireGuard-protected multiplayer listener. Because this tutorial uses a third-party Python client, we’ll force direct multiplayer mode so the profile only contains the plain gRPC/mTLS settings:
|
||||
|
||||
```bash
|
||||
[server] sliver > multiplayer
|
||||
[server] sliver > multiplayer --disable-wg
|
||||
|
||||
[*] Multiplayer mode enabled!
|
||||
|
||||
[server] sliver > new-operator -n tester -l 127.0.0.1
|
||||
[server] sliver > new-operator -n tester -l 127.0.0.1 --permissions all --disable-wg
|
||||
|
||||
[*] Generating new client certificate, please wait ...
|
||||
[*] Saved new client config to: /Users/tester/tools/tester_127.0.0.1.cfg
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"tutorials":[{"name":"1 - Getting Started","content":"`sliver-server` is the binary you want to use to run the Sliver C2 server, `sliver-client` is solely a client to connect to a Sliver C2 server. Sliver server also acts as a client on its own, so you don’t necessarily run sliver server and client separately.\n\nFirst time running Sliver will take a couple seconds as it's retrieving its dependencies. Consecutive executions will be much faster. Go ahead and launch the `sliver-server`.\n\n```asciinema\n{\"src\": \"/asciinema/startup.cast\", \"cols\": \"132\", \"rows\": \"28\", \"idleTimeLimit\": 8}\n```\n\nLet's take a couple minutes to discuss what Sliver actually is and how it's set up.\n\n\n\nNow that Sliver is running, lets generate and execute your first implant to try out some of the basic features of Sliver, for now we’re going to run everything on the local host.\n\nHere's what we're going to do: \n* Generate your implant using the `generate` command as shown below.\n* Start HTTP listener on port 80\n* Execute implant in a separate terminal\n\n```asciinema\n{\"src\": \"/asciinema/first-implant.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNow let’s select our implant and run our first command using the `use` command.\n\n```bash\n[server] sliver > use\n? Select a session or beacon: \nSESSION 1884a365 RELATED_EARDRUM [::1]:49153 test.local tester darwin/amd64\n[*] Active session RELATED_EARDRUM (1884a365-085f-4506-b28e-80c481730fd0)\n\n[server] sliver (RELATED_EARDRUM) > pwd\n\n[*] /Users/tester/tools\n```\n\nOnce you have reached this point, go ahead and explore some of the commands listed below. In each case, first check out the command's help using the **`-h`** flag then try it out!\n\n```bash\nExploring and interacting with the filesystem\n\nFilesystem\n cat Dump file to stdout\n cd Change directory\n cp Copy a file\n download Download a file\n grep Search for strings that match a regex within a file or directory\n head Grab the first number of bytes or lines from a file\n ls List current directory\n mkdir Make a directory\n mv Move or rename a file\n pwd Print working directory\n rm Remove a file or directory\n tail Grab the last number of bytes or lines from a file\n upload Upload a file\n```\n\n```asciinema\n{\"src\": \"/asciinema/filesystem.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nGetting some environmental information\n```bash\nInfo\n env List environment variables\n getgid Get session process GID\n getpid Get session pid\n getuid Get session process UID\n info Get session info\n ping Send round trip message to implant (does not use ICMP)\n whoami Get session user execution context\n```\nExecute a binary\n\n```asciinema\n{\"src\": \"/asciinema/execute.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nRunning an interactive shell\n\n```asciinema\n{\"src\": \"/asciinema/shell.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNot all commands are supported for all operating systems or meant to be used on all operating systems, here are a few examples:\n\n```\nmount Get information on mounted filesystems (only for Windows and Linux)\nscreenshot Take a screenshot (only for Windows and Linux)\nmemfiles List current memfiles (Linux only)\n```\n\nThis is an open-source project if you see a bug please file an issue! Pull requests are always welcome."},{"name":"2 - Beacons vs Sessions","content":"Sliver implants support two types of connections, sessions and beacons.\n\nSessions use long-polling connections, which means they constantly maintain a single TCP connection open while communicating with the server. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.\n\nTypically during an engagement you will want to deploy a beacon on the target system and switch to a session while doing more active enumeration.\n\nLet’s start with generating and deploying a beacon using `http`.\n\n```asciinema\n{\"src\": \"/asciinema/beacon_generation.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nYou can see the beacon callback times either in the `info` command or using `beacons watch`.\n\n```bash\n[server] sliver > beacons watch\n\n ID Name Transport Username Operating System Last Check-In Next Check-In\n========== =============== =========== ================= ==========Line truncated
|
||||
{"tutorials":[{"name":"1 - Getting Started","content":"`sliver-server` is the binary you want to use to run the Sliver C2 server, `sliver-client` is solely a client to connect to a Sliver C2 server. Sliver server also acts as a client on its own, so you don’t necessarily run sliver server and client separately.\n\nFirst time running Sliver will take a couple seconds as it's retrieving its dependencies. Consecutive executions will be much faster. Go ahead and launch the `sliver-server`.\n\n```asciinema\n{\"src\": \"/asciinema/startup.cast\", \"cols\": \"132\", \"rows\": \"28\", \"idleTimeLimit\": 8}\n```\n\nLet's take a couple minutes to discuss what Sliver actually is and how it's set up.\n\n\n\nNow that Sliver is running, lets generate and execute your first implant to try out some of the basic features of Sliver, for now we’re going to run everything on the local host.\n\nHere's what we're going to do: \n* Generate your implant using the `generate` command as shown below.\n* Start HTTP listener on port 80\n* Execute implant in a separate terminal\n\n```asciinema\n{\"src\": \"/asciinema/first-implant.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNow let’s select our implant and run our first command using the `use` command.\n\n```bash\n[server] sliver > use\n? Select a session or beacon: \nSESSION 1884a365 RELATED_EARDRUM [::1]:49153 test.local tester darwin/amd64\n[*] Active session RELATED_EARDRUM (1884a365-085f-4506-b28e-80c481730fd0)\n\n[server] sliver (RELATED_EARDRUM) > pwd\n\n[*] /Users/tester/tools\n```\n\nOnce you have reached this point, go ahead and explore some of the commands listed below. In each case, first check out the command's help using the **`-h`** flag then try it out!\n\n```bash\nExploring and interacting with the filesystem\n\nFilesystem\n cat Dump file to stdout\n cd Change directory\n cp Copy a file\n download Download a file\n grep Search for strings that match a regex within a file or directory\n head Grab the first number of bytes or lines from a file\n ls List current directory\n mkdir Make a directory\n mv Move or rename a file\n pwd Print working directory\n rm Remove a file or directory\n tail Grab the last number of bytes or lines from a file\n upload Upload a file\n```\n\n```asciinema\n{\"src\": \"/asciinema/filesystem.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nGetting some environmental information\n```bash\nInfo\n env List environment variables\n getgid Get session process GID\n getpid Get session pid\n getuid Get session process UID\n info Get session info\n ping Send round trip message to implant (does not use ICMP)\n whoami Get session user execution context\n```\nExecute a binary\n\n```asciinema\n{\"src\": \"/asciinema/execute.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nRunning an interactive shell\n\n```asciinema\n{\"src\": \"/asciinema/shell.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNot all commands are supported for all operating systems or meant to be used on all operating systems, here are a few examples:\n\n```\nmount Get information on mounted filesystems (only for Windows and Linux)\nscreenshot Take a screenshot (only for Windows and Linux)\nmemfiles List current memfiles (Linux only)\n```\n\nThis is an open-source project if you see a bug please file an issue! Pull requests are always welcome."},{"name":"2 - Beacons vs Sessions","content":"Sliver implants support two types of connections, sessions and beacons.\n\nSessions use long-polling connections, which means they constantly maintain a single TCP connection open while communicating with the server. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.\n\nTypically during an engagement you will want to deploy a beacon on the target system and switch to a session while doing more active enumeration.\n\nLet’s start with generating and deploying a beacon using `http`.\n\n```asciinema\n{\"src\": \"/asciinema/beacon_generation.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nYou can see the beacon callback times either in the `info` command or using `beacons watch`.\n\n```bash\n[server] sliver > beacons watch\n\n ID Name Transport Username Operating System Last Check-In Next Check-In\n========== =============== =========== ================= ==========Line truncated
|
||||
+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.49.0
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
|
||||
golang.org/x/mod v0.33.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/text v0.35.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.3
|
||||
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/mark3labs/mcp-go v0.45.0
|
||||
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/sync v0.19.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.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-20260316180232-0b37fe3546d5 // 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=
|
||||
@@ -532,6 +553,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc=
|
||||
github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -541,8 +564,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 +577,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 +604,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 +640,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 +696,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 +815,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 +868,13 @@ 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/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
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 +890,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 +913,13 @@ 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/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
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=
|
||||
@@ -899,6 +935,8 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -922,7 +960,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 +975,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 +986,10 @@ 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/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
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 +1003,10 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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 +1023,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 +1039,34 @@ 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/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/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/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/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
|
||||
|
||||
Loaded 100 of 1767 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user