Compare commits

..
Author SHA1 Message Date
moloch-- ad274cc260 Redesign unit tests 2026-02-25 08:44:17 -08:00
2277 changed files with 208766 additions and 298201 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
* @BishopFox/bf-red-team
* @BishopFox/red-team
+7 -18
View File
@@ -2,8 +2,8 @@ name: Release
on:
push:
tags:
- "v*.*.*"
branches:
- master
permissions:
contents: write
@@ -21,22 +21,11 @@ jobs:
with:
fetch-depth: 0
- name: Validate Release Tag
- name: Find Semver Tag
id: tag
run: |
git fetch --prune --tags -f origin
tag="${GITHUB_REF_NAME}"
if ! printf '%s\n' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Tag $tag is not a release semver tag."
exit 0
fi
if ! git merge-base --is-ancestor "$GITHUB_SHA" "origin/master"; then
echo "Tag $tag does not point to a commit on origin/master."
exit 0
fi
git fetch --prune --tags -f
tag=$(git tag --points-at "$GITHUB_SHA" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
echo "tag=$tag" >> "$GITHUB_OUTPUT"
release:
@@ -52,10 +41,10 @@ jobs:
with:
fetch-depth: 0
- name: Go 1.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: Install Minisign
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
+2 -2
View File
@@ -16,10 +16,10 @@ jobs:
steps:
# Setup go environment
- name: Go 1.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: Checkout repository
uses: actions/checkout@v6
-56
View File
@@ -1,56 +0,0 @@
name: golangci-lint
on:
push:
branches:
- master
pull_request:
branches:
- master
paths:
- ".golangci.yml"
- ".github/workflows/golangci-lint.yml"
- "go.mod"
- "go.sum"
- "go.work"
- "go.work.sum"
- "Makefile"
- "vendor/**"
- "**/*.go"
permissions:
contents: read
pull-requests: read
jobs:
lint:
name: golangci-lint (${{ matrix.goos }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
goos:
- linux
- darwin
- windows
env:
GOOS: ${{ matrix.goos }}
GOARCH: amd64
CGO_ENABLED: "0"
steps:
- name: Check Out Code
uses: actions/checkout@v6
- name: Go 1.26.6
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.12.2
# Keep legacy findings visible without making them blockers.
only-new-issues: true
args: ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
+8 -53
View File
@@ -6,13 +6,11 @@ on:
paths:
- "go.mod"
- "go.sum"
- ".github/workflows/unit-tests.yml"
- "Makefile"
- "**/*.go"
- "**/*.c"
- "**/*.h"
- "**/*.sh"
- "**/*.ps1"
types:
- opened
- reopened
@@ -20,15 +18,15 @@ on:
- ready_for_review
jobs:
linux-build-test:
name: Linux Build & Test
linux-windows-build-test:
name: Linux & Windows Test
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- name: Go 1.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: OS Packages
run: |
@@ -66,59 +64,16 @@ 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.26.6
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
- 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.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: Check Out Code
uses: actions/checkout@v6
@@ -153,10 +108,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Go 1.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: Check Out Code
uses: actions/checkout@v6
-5
View File
@@ -1,8 +1,3 @@
/crush
/examples
/.vscode
/.vscode/*
/.devcontainer
/.idea
-27
View File
@@ -1,27 +0,0 @@
version: "2"
run:
timeout: 10m
modules-download-mode: vendor
build-tags:
- client
- server
- osusergo
- netgo
- go_sqlite
# Raw implant entry points are Go templates; lint their supporting packages.
- sliver_lint
linters:
default: standard
enable:
- gocyclo
- misspell
- revive
settings:
gocyclo:
min-complexity: 15
formatters:
enable:
- gofmt
+1 -1
View File
@@ -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 Go test suite, this is the only way you should run unit tests.
- `./go-tests.sh` runs the curated Go test suite with build tags.
- 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
+144 -291
View File
@@ -2,31 +2,31 @@
# Makefile for Sliver
#
.DEFAULT_GOAL := default
GO ?= go
ARTIFACT_SUFFIX ?=
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
GOLANGCI_LINT ?= golangci-lint
# The tree has legacy lint findings, so gate issues introduced by this change.
GOLANGCI_LINT_BASE ?= master
GOLANGCI_LINT_ARGS ?= --new-from-merge-base=$(GOLANGCI_LINT_BASE)
GOLANGCI_LINT_PACKAGES = \
./client/... \
./server/... \
./util/... \
./protobuf/... \
./docs \
./implant/...
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
CGO_ENABLED = 1
endif
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Build Information
#
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 26
MIN_SUPPORTED_GO_MINOR_VERSION = 24
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
@@ -34,298 +34,151 @@ 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
.PHONY: lint
lint:
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_ARGS) $(GOLANGCI_LINT_PACKAGES)
ifneq ($(OS),Windows_NT)
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Build Information
#
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
LDFLAGS = -ldflags "-s -w \
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
SED_INPLACE := sed -i
STATIC_TARGET := linux
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
SED_INPLACE := sed -i
STATIC_TARGET := linux
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Programs required for generating protobuf/grpc files
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ifeq ($(MAKECMDGOALS), pb)
K := $(foreach exec,$(PB_COMPILERS),\
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
endif
# *** Darwin ***
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
STATIC_TARGET := macos
endif
# If no target is specified, determine GOARCH
ifeq ($(UNAME_P),arm)
ifeq ($(MAKECMDGOALS), )
ifeq ($(origin GOARCH), undefined)
ENV += GOARCH=arm64
endif
endif
endif
ifeq ($(MAKECMDGOALS), linux)
# Redefine LDFLAGS to add the static part
LDFLAGS = -ldflags "-s -w \
-extldflags '-static' \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
#
# 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
# *** 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
#
# >>> WINDOWS <<<
# Targets
#
else
.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
SHELL := cmd.exe
.SHELLFLAGS := /C
# 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
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-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
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: 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_DEBUG = -ldflags "-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
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
.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
+1 -1
View File
@@ -4,7 +4,7 @@ Sliver is an open source cross-platform adversary emulation/red team framework,
The server and client support MacOS, Windows, and Linux. Implants are supported on MacOS, Windows, and Linux (and possibly every Golang compiler target but we've not tested them all).
[![Release](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [![golangci-lint](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Release](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/BishopFox/sliver)](https://goreportcard.com/report/github.com/BishopFox/sliver) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
+7 -17
View File
@@ -34,25 +34,15 @@ 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"`
WG *ClientWGConfig `json:"wg,omitempty"`
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"`
}
// GetConfigDir - Returns the path to the config dir
-1
View File
@@ -52,7 +52,6 @@ func init() {
rootCmd.TraverseChildren = true
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
rootCmd.PersistentFlags().Bool(enableWGFlag, false, "connect to multiplayer through the operator config's WireGuard wrapper")
// 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
-177
View File
@@ -1,177 +0,0 @@
package cli
import (
"fmt"
"io"
"strings"
"time"
"unicode/utf8"
bspinner "charm.land/bubbles/v2/spinner"
"github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"google.golang.org/grpc"
)
const (
minConnectSpinnerDuration = 900 * time.Millisecond
minConnectStatusDuration = 350 * time.Millisecond
)
type connectResult struct {
rpc rpcpb.SliverRPCClient
conn *grpc.ClientConn
err error
}
func connectWithSpinner(out io.Writer, target string, connect func(transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error)) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
statusCh := make(chan string, 8)
resultCh := make(chan connectResult, 1)
go func() {
rpc, conn, err := connect(func(status string) {
sendStatus(statusCh, status)
})
resultCh <- connectResult{rpc: rpc, conn: conn, err: err}
}()
currentStatus := ""
currentStatusSince := time.Time{}
queuedStatus := ""
lastWidth := 0
spinner := bspinner.New(bspinner.WithSpinner(bspinner.Line))
ticker := time.NewTicker(spinner.Spinner.FPS)
defer ticker.Stop()
startedAt := time.Now()
var pendingResult *connectResult
setStatus := func(status string) {
status = strings.TrimSpace(status)
if status == "" || status == currentStatus {
return
}
currentStatus = status
currentStatusSince = time.Now()
}
stageStatus := func(status string) {
status = strings.TrimSpace(status)
if status == "" {
return
}
if currentStatus == "" || time.Since(currentStatusSince) >= minConnectStatusDuration {
queuedStatus = ""
setStatus(status)
return
}
queuedStatus = status
}
flushQueuedStatus := func() {
if queuedStatus == "" || time.Since(currentStatusSince) < minConnectStatusDuration {
return
}
nextStatus := queuedStatus
queuedStatus = ""
setStatus(nextStatus)
}
canReturnPendingResult := func() bool {
if pendingResult == nil {
return false
}
if pendingResult.err != nil {
return true
}
if time.Since(startedAt) < minConnectSpinnerDuration {
return false
}
if queuedStatus != "" {
return false
}
if !currentStatusSince.IsZero() && time.Since(currentStatusSince) < minConnectStatusDuration {
return false
}
return true
}
render := func() {
line := fmt.Sprintf("%s %s", spinner.View(), formatConnectSpinnerMessage(target, currentStatus))
lastWidth = writeSpinnerLine(out, line, lastWidth)
}
render()
for {
select {
case status := <-statusCh:
stageStatus(status)
spinner, _ = spinner.Update(spinner.Tick())
render()
case result := <-resultCh:
pendingResult = &result
case <-ticker.C:
spinner, _ = spinner.Update(spinner.Tick())
flushQueuedStatus()
render()
if canReturnPendingResult() {
result := *pendingResult
pendingResult = nil
clearSpinnerLine(out, lastWidth)
return result.rpc, result.conn, result.err
}
}
if pendingResult != nil && pendingResult.err != nil {
result := *pendingResult
pendingResult = nil
clearSpinnerLine(out, lastWidth)
return result.rpc, result.conn, result.err
}
}
}
func sendStatus(statusCh chan string, status string) {
status = strings.TrimSpace(status)
if status == "" {
return
}
statusCh <- status
}
func formatConnectSpinnerMessage(target string, status string) string {
target = strings.TrimSpace(target)
status = strings.TrimSpace(status)
if target == "" {
if status == "" {
return "Connecting ..."
}
return fmt.Sprintf("Connecting (%s) ...", status)
}
if status == "" {
return fmt.Sprintf("Connecting to %s ...", target)
}
return fmt.Sprintf("Connecting to %s (%s) ...", target, status)
}
func writeSpinnerLine(out io.Writer, line string, lastWidth int) int {
width := utf8.RuneCountInString(line)
padding := ""
if lastWidth > width {
padding = strings.Repeat(" ", lastWidth-width)
}
fmt.Fprintf(out, "\r%s%s", line, padding)
if width > lastWidth {
return width
}
return lastWidth
}
func clearSpinnerLine(out io.Writer, lastWidth int) {
if lastWidth <= 0 {
return
}
fmt.Fprintf(out, "\r%s\r", strings.Repeat(" ", lastWidth))
}
-132
View File
@@ -1,132 +0,0 @@
package cli
import (
"bytes"
"strings"
"testing"
"time"
"github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"google.golang.org/grpc"
)
func TestFormatConnectSpinnerMessage(t *testing.T) {
tests := []struct {
name string
target string
status string
want string
}{
{
name: "target only",
target: "127.0.0.1:31337",
want: "Connecting to 127.0.0.1:31337 ...",
},
{
name: "target with status",
target: "127.0.0.1:31337",
status: "wireguard",
want: "Connecting to 127.0.0.1:31337 (wireguard) ...",
},
{
name: "status without target",
status: "grpc/mtls",
want: "Connecting (grpc/mtls) ...",
},
{
name: "trim whitespace",
target: " 127.0.0.1:31337 ",
status: " grpc/mtls over wireguard ",
want: "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := formatConnectSpinnerMessage(test.target, test.status); got != test.want {
t.Fatalf("expected %q, got %q", test.want, got)
}
})
}
}
func TestConnectWithSpinnerOutput(t *testing.T) {
var out bytes.Buffer
spinnerDelay := 2 * (100 * time.Millisecond)
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
statusFn("wireguard")
time.Sleep(spinnerDelay)
statusFn("grpc/mtls over wireguard")
time.Sleep(spinnerDelay)
return nil, nil, nil
})
if err != nil {
t.Fatalf("connectWithSpinner returned error: %v", err)
}
got := out.String()
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
t.Fatalf("expected wireguard status in output, got %q", got)
}
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
t.Fatalf("expected grpc over wireguard status in output, got %q", got)
}
frameCount := 0
for _, frame := range []string{"|", "/", "-", "\\"} {
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337") {
frameCount++
}
}
if frameCount < 2 {
t.Fatalf("expected multiple spinner frames in output, got %q", got)
}
}
func TestConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
var out bytes.Buffer
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
statusFn("grpc/mtls")
return nil, nil, nil
})
if err != nil {
t.Fatalf("connectWithSpinner returned error: %v", err)
}
got := out.String()
frameCount := 0
for _, frame := range []string{"|", "/", "-", "\\"} {
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337 (grpc/mtls) ...") {
frameCount++
}
}
if frameCount < 2 {
t.Fatalf("expected multiple spinner frames for fast success, got %q", got)
}
}
func TestConnectWithSpinnerFastSuccessShowsEachStatus(t *testing.T) {
var out bytes.Buffer
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
statusFn("wireguard")
statusFn("grpc/mtls over wireguard")
return nil, nil, nil
})
if err != nil {
t.Fatalf("connectWithSpinner returned error: %v", err)
}
got := out.String()
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
t.Fatalf("expected fast success output to include wireguard status, got %q", got)
}
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
t.Fatalf("expected fast success output to include grpc over wireguard status, got %q", got)
}
if strings.Contains(got, "Connected to 127.0.0.1:31337") {
t.Fatalf("did not expect past-tense success output, got %q", got)
}
}
+7 -14
View File
@@ -20,7 +20,6 @@ package cli
import (
"fmt"
"os"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command"
@@ -45,9 +44,6 @@ 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 {
@@ -65,20 +61,17 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
return nil
}
target := fmt.Sprintf("%s:%d", config.LHost, config.LPort)
// Don't clobber output when simply running an implant command from system shell.
if run {
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
}
var rpc rpcpb.SliverRPCClient
var ln *grpc.ClientConn
// Don't clobber output when simply running an implant command from system shell.
if run {
rpc, ln, err = connectWithSpinner(os.Stdout, target, func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
return transport.MTLSConnectWithStatus(config, statusFn)
})
} else {
rpc, ln, err = transport.MTLSConnect(config)
}
rpc, ln, err = transport.MTLSConnect(config)
if err != nil {
fmt.Printf("Connection to server failed %s\n", err)
fmt.Printf("Connection to server failed %s", err)
return nil
}
return console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
+1 -5
View File
@@ -43,10 +43,6 @@ 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 {
@@ -59,7 +55,7 @@ func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
fmt.Printf("Connection to server failed %s\n", err)
return nil
}
defer transport.CloseGRPCConnection(ln)
defer ln.Close()
con.Rpc = rpc
go handleConnectionLost(ln)
-29
View File
@@ -1,29 +0,0 @@
package cli
import (
"github.com/bishopfox/sliver/client/transport"
"github.com/spf13/cobra"
)
const (
enableWGFlag = "enable-wg"
)
func applyMultiplayerConnectMode(cmd *cobra.Command) error {
if cmd == nil {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectDirect)
return nil
}
enableWG, err := cmd.Flags().GetBool(enableWGFlag)
if err != nil {
return err
}
mode := transport.MultiplayerConnectDirect
if enableWG {
mode = transport.MultiplayerConnectEnableWG
}
transport.SetMultiplayerConnectMode(mode)
return nil
}
-1
View File
@@ -12,7 +12,6 @@ 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.
-5
View File
@@ -1,5 +0,0 @@
# 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.
-242
View File
@@ -1,242 +0,0 @@
package ai
/*
Sliver Implant Framework
Copyright (C) 2026 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"fmt"
"os"
"strings"
"time"
tea "charm.land/bubbletea/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/termio"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var openAIProgramTTY = tea.OpenTTY
// AICmd launches the AI conversation TUI.
func AICmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
if con == nil || con.Rpc == nil {
if con != nil {
con.PrintErrorf("Connect to a server before using `ai`.\n")
}
return
}
listenerID, listener := con.CreateEventListener()
defer con.RemoveEventListener(listenerID)
restoreEventNotifications := con.SuppressEventNotifications()
defer restoreEventNotifications()
model := newAIModel(con, buildAIContext(con), listener)
model.showExperimentalWarningModal()
width, height := 100, 30
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
opts := []tea.ProgramOption{
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
}
ttyOpts, cleanup := configureAIProgramTTY()
if cleanup != nil {
defer cleanup()
}
opts = append(opts, ttyOpts...)
program := tea.NewProgram(model, opts...)
if _, err := program.Run(); err != nil {
con.PrintErrorf("AI TUI error: %s\n", err)
}
}
// Console logging can swap stdout/stderr to pipes so session output can be tee'd
// to disk. Bubble Tea needs a real TTY output to enable terminal features like
// mouse reporting, so bind the AI TUI back to the interactive terminal when
// stdout is no longer the controlling TTY.
func configureAIProgramTTY() ([]tea.ProgramOption, func()) {
stdinTTY := isAITTY(os.Stdin)
stdoutTTY := isAITTY(os.Stdout)
switch {
case !stdinTTY || stdoutTTY:
return nil, nil
case isAITTY(termio.InteractiveInput()) && isAITTY(termio.InteractiveOutput()):
return []tea.ProgramOption{
tea.WithInput(termio.InteractiveInput()),
tea.WithOutput(termio.InteractiveOutput()),
}, nil
default:
inTTY, outTTY, err := openAIProgramTTY()
if err != nil {
return nil, nil
}
return []tea.ProgramOption{
tea.WithInput(inTTY),
tea.WithOutput(outTTY),
}, func() {
_ = inTTY.Close()
if outTTY != inTTY {
_ = outTTY.Close()
}
}
}
}
func isAITTY(file *os.File) bool {
return file != nil && term.IsTerminal(int(file.Fd()))
}
type aiContext struct {
target aiTargetSummary
connection aiConnectionSummary
status string
}
type aiTargetSummary struct {
SessionID string
BeaconID string
Label string
Host string
OS string
Arch string
C2 string
Mode string
Details []string
}
type aiConnectionSummary struct {
Profile string
Server string
Operator string
State string
}
func defaultAITargetSummary() aiTargetSummary {
return aiTargetSummary{
Label: "No active target",
Host: "Select a session or beacon with `use` or ctrl+s",
Mode: "offline preview",
C2: "n/a",
OS: "unknown",
Arch: "unknown",
}
}
func aiSessionTargetSummary(session *clientpb.Session) aiTargetSummary {
if session == nil {
return defaultAITargetSummary()
}
return aiTargetSummary{
SessionID: session.ID,
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
Host: fallback(session.Hostname, "<unknown host>"),
OS: fallback(session.OS, "unknown"),
Arch: fallback(session.Arch, "unknown"),
C2: fallback(session.ActiveC2, "unknown"),
Mode: "interactive session",
Details: []string{
fmt.Sprintf("User: %s", fallback(session.Username, "<unknown>")),
fmt.Sprintf("PID: %d", session.PID),
fmt.Sprintf("Remote: %s", fallback(session.RemoteAddress, "<unknown>")),
},
}
}
func aiBeaconTargetSummary(beacon *clientpb.Beacon) aiTargetSummary {
if beacon == nil {
return defaultAITargetSummary()
}
return aiTargetSummary{
BeaconID: beacon.ID,
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
Host: fallback(beacon.Hostname, "<unknown host>"),
OS: fallback(beacon.OS, "unknown"),
Arch: fallback(beacon.Arch, "unknown"),
C2: fallback(beacon.ActiveC2, "unknown"),
Mode: "asynchronous beacon",
Details: []string{
fmt.Sprintf("User: %s", fallback(beacon.Username, "<unknown>")),
fmt.Sprintf("PID: %d", beacon.PID),
fmt.Sprintf("Remote: %s", fallback(beacon.RemoteAddress, "<unknown>")),
fmt.Sprintf("Interval: %s", time.Duration(beacon.Interval).String()),
fmt.Sprintf("Next checkin: %s", formatUnix(beacon.NextCheckin)),
},
}
}
func buildAIContext(con *console.SliverClient) aiContext {
ctx := aiContext{
target: defaultAITargetSummary(),
connection: aiConnectionSummary{
Profile: "<disconnected>",
Server: "<unknown>",
Operator: "<unknown>",
State: "idle",
},
status: "Loading AI conversations from the server...",
}
if con != nil {
if details, state, ok := con.CurrentConnection(); ok {
ctx.connection.State = strings.ToLower(state.String())
if details != nil && details.Config != nil {
ctx.connection.Profile = fallback(details.ConfigKey, "<profile unavailable>")
ctx.connection.Server = fmt.Sprintf("%s:%d", details.Config.LHost, details.Config.LPort)
ctx.connection.Operator = fallback(details.Config.Operator, "<unknown>")
}
}
session, beacon := con.ActiveTarget.Get()
switch {
case session != nil:
ctx.target = aiSessionTargetSummary(session)
case beacon != nil:
ctx.target = aiBeaconTargetSummary(beacon)
}
}
return ctx
}
func fallback(value, def string) string {
value = strings.TrimSpace(value)
if value == "" {
return def
}
return value
}
func formatUnix(ts int64) string {
if ts <= 0 {
return "<unknown>"
}
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
}
-49
View File
@@ -1,49 +0,0 @@
package ai
/*
Sliver Implant Framework
Copyright (C) 2026 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
// Commands returns the ai command.
func Commands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newAICommand(consts.SliverCoreHelpGroup, con)}
}
// ServerCommands returns the ai command for the top-level client REPL.
func ServerCommands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newAICommand(consts.GenericHelpGroup, con)}
}
func newAICommand(groupID string, con *console.SliverClient) *cobra.Command {
return &cobra.Command{
Use: consts.AIStr,
Short: "Open the Sliver AI conversation TUI",
Long: help.GetHelpFor([]string{consts.AIStr}),
Args: cobra.NoArgs,
GroupID: groupID,
Run: func(cmd *cobra.Command, args []string) {
AICmd(cmd, con, args)
},
}
}
-591
View File
@@ -1,591 +0,0 @@
package ai
/*
Sliver Implant Framework
Copyright (C) 2026 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"context"
"net"
"sync"
"testing"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
"google.golang.org/protobuf/proto"
)
func TestLoadAIStateCmdLoadsConversationOverRPC(t *testing.T) {
server := &aiRPCServer{
providersResp: &clientpb.AIProviderConfigs{
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
Config: &clientpb.AIConfigSummary{
Provider: "openai",
Model: "gpt-test",
Valid: true,
},
},
conversationsResp: &clientpb.AIConversations{
Conversations: []*clientpb.AIConversation{
{ID: "conv-1", Provider: "openai", Model: "gpt-test", Title: "First"},
{ID: "conv-2", Provider: "openai", Model: "gpt-test", Title: "Second"},
},
},
conversationByID: map[string]*clientpb.AIConversation{
"conv-1": {
ID: "conv-1",
Provider: "openai",
Model: "gpt-test",
Title: "First",
Messages: []*clientpb.AIConversationMessage{
{Role: "user", Content: "Hello"},
},
},
},
}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
cmd := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")
msg := cmd()
loaded, ok := msg.(aiLoadedMsg)
if !ok {
t.Fatalf("expected aiLoadedMsg, got %T", msg)
}
if loaded.selectedID != "conv-1" {
t.Fatalf("unexpected selected conversation id: got=%q want=%q", loaded.selectedID, "conv-1")
}
if loaded.conversation == nil || loaded.conversation.GetID() != "conv-1" {
t.Fatalf("unexpected loaded conversation: %+v", loaded.conversation)
}
if len(loaded.conversations) != 2 {
t.Fatalf("unexpected conversation count: got=%d want=%d", len(loaded.conversations), 2)
}
server.mu.Lock()
defer server.mu.Unlock()
if server.getProvidersCalls != 1 {
t.Fatalf("expected GetAIProviders to be called once, got %d", server.getProvidersCalls)
}
if server.getConversationsCalls != 1 {
t.Fatalf("expected GetAIConversations to be called once, got %d", server.getConversationsCalls)
}
if len(server.getConversationReqs) != 1 {
t.Fatalf("expected GetAIConversation to be called once, got %d", len(server.getConversationReqs))
}
if server.getConversationReqs[0].GetID() != "conv-1" || !server.getConversationReqs[0].GetIncludeMessages() {
t.Fatalf("unexpected GetAIConversation request: %+v", server.getConversationReqs[0])
}
}
func TestLoadAIStateCmdCreatesConversationWhenNoneExist(t *testing.T) {
server := &aiRPCServer{
providersResp: &clientpb.AIProviderConfigs{
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
Config: &clientpb.AIConfigSummary{
Provider: "openai",
Model: "gpt-test",
SystemPrompt: "Stay concise.",
Valid: true,
},
},
conversationsResp: &clientpb.AIConversations{},
saveConversationResp: &clientpb.AIConversation{
ID: "conv-created",
Provider: "openai",
Model: "gpt-test",
Title: "New conversation",
SystemPrompt: "Stay concise.",
},
conversationByID: map[string]*clientpb.AIConversation{
"conv-created": {
ID: "conv-created",
Provider: "openai",
Model: "gpt-test",
Title: "New conversation",
SystemPrompt: "Stay concise.",
},
},
}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
msg := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")()
loaded, ok := msg.(aiLoadedMsg)
if !ok {
t.Fatalf("expected aiLoadedMsg, got %T", msg)
}
if loaded.status != "Created a new AI conversation." {
t.Fatalf("unexpected status: %q", loaded.status)
}
if loaded.selectedID != "conv-created" {
t.Fatalf("unexpected selected conversation id: %q", loaded.selectedID)
}
server.mu.Lock()
defer server.mu.Unlock()
if len(server.saveConversationReqs) != 1 {
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
}
request := server.saveConversationReqs[0]
if request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetTitle() != "New conversation" {
t.Fatalf("unexpected SaveAIConversation request: %+v", request)
}
if request.GetSystemPrompt() != "Stay concise." {
t.Fatalf("unexpected SaveAIConversation system prompt: %q", request.GetSystemPrompt())
}
}
func TestSubmitPromptCmdCreatesConversationAndSavesUserMessage(t *testing.T) {
server := &aiRPCServer{
saveConversationResp: &clientpb.AIConversation{
ID: "conv-created",
Provider: "openai",
Model: "gpt-test",
Title: "Explain the workflow.",
},
saveMessageResp: &clientpb.AIConversationMessage{
ID: "msg-1",
ConversationID: "conv-created",
Role: "user",
Content: "Explain the workflow.\n\nWith details.",
},
}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
msg := submitPromptCmd(
&console.SliverClient{Rpc: rpcClient},
aiTargetSummary{SessionID: "session-1"},
nil,
"openai",
"gpt-test",
"Stay concise.",
"Explain the workflow.\n\nWith details.",
)()
submitted, ok := msg.(aiPromptSubmittedMsg)
if !ok {
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
}
if submitted.conversationID != "conv-created" {
t.Fatalf("unexpected conversation id: %q", submitted.conversationID)
}
server.mu.Lock()
defer server.mu.Unlock()
if len(server.saveConversationReqs) != 1 {
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
}
if server.saveConversationReqs[0].GetTitle() != "Explain the workflow." {
t.Fatalf("unexpected conversation title: %q", server.saveConversationReqs[0].GetTitle())
}
if server.saveConversationReqs[0].GetTargetSessionID() != "session-1" {
t.Fatalf("expected target session to be forwarded, got %+v", server.saveConversationReqs[0])
}
if server.saveConversationReqs[0].GetSystemPrompt() != "Stay concise." {
t.Fatalf("unexpected conversation system prompt: %q", server.saveConversationReqs[0].GetSystemPrompt())
}
if len(server.saveMessageReqs) != 1 {
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
}
request := server.saveMessageReqs[0]
if request.GetConversationID() != "conv-created" || request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetRole() != "user" {
t.Fatalf("unexpected SaveAIConversationMessage request: %+v", request)
}
if request.GetContent() != "Explain the workflow.\n\nWith details." {
t.Fatalf("unexpected prompt content: %q", request.GetContent())
}
}
func TestSubmitPromptCmdUsesExistingConversationSettings(t *testing.T) {
server := &aiRPCServer{
saveMessageResp: &clientpb.AIConversationMessage{
ID: "msg-1",
ConversationID: "conv-1",
Role: "user",
Content: "hello",
},
}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
msg := submitPromptCmd(
&console.SliverClient{Rpc: rpcClient},
aiTargetSummary{},
&clientpb.AIConversation{ID: "conv-1", Provider: "anthropic", Model: "claude-test", Title: "Thread"},
"openai",
"gpt-test",
"Stay concise.",
"hello",
)()
if _, ok := msg.(aiPromptSubmittedMsg); !ok {
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
}
server.mu.Lock()
defer server.mu.Unlock()
if len(server.saveConversationReqs) != 0 {
t.Fatalf("did not expect SaveAIConversation to be called, got %d calls", len(server.saveConversationReqs))
}
if len(server.saveMessageReqs) != 1 {
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
}
request := server.saveMessageReqs[0]
if request.GetProvider() != "anthropic" || request.GetModel() != "claude-test" {
t.Fatalf("unexpected conversation settings in message request: %+v", request)
}
}
func TestDeleteConversationCmdSendsDeleteRequest(t *testing.T) {
server := &aiRPCServer{}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
msg := deleteConversationCmd(
&console.SliverClient{Rpc: rpcClient},
"conv-1",
"conv-2",
`Deleted "Thread".`,
)()
deleted, ok := msg.(aiConversationDeletedMsg)
if !ok {
t.Fatalf("expected aiConversationDeletedMsg, got %T", msg)
}
if deleted.conversationID != "conv-1" || deleted.selectedID != "conv-2" {
t.Fatalf("unexpected delete message: %+v", deleted)
}
server.mu.Lock()
defer server.mu.Unlock()
if len(server.deleteConversationReqs) != 1 {
t.Fatalf("expected DeleteAIConversation to be called once, got %d", len(server.deleteConversationReqs))
}
if server.deleteConversationReqs[0].GetID() != "conv-1" {
t.Fatalf("unexpected DeleteAIConversation request: %+v", server.deleteConversationReqs[0])
}
}
func TestWaitForAIConversationEventCmdFiltersForAIEvents(t *testing.T) {
eventPayload := &clientpb.AIConversationEvent{
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
Conversation: &clientpb.AIConversation{
ID: "conv-1",
OperatorName: "alice",
},
}
data, err := proto.Marshal(eventPayload)
if err != nil {
t.Fatalf("marshal conversation event: %v", err)
}
listener := make(chan *clientpb.Event, 2)
listener <- &clientpb.Event{EventType: consts.WatchtowerEvent}
listener <- &clientpb.Event{EventType: consts.AIConversationEvent, Data: data}
msg := waitForAIConversationEventCmd(listener)()
aiEvent, ok := msg.(aiConversationEventMsg)
if !ok {
t.Fatalf("expected aiConversationEventMsg, got %T", msg)
}
if aiEvent.event == nil || aiEvent.event.GetConversation() == nil || aiEvent.event.GetConversation().GetID() != "conv-1" {
t.Fatalf("unexpected AI conversation event payload: %+v", aiEvent.event)
}
}
func TestWaitForAIConversationEventCmdReturnsToastMessages(t *testing.T) {
listener := make(chan *clientpb.Event, 1)
listener <- &clientpb.Event{
EventType: consts.ClientToastEvent,
Data: []byte("Session alpha connected"),
Err: "info",
}
msg := waitForAIConversationEventCmd(listener)()
toast, ok := msg.(aiToastMsg)
if !ok {
t.Fatalf("expected aiToastMsg, got %T", msg)
}
if toast.level != "info" || toast.message != "Session alpha connected" {
t.Fatalf("unexpected toast payload: %+v", toast)
}
}
func TestWaitForAIConversationEventCmdReturnsClosedMessage(t *testing.T) {
listener := make(chan *clientpb.Event)
close(listener)
msg := waitForAIConversationEventCmd(listener)()
if _, ok := msg.(aiListenerClosedMsg); !ok {
t.Fatalf("expected aiListenerClosedMsg, got %T", msg)
}
}
func TestAIModelAppliesSharedConversationEventsWithoutReload(t *testing.T) {
listener := make(chan *clientpb.Event)
close(listener)
model := newAIModel(nil, aiContext{
connection: aiConnectionSummary{Operator: "alice"},
}, listener)
model.loading = false
model.conversations = []*clientpb.AIConversation{{ID: "conv-1", Title: "First"}}
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
Conversation: &clientpb.AIConversation{
ID: "conv-2",
OperatorName: "bob",
Title: "Shared thread",
},
}})
nextModel := updated.(*aiModel)
if nextModel.loading {
t.Fatal("did not expect shared conversation event to trigger a reload")
}
if conversationIndexByID(nextModel.conversations, "conv-2") < 0 {
t.Fatalf("expected shared conversation to be merged locally, got %+v", nextModel.conversations)
}
if cmd == nil {
t.Fatal("expected shared conversation event to continue listening for events")
}
}
func TestAIModelAppliesDeleteEventsWhileAwaitingResponse(t *testing.T) {
listener := make(chan *clientpb.Event)
close(listener)
model := newAIModel(nil, aiContext{
connection: aiConnectionSummary{Operator: "alice"},
}, listener)
model.loading = false
model.awaitingResponse = true
model.currentConversation = &clientpb.AIConversation{
ID: "conv-2",
OperatorName: "alice",
UpdatedAt: 100,
TurnState: clientpb.AIConversationTurnState_AI_TURN_STATE_IN_PROGRESS,
Messages: []*clientpb.AIConversationMessage{
{Role: "user", Content: "still waiting"},
},
}
model.conversations = []*clientpb.AIConversation{
{ID: "conv-2", Title: "Current"},
{ID: "conv-1", Title: "Fallback"},
}
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_DELETED,
ConversationID: "conv-2",
Conversation: &clientpb.AIConversation{
ID: "conv-2",
},
}})
nextModel := updated.(*aiModel)
if nextModel.loading {
t.Fatal("did not expect delete tombstone to trigger a reload")
}
if nextModel.currentConversation != nil {
t.Fatalf("expected deleted conversation to be cleared, got %+v", nextModel.currentConversation)
}
if conversationIndexByID(nextModel.conversations, "conv-2") >= 0 {
t.Fatalf("expected deleted conversation to be removed, got %+v", nextModel.conversations)
}
if cmd == nil {
t.Fatal("expected delete tombstone to continue listening for events")
}
}
type aiRPCServer struct {
rpcpb.UnimplementedSliverRPCServer
mu sync.Mutex
providersResp *clientpb.AIProviderConfigs
conversationsResp *clientpb.AIConversations
conversationByID map[string]*clientpb.AIConversation
saveConversationResp *clientpb.AIConversation
saveMessageResp *clientpb.AIConversationMessage
sessionsResp *clientpb.Sessions
beaconsResp *clientpb.Beacons
getProvidersCalls int
getConversationsCalls int
getSessionsCalls int
getBeaconsCalls int
getConversationReqs []*clientpb.AIConversationReq
deleteConversationReqs []*clientpb.AIConversationReq
saveConversationReqs []*clientpb.AIConversation
saveMessageReqs []*clientpb.AIConversationMessage
getConversationStart chan struct{}
getConversationWait <-chan struct{}
saveMessageStarted chan struct{}
saveMessageRelease <-chan struct{}
}
func (s *aiRPCServer) GetAIProviders(context.Context, *commonpb.Empty) (*clientpb.AIProviderConfigs, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getProvidersCalls++
if s.providersResp == nil {
return &clientpb.AIProviderConfigs{}, nil
}
return proto.Clone(s.providersResp).(*clientpb.AIProviderConfigs), nil
}
func (s *aiRPCServer) GetAIConversations(context.Context, *commonpb.Empty) (*clientpb.AIConversations, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getConversationsCalls++
if s.conversationsResp == nil {
return &clientpb.AIConversations{}, nil
}
return proto.Clone(s.conversationsResp).(*clientpb.AIConversations), nil
}
func (s *aiRPCServer) GetSessions(context.Context, *commonpb.Empty) (*clientpb.Sessions, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getSessionsCalls++
if s.sessionsResp == nil {
return &clientpb.Sessions{}, nil
}
return proto.Clone(s.sessionsResp).(*clientpb.Sessions), nil
}
func (s *aiRPCServer) GetBeacons(context.Context, *commonpb.Empty) (*clientpb.Beacons, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getBeaconsCalls++
if s.beaconsResp == nil {
return &clientpb.Beacons{}, nil
}
return proto.Clone(s.beaconsResp).(*clientpb.Beacons), nil
}
func (s *aiRPCServer) GetAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*clientpb.AIConversation, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getConversationReqs = append(s.getConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
if s.getConversationStart != nil {
select {
case s.getConversationStart <- struct{}{}:
default:
}
}
if s.getConversationWait != nil {
s.mu.Unlock()
<-s.getConversationWait
s.mu.Lock()
}
if s.conversationByID == nil {
return &clientpb.AIConversation{}, nil
}
conversation := s.conversationByID[req.GetID()]
if conversation == nil {
return &clientpb.AIConversation{}, nil
}
return proto.Clone(conversation).(*clientpb.AIConversation), nil
}
func (s *aiRPCServer) SaveAIConversation(_ context.Context, req *clientpb.AIConversation) (*clientpb.AIConversation, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.saveConversationReqs = append(s.saveConversationReqs, proto.Clone(req).(*clientpb.AIConversation))
if s.saveConversationResp == nil {
return proto.Clone(req).(*clientpb.AIConversation), nil
}
return proto.Clone(s.saveConversationResp).(*clientpb.AIConversation), nil
}
func (s *aiRPCServer) DeleteAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*commonpb.Empty, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.deleteConversationReqs = append(s.deleteConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
return &commonpb.Empty{}, nil
}
func (s *aiRPCServer) SaveAIConversationMessage(_ context.Context, req *clientpb.AIConversationMessage) (*clientpb.AIConversationMessage, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.saveMessageReqs = append(s.saveMessageReqs, proto.Clone(req).(*clientpb.AIConversationMessage))
if s.saveMessageStarted != nil {
select {
case s.saveMessageStarted <- struct{}{}:
default:
}
}
if s.saveMessageRelease != nil {
s.mu.Unlock()
<-s.saveMessageRelease
s.mu.Lock()
}
if s.saveMessageResp == nil {
return proto.Clone(req).(*clientpb.AIConversationMessage), nil
}
return proto.Clone(s.saveMessageResp).(*clientpb.AIConversationMessage), nil
}
func newAITestRPCClient(t *testing.T, srv rpcpb.SliverRPCServer) (rpcpb.SliverRPCClient, func()) {
t.Helper()
listener := bufconn.Listen(1024 * 1024)
grpcServer := grpc.NewServer()
rpcpb.RegisterSliverRPCServer(grpcServer, srv)
go func() {
_ = grpcServer.Serve(listener)
}()
dialer := func(context.Context, string) (net.Conn, error) { return listener.Dial() }
conn, err := grpc.DialContext(context.Background(), "bufnet",
grpc.WithContextDialer(dialer),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
grpcServer.Stop()
_ = listener.Close()
t.Fatalf("dial bufconn: %v", err)
}
cleanup := func() {
_ = conn.Close()
grpcServer.Stop()
_ = listener.Close()
}
return rpcpb.NewSliverRPCClient(conn), cleanup
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-251
View File
@@ -1,251 +0,0 @@
package alias
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"fmt"
"strconv"
"strings"
"github.com/spf13/cobra"
)
// Alias commands run with cobra's DisableFlagParsing enabled (see LoadAlias),
// the same treatment extension commands received for their arguments. Before
// that, an unknown flag such as the payload's own `-pid` was rejected outright
// by pflag ("unknown flag"), and operators had to inject cobra's "--"
// separator (documented on the wiki) to pass arguments through at all (#2264).
//
// With flag parsing disabled cobra hands Run the raw, unstripped slice, and we
// extract the Sliver-owned flags ourselves here. Everything we do not own is
// passed to the payload untouched, so all of these now work:
//
// amsi-bypass -pid 6969
// fakealias --process notepad.exe arg1 arg2
// fakealias -- --anything still-here (existing workaround, unchanged)
type ownedFlagKind int
const (
ownedBool ownedFlagKind = iota
ownedString
ownedUint32
ownedInt64
)
// ownedFlagSpec describes one Sliver-owned flag that splitAliasArgs extracts
// from the raw argument slice before the rest is handed to the alias payload.
type ownedFlagSpec struct {
long string // canonical pflag name, e.g. "process-arguments"
short string // shorthand letter, e.g. "A" ("" if the flag has none)
kind ownedFlagKind // how the value is consumed and validated
}
// baseAliasOwnedFlags are registered for every alias command.
var baseAliasOwnedFlags = []ownedFlagSpec{
{long: "process", short: "p", kind: ownedString},
{long: "process-arguments", short: "A", kind: ownedString},
{long: "ppid", short: "P", kind: ownedUint32},
{long: "save", short: "s", kind: ownedBool},
{long: "timeout", short: "t", kind: ownedInt64},
}
// assemblyAliasOwnedFlags are only owned when the alias wraps a .NET assembly;
// for every other alias they are unknown tokens and pass through to the payload.
var assemblyAliasOwnedFlags = []ownedFlagSpec{
{long: "method", short: "m", kind: ownedString},
{long: "class", short: "c", kind: ownedString},
{long: "app-domain", short: "d", kind: ownedString},
{long: "arch", short: "a", kind: ownedString},
{long: "in-process", short: "i", kind: ownedBool},
{long: "runtime", short: "r", kind: ownedString},
{long: "amsi-bypass", short: "M", kind: ownedBool},
{long: "etw-bypass", short: "E", kind: ownedBool},
}
// aliasOwnedFlagSpecs returns the flag set owned by a particular alias.
func aliasOwnedFlagSpecs(isAssembly bool) []ownedFlagSpec {
if !isAssembly {
return baseAliasOwnedFlags
}
specs := make([]ownedFlagSpec, 0, len(baseAliasOwnedFlags)+len(assemblyAliasOwnedFlags))
specs = append(specs, baseAliasOwnedFlags...)
return append(specs, assemblyAliasOwnedFlags...)
}
// ownedFlagMatch is a raw token that spelled one of the Sliver-owned flags.
type ownedFlagMatch struct {
spec ownedFlagSpec
value string // value from a "--flag=value" / "-f=value" spelling
inline bool // whether the token carried its value inline
}
// splitAliasArgs separates the Sliver-owned flags from the alias payload's own
// arguments in a raw (DisableFlagParsing) argument slice. It accepts the long
// forms --flag, --flag=value, --flag value and the shorthand forms -f, -f=value,
// -f value, mirroring pflag. Two deliberate differences from pflag:
//
// - a glued shorthand value (-pnotepad) does NOT match --process: tokens
// like the payload's own "-pid" must survive intact (#2264);
// - after a literal "--" every token is copied through verbatim, preserving
// the long-standing separator workaround.
//
// The returned map holds the raw string value of each flag that was provided;
// callers re-publish the values with applyOwnedAliasFlags.
func splitAliasArgs(raw []string, specs []ownedFlagSpec) (map[string]string, bool, []string, error) {
values := make(map[string]string)
extArgs := make([]string, 0, len(raw))
byLong, byShort := ownedFlagTables(specs)
passthrough := false
for i := 0; i < len(raw); i++ {
tok := raw[i]
// Once we have seen "--", copy everything through untouched.
if passthrough {
extArgs = append(extArgs, tok)
continue
}
if tok == "--" {
passthrough = true
continue
}
if tok == "--help" || tok == "-h" {
return values, true, extArgs, nil
}
match, ok := matchOwnedFlag(tok, byLong, byShort)
if !ok {
// Not one of ours (including glued shorthands and the payload's
// own flags): the alias owns the token, pass it through untouched.
extArgs = append(extArgs, tok)
continue
}
i = takeOwnedValue(match, raw, i, values)
}
if err := validateOwnedValues(specs, values); err != nil {
return nil, false, nil, err
}
return values, false, extArgs, nil
}
// ownedFlagTables indexes the specs by long name and shorthand.
func ownedFlagTables(specs []ownedFlagSpec) (byLong, byShort map[string]ownedFlagSpec) {
byLong = make(map[string]ownedFlagSpec, len(specs))
byShort = make(map[string]ownedFlagSpec, len(specs))
for _, spec := range specs {
byLong[spec.long] = spec
if spec.short != "" {
byShort[spec.short] = spec
}
}
return byLong, byShort
}
// matchOwnedFlag checks whether tok spells one of the Sliver-owned flags, in
// the long form (--name, --name=value) or shorthand form (-f, -f=value). A
// glued shorthand value (-pnotepad) deliberately does not match: pflag would
// read "-pid" as -p with value "id", which is exactly the mangling that broke
// #2264, so such tokens must fall through to the payload.
func matchOwnedFlag(tok string, byLong, byShort map[string]ownedFlagSpec) (ownedFlagMatch, bool) {
var match ownedFlagMatch
var name, value string
var hasValue, isShort bool
switch {
case strings.HasPrefix(tok, "--"):
name, value, hasValue = strings.Cut(tok[2:], "=")
case strings.HasPrefix(tok, "-") && len(tok) > 1:
name, value, hasValue = strings.Cut(tok[1:], "=")
isShort = len(name) == 1
default:
return match, false
}
spec, ok := byLong[name]
if isShort {
spec, ok = byShort[name]
}
if !ok {
return match, false
}
match.spec, match.value, match.inline = spec, value, hasValue
return match, true
}
// takeOwnedValue records the flag's value in values and returns the index of
// the last raw token it consumed. The "--flag value" / "-f value" spellings
// consume the following token; a bool without an inline value defaults to
// true; a value flag as the very last token is dropped and keeps its default,
// mirroring the extension behaviour.
func takeOwnedValue(match ownedFlagMatch, raw []string, i int, values map[string]string) int {
switch {
case match.spec.kind == ownedBool:
if !match.inline {
match.value = "true"
}
values[match.spec.long] = match.value
case match.inline:
values[match.spec.long] = match.value
case i+1 < len(raw):
values[match.spec.long] = raw[i+1]
return i + 1
}
return i
}
// validateOwnedValues rejects non-numeric values for the numeric flag kinds,
// the way pflag's typed values would have.
func validateOwnedValues(specs []ownedFlagSpec, values map[string]string) error {
for _, spec := range specs {
value, provided := values[spec.long]
if !provided {
continue
}
var err error
switch spec.kind {
case ownedUint32:
_, err = strconv.ParseUint(value, 10, 32)
case ownedInt64:
_, err = strconv.ParseInt(value, 10, 64)
}
if err != nil {
return fmt.Errorf("invalid value %q for --%s", value, spec.long)
}
}
return nil
}
// applyOwnedAliasFlags republishes the manually parsed Sliver-owned flag values
// onto the command's flag set. DisableFlagParsing prevents pflag from
// populating them, so readers such as runAliasCommand's Get* calls and
// ActiveTarget.Request (which derives the gRPC timeout from --timeout) would
// otherwise observe only the defaults.
func applyOwnedAliasFlags(cmd *cobra.Command, values map[string]string) error {
for name, value := range values {
flag := cmd.Flags().Lookup(name)
if flag == nil {
continue // e.g. an assembly flag on a non-assembly alias
}
if err := flag.Value.Set(value); err != nil {
return fmt.Errorf("invalid value %q for --%s: %w", value, name, err)
}
}
return nil
}
-259
View File
@@ -1,259 +0,0 @@
package alias
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"reflect"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// TestSplitAliasArgs covers the argument passthrough regression from #2264:
// the alias payload's own flags must reach the payload intact, while the
// Sliver-owned flags are extracted and validated.
func TestSplitAliasArgs(t *testing.T) {
base := aliasOwnedFlagSpecs(false)
assembly := aliasOwnedFlagSpecs(true)
cases := []struct {
name string
specs []ownedFlagSpec
in []string
values map[string]string
help bool
extArgs []string
wantErr bool
}{
{
name: "payload flag without -- separator (#2264 regression)",
specs: base,
in: []string{"-pid", "6969"},
values: map[string]string{},
extArgs: []string{"-pid", "6969"},
},
{
name: "glued shorthand is not ours and passes through",
specs: base,
in: []string{"-pid"},
values: map[string]string{},
extArgs: []string{"-pid"},
},
{
name: "payload flag with -- separator (existing workaround)",
specs: base,
in: []string{"--", "--pid", "6969"},
values: map[string]string{},
extArgs: []string{"--pid", "6969"},
},
{
name: "owned flag after -- belongs to the payload",
specs: base,
in: []string{"--", "--save"},
values: map[string]string{},
extArgs: []string{"--save"},
},
{
name: "long flag with separate value, then payload args",
specs: base,
in: []string{"--process", "notepad.exe", "arg1", "arg2"},
values: map[string]string{"process": "notepad.exe"},
extArgs: []string{"arg1", "arg2"},
},
{
name: "long flag with equals value",
specs: base,
in: []string{"--process=notepad.exe", "arg1"},
values: map[string]string{"process": "notepad.exe"},
extArgs: []string{"arg1"},
},
{
name: "shorthand with separate value",
specs: base,
in: []string{"-p", "notepad.exe", "arg1"},
values: map[string]string{"process": "notepad.exe"},
extArgs: []string{"arg1"},
},
{
name: "shorthand with equals value",
specs: base,
in: []string{"-p=notepad.exe"},
values: map[string]string{"process": "notepad.exe"},
extArgs: []string{},
},
{
name: "bool shorthand and long false form",
specs: base,
in: []string{"-s", "--save=false", "arg1"},
values: map[string]string{"save": "false"},
extArgs: []string{"arg1"},
},
{
name: "quoted value containing spaces stays a single argument",
specs: base,
in: []string{"-A", "-kalc -kdisable", "arg1"},
values: map[string]string{"process-arguments": "-kalc -kdisable"},
extArgs: []string{"arg1"},
},
{
name: "valid uint32 and int64 values",
specs: base,
in: []string{"-P", "1234", "--timeout=90"},
values: map[string]string{"ppid": "1234", "timeout": "90"},
extArgs: []string{},
},
{
name: "invalid uint32 value errors",
specs: base,
in: []string{"--ppid", "notanumber"},
wantErr: true,
},
{
name: "invalid int64 value errors",
specs: base,
in: []string{"--timeout=x"},
wantErr: true,
},
{
name: "assembly flags are owned for assembly aliases",
specs: assembly,
in: []string{"--method", "Run", "--class", "Prog", "-i", "-M", "--arch", "x64", "payloadArg"},
values: map[string]string{
"method": "Run", "class": "Prog", "in-process": "true",
"amsi-bypass": "true", "arch": "x64",
},
extArgs: []string{"payloadArg"},
},
{
name: "assembly flags pass through for non-assembly aliases",
specs: base,
in: []string{"--method", "Run"},
values: map[string]string{},
extArgs: []string{"--method", "Run"},
},
{
name: "help flags are detected",
specs: base,
in: []string{"--help"},
help: true,
values: map[string]string{},
extArgs: []string{},
},
{
name: "short help is detected",
specs: base,
in: []string{"-h"},
help: true,
values: map[string]string{},
extArgs: []string{},
},
{
name: "value flag as last token keeps its default",
specs: base,
in: []string{"arg1", "--process"},
values: map[string]string{},
extArgs: []string{"arg1"},
},
{
name: "empty input",
specs: base,
in: nil,
values: map[string]string{},
extArgs: []string{},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
values, help, extArgs, err := splitAliasArgs(tc.in, tc.specs)
if tc.wantErr {
if err == nil {
t.Fatalf("expected error, got values=%#v help=%v extArgs=%#v", values, help, extArgs)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if help != tc.help {
t.Errorf("help: got %v, want %v", help, tc.help)
}
if !reflect.DeepEqual(values, tc.values) {
t.Errorf("values: got %#v, want %#v", values, tc.values)
}
if !reflect.DeepEqual(extArgs, tc.extArgs) {
t.Errorf("extArgs: got %#v, want %#v", extArgs, tc.extArgs)
}
})
}
}
// TestApplyOwnedAliasFlags checks that manually parsed values are re-published
// on the command's flag set for every value type an alias can own.
func TestApplyOwnedAliasFlags(t *testing.T) {
cmd := &cobra.Command{}
f := pflag.NewFlagSet("alias-test", pflag.ContinueOnError)
f.StringP("process", "p", "", "host process")
f.StringP("process-arguments", "A", "", "host process arguments")
f.Uint32P("ppid", "P", 0, "parent pid")
f.BoolP("save", "s", false, "save output")
f.Int64P("timeout", "t", 60, "timeout")
f.BoolP("amsi-bypass", "M", false, "amsi bypass")
cmd.Flags().AddFlagSet(f)
err := applyOwnedAliasFlags(cmd, map[string]string{
"process": "notepad.exe",
"process-arguments": "-k foo",
"ppid": "1234",
"save": "true",
"timeout": "90",
"amsi-bypass": "true",
})
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
for name, want := range map[string]interface{}{
"process": "notepad.exe", "process-arguments": "-k foo",
} {
got, _ := cmd.Flags().GetString(name)
if got != want.(string) {
t.Errorf("%s: got %q, want %q", name, got, want)
}
}
if ppid, _ := cmd.Flags().GetUint32("ppid"); ppid != 1234 {
t.Errorf("ppid: got %d, want 1234", ppid)
}
if save, _ := cmd.Flags().GetBool("save"); !save {
t.Error("save: got false, want true")
}
if timeout, _ := cmd.Flags().GetInt64("timeout"); timeout != 90 {
t.Errorf("timeout: got %d, want 90", timeout)
}
if amsi, _ := cmd.Flags().GetBool("amsi-bypass"); !amsi {
t.Error("amsi-bypass: got false, want true")
}
// Unset flags keep their defaults.
if inProc := cmd.Flags().Lookup("in-process"); inProc != nil {
t.Error("in-process should not be registered on this command")
}
}
+7 -37
View File
@@ -215,6 +215,8 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
longHelp.WriteString(fmt.Sprintf("%s (%s):\t%s%s", strings.ToUpper(arg.Name), aType, optStr, arg.Desc))
}
}
longHelp.WriteString("\n\n⚠️ If you're having issues passing arguments to the alias please read:\n")
longHelp.WriteString("https://github.com/BishopFox/sliver/wiki/Aliases-&-Extensions#aliases-command-parsing")
// for each alias command, add a new app command
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
@@ -225,14 +227,9 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
Run: func(cmd *cobra.Command, args []string) {
runAliasCommand(cmd, con, args)
},
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
// DisableFlagParsing keeps the alias payload's own flags (which are
// unknown to pflag) from being rejected or swallowed; we split the
// Sliver-owned flags out of the raw slice ourselves in
// runAliasCommand (#2264, same treatment as extensions).
DisableFlagParsing: true,
GroupID: consts.AliasHelpGroup,
Annotations: makeAliasPlatformFilters(aliasManifest),
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
GroupID: consts.AliasHelpGroup,
Annotations: makeAliasPlatformFilters(aliasManifest),
}
if aliasManifest.IsAssembly {
@@ -253,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.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
addAliasCmd.Flags().AddFlagSet(f)
// Setup completions for alias arguments
@@ -336,27 +333,6 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
return
}
aliasManifest := loadedAlias.Manifest
// The command runs with DisableFlagParsing, so args is the raw, unstripped
// slice. Separate the Sliver-owned flags from the payload's own arguments
// and re-publish them on the flag set (so the Get* readers below and
// Request()'s --timeout still work), then hand only the payload arguments
// to the execution paths (#2264, same treatment as extensions in #2323).
ownedValues, helpRequested, payloadArgs, err := splitAliasArgs(args, aliasOwnedFlagSpecs(aliasManifest.IsAssembly))
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if helpRequested {
_ = cmd.Help()
return
}
if err := applyOwnedAliasFlags(cmd, ownedValues); err != nil {
con.PrintErrorf("%s\n", err)
return
}
args = payloadArgs
binPath, err := aliasManifest.getFileForTarget(cmd.Name(), goos, goarch)
if err != nil {
con.PrintErrorf("Fail to find alias file: %s\n", err)
@@ -424,13 +400,7 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
var outFilePath *os.File
if save, _ := cmd.Flags().GetBool("save"); save {
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)))
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(session.GetHostname())))
outFilePath, err = os.CreateTemp("", outFile)
if err != nil {
con.PrintErrorf("%s\n", err)
+1 -1
View File
@@ -164,7 +164,7 @@ func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
// Something is wrong with this entry
pkgCache.Delete(key)
pkgCache.Delete(value)
return true
}
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
-18
View File
@@ -1,23 +1,5 @@
package armory
/*
Sliver Implant Framework
Copyright (C) 2026 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
-18
View File
@@ -1,23 +1,5 @@
package armory
/*
Sliver Implant Framework
Copyright (C) 2026 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"fmt"
"os"
+1 -1
View File
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
},
}
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("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")
-31
View File
@@ -1,31 +0,0 @@
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)
},
}
}
-36
View File
@@ -1,36 +0,0 @@
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
-199
View File
@@ -1,199 +0,0 @@
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")
}
}
-8
View File
@@ -114,14 +114,6 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("shikata-ga-nai", "S", false, "encode shellcode using shikata ga nai prior to execution")
f.StringP("architecture", "A", "amd64", "architecture of the shellcode: 386, amd64 (used with --shikata-ga-nai flag)")
f.Uint32P("iterations", "I", 1, "number of encoding iterations (used with --shikata-ga-nai flag)")
f.Uint32("shellcode-entropy", 1, "Shellcode entropy (Donut: 1=none, 2=random names, 3=random+encrypt) (windows PE input only)")
f.Bool("shellcode-compress", false, "Enable shellcode compression (aPLib) (windows PE input only)")
f.Uint32("shellcode-exitopt", 1, "Shellcode exit option (Donut: 1=exit thread, 2=exit process, 3=block) (windows PE input only)")
f.Uint32("shellcode-bypass", 3, "Shellcode bypass mode (Donut: 1=none, 2=abort, 3=continue) (windows PE input only)")
f.Uint32("shellcode-headers", 1, "Shellcode headers handling (Donut: 1=overwrite, 2=keep) (windows PE input only)")
f.Bool("shellcode-thread", false, "Run unmanaged EXE entrypoint as a new thread (Donut) (windows PE input only)")
f.Bool("shellcode-unicode", false, "Use Unicode command line for unmanaged DLL entrypoints (Donut) (windows PE input only)")
f.Uint32("shellcode-oep", 0, "Override original entry point (OEP) (Donut) (windows PE input only)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
+1 -219
View File
@@ -20,44 +20,27 @@ package exec
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/Binject/debug/pe"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
wasmdonut "github.com/sliverarmory/wasm-donut"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto"
)
const (
defaultExecuteShellcodeDonutEntropy = wasmdonut.DonutEntropyNone
defaultExecuteShellcodeDonutCompress = wasmdonut.DonutCompressNone
defaultExecuteShellcodeDonutExitOpt = wasmdonut.DonutExitThread
defaultExecuteShellcodeDonutBypass = wasmdonut.DonutBypassContinue
defaultExecuteShellcodeDonutHeaders = wasmdonut.DonutHeadersOverwrite
imageFileDLLMask = 0x2000
)
// ExecuteShellcodeCmd - Execute shellcode in-memory.
func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
}
targetName := activeTargetName(session, beacon)
targetOS := activeTargetOS(session, beacon)
targetArch := activeTargetArch(session, beacon)
rwxPages, _ := cmd.Flags().GetBool("rwx-pages")
interactive, _ := cmd.Flags().GetBool("interactive")
@@ -77,27 +60,6 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
con.PrintErrorf("Cannot use both `--pid` and `--interactive`\n")
return
}
shellcodeConfig, shellcodeFlagsChanged, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
shouldConvertPE, isDLLHint := shouldConvertExecuteShellcodePE(shellcodePath, shellcodeFlagsChanged)
if shouldConvertPE {
if targetOS != "windows" {
con.PrintErrorf("PE input and --shellcode-* options are only supported for Windows targets in execute-shellcode\n")
return
}
con.PrintInfof("Converting PE input to shellcode ...\n")
shellcodeBin, err = donutShellcodeFromPE(shellcodeBin, targetArch, isDLLHint, shellcodeConfig)
if err != nil {
con.PrintErrorf("Failed to convert PE input to shellcode: %s\n", err)
return
}
}
shikataGaNai, _ := cmd.Flags().GetBool("shikata-ga-nai")
if shikataGaNai {
if !rwxPages {
@@ -134,7 +96,7 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
return
}
ctrl := make(chan bool)
msg := fmt.Sprintf("Sending shellcode to %s ...", targetName)
msg := fmt.Sprintf("Sending shellcode to %s ...", session.GetName())
con.SpinUntil(msg, ctrl)
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
Data: shellcodeBin,
@@ -173,186 +135,6 @@ func PrintExecuteShellcode(task *sliverpb.Task, con *console.SliverClient) {
}
}
func parseExecuteShellcodeFlags(cmd *cobra.Command) (*clientpb.ShellcodeConfig, bool, error) {
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
shellcodeCompressEnabled, _ := cmd.Flags().GetBool("shellcode-compress")
shellcodeExitOpt, _ := cmd.Flags().GetUint32("shellcode-exitopt")
shellcodeBypass, _ := cmd.Flags().GetUint32("shellcode-bypass")
shellcodeHeaders, _ := cmd.Flags().GetUint32("shellcode-headers")
shellcodeThread, _ := cmd.Flags().GetBool("shellcode-thread")
shellcodeUnicode, _ := cmd.Flags().GetBool("shellcode-unicode")
shellcodeOEP, _ := cmd.Flags().GetUint32("shellcode-oep")
anyChanged := cmd.Flags().Changed("shellcode-entropy") ||
cmd.Flags().Changed("shellcode-compress") ||
cmd.Flags().Changed("shellcode-exitopt") ||
cmd.Flags().Changed("shellcode-bypass") ||
cmd.Flags().Changed("shellcode-headers") ||
cmd.Flags().Changed("shellcode-thread") ||
cmd.Flags().Changed("shellcode-unicode") ||
cmd.Flags().Changed("shellcode-oep")
if shellcodeEntropy < 1 || shellcodeEntropy > 3 {
return nil, false, fmt.Errorf("shellcode-entropy must be between 1 and 3")
}
if shellcodeExitOpt < 1 || shellcodeExitOpt > 3 {
return nil, false, fmt.Errorf("shellcode-exitopt must be between 1 and 3")
}
if shellcodeBypass < 1 || shellcodeBypass > 3 {
return nil, false, fmt.Errorf("shellcode-bypass must be between 1 and 3")
}
if shellcodeHeaders < 1 || shellcodeHeaders > 2 {
return nil, false, fmt.Errorf("shellcode-headers must be 1 or 2")
}
shellcodeCompress := uint32(1)
if shellcodeCompressEnabled {
shellcodeCompress = 2
}
return &clientpb.ShellcodeConfig{
Entropy: shellcodeEntropy,
Compress: shellcodeCompress,
ExitOpt: shellcodeExitOpt,
Bypass: shellcodeBypass,
Headers: shellcodeHeaders,
Thread: shellcodeThread,
Unicode: shellcodeUnicode,
OEP: shellcodeOEP,
}, anyChanged, nil
}
func shouldConvertExecuteShellcodePE(shellcodePath string, shellcodeFlagsChanged bool) (bool, bool) {
ext := strings.ToLower(filepath.Ext(shellcodePath))
isPEByExt := ext == ".exe" || ext == ".dll"
return isPEByExt || shellcodeFlagsChanged, ext == ".dll"
}
func activeTargetName(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetName()
}
if beacon != nil {
return beacon.GetName()
}
return "target"
}
func activeTargetOS(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return strings.ToLower(session.GetOS())
}
if beacon != nil {
return strings.ToLower(beacon.GetOS())
}
return ""
}
func activeTargetArch(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetArch()
}
if beacon != nil {
return beacon.GetArch()
}
return ""
}
type executeShellcodeDonutOptions struct {
entropy int
compress int
exitOpt int
bypass int
headers int
thread bool
unicode bool
oep uint32
}
func normalizeExecuteShellcodeDonutConfig(config *clientpb.ShellcodeConfig) executeShellcodeDonutOptions {
opts := executeShellcodeDonutOptions{
entropy: defaultExecuteShellcodeDonutEntropy,
compress: defaultExecuteShellcodeDonutCompress,
exitOpt: defaultExecuteShellcodeDonutExitOpt,
bypass: defaultExecuteShellcodeDonutBypass,
headers: defaultExecuteShellcodeDonutHeaders,
}
if config == nil {
return opts
}
if config.Entropy >= 1 && config.Entropy <= 3 {
opts.entropy = int(config.Entropy)
}
if config.Compress >= 1 && config.Compress <= 2 {
opts.compress = int(config.Compress)
}
if config.ExitOpt >= 1 && config.ExitOpt <= 3 {
opts.exitOpt = int(config.ExitOpt)
}
if config.Bypass >= 1 && config.Bypass <= 3 {
opts.bypass = int(config.Bypass)
}
if config.Headers >= 1 && config.Headers <= 2 {
opts.headers = int(config.Headers)
}
opts.thread = config.Thread
opts.unicode = config.Unicode
if config.OEP > 0 {
opts.oep = config.OEP
}
return opts
}
func donutShellcodeFromPE(data []byte, arch string, isDLLHint bool, config *clientpb.ShellcodeConfig) ([]byte, error) {
peFile, err := pe.NewFile(bytes.NewReader(data))
if err != nil {
return nil, err
}
isDLL := isDLLHint || ((peFile.FileHeader.Characteristics & imageFileDLLMask) != 0)
ext := ".exe"
if isDLL {
ext = ".dll"
}
opts := normalizeExecuteShellcodeDonutConfig(config)
result, err := wasmdonut.Generate(context.Background(), data, ext, wasmdonut.GenerateOptions{
Ext: ext,
Arch: getExecuteShellcodeDonutArch(arch),
Bypass: opts.bypass,
Headers: opts.headers,
Entropy: opts.entropy,
Compress: opts.compress,
ExitOpt: opts.exitOpt,
Thread: opts.thread,
Unicode: opts.unicode,
OEP: opts.oep,
})
if err != nil {
return nil, err
}
return addExecuteShellcodeStackCheck(result.Loader), nil
}
func getExecuteShellcodeDonutArch(arch string) int {
donutArch := wasmdonut.DonutArchX84
switch strings.ToLower(arch) {
case "x32", "x86", "386":
donutArch = wasmdonut.DonutArchX86
case "x64", "amd64":
donutArch = wasmdonut.DonutArchX64
case "x84":
donutArch = wasmdonut.DonutArchX84
}
return donutArch
}
func addExecuteShellcodeStackCheck(shellcode []byte) []byte {
stackCheckPrologue := []byte{
0x48, 0x83, 0xE4, 0xF0, // and rsp,0xfffffffffffffff0
0x48, 0x83, 0xC4, 0x08, // add rsp,0x8
}
return append(stackCheckPrologue, shellcode...)
}
func executeInteractive(cmd *cobra.Command, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverClient) {
// Check active session
session := con.ActiveTarget.GetSessionInteractive()
@@ -1,100 +0,0 @@
package exec
import (
"testing"
"github.com/spf13/cobra"
)
func TestParseExecuteShellcodeFlagsDefaults(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if changed {
t.Fatalf("expected changed=false with defaults")
}
if cfg == nil {
t.Fatalf("expected non-nil config")
}
if cfg.Entropy != 1 || cfg.Compress != 1 || cfg.ExitOpt != 1 || cfg.Bypass != 3 || cfg.Headers != 1 {
t.Fatalf("unexpected defaults: %+v", cfg)
}
if cfg.Thread || cfg.Unicode || cfg.OEP != 0 {
t.Fatalf("unexpected default bool/OEP values: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsChanged(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-entropy", "2"); err != nil {
t.Fatalf("set shellcode-entropy: %v", err)
}
if err := cmd.Flags().Set("shellcode-compress", "true"); err != nil {
t.Fatalf("set shellcode-compress: %v", err)
}
if err := cmd.Flags().Set("shellcode-thread", "true"); err != nil {
t.Fatalf("set shellcode-thread: %v", err)
}
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if !changed {
t.Fatalf("expected changed=true")
}
if cfg.Entropy != 2 || cfg.Compress != 2 || !cfg.Thread {
t.Fatalf("unexpected changed config: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsValidation(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-bypass", "9"); err != nil {
t.Fatalf("set shellcode-bypass: %v", err)
}
if _, _, err := parseExecuteShellcodeFlags(cmd); err == nil {
t.Fatalf("expected validation error for shellcode-bypass")
}
}
func TestShouldConvertExecuteShellcodePE(t *testing.T) {
testCases := []struct {
name string
path string
flagsChanged bool
wantConvert bool
wantDLL bool
}{
{name: "exe by extension", path: "/tmp/payload.exe", flagsChanged: false, wantConvert: true, wantDLL: false},
{name: "dll by extension", path: "/tmp/payload.dll", flagsChanged: false, wantConvert: true, wantDLL: true},
{name: "raw shellcode", path: "/tmp/payload.bin", flagsChanged: false, wantConvert: false, wantDLL: false},
{name: "flags force conversion", path: "/tmp/payload.bin", flagsChanged: true, wantConvert: true, wantDLL: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotConvert, gotDLL := shouldConvertExecuteShellcodePE(tc.path, tc.flagsChanged)
if gotConvert != tc.wantConvert || gotDLL != tc.wantDLL {
t.Fatalf("unexpected result convert=%v dll=%v, expected convert=%v dll=%v", gotConvert, gotDLL, tc.wantConvert, tc.wantDLL)
}
})
}
}
func newExecuteShellcodeFlagsTestCommand(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "execute-shellcode"}
cmd.Flags().Uint32("shellcode-entropy", 1, "")
cmd.Flags().Bool("shellcode-compress", false, "")
cmd.Flags().Uint32("shellcode-exitopt", 1, "")
cmd.Flags().Uint32("shellcode-bypass", 3, "")
cmd.Flags().Uint32("shellcode-headers", 1, "")
cmd.Flags().Bool("shellcode-thread", false, "")
cmd.Flags().Bool("shellcode-unicode", false, "")
cmd.Flags().Uint32("shellcode-oep", 0, "")
return cmd
}
-123
View File
@@ -1,123 +0,0 @@
package extensions
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"strconv"
"strings"
"github.com/spf13/cobra"
)
// splitExtensionArgs separates Sliver-owned flags (--save, --timeout, --help)
// from the extension's own arguments in a raw argument slice.
//
// Extension commands run with cobra's DisableFlagParsing enabled (see
// ExtensionRegisterCommand). Without it, pflag parses the command line first
// and, because the extension's own flags (e.g. --type, --fqdn) are unknown to
// pflag, ParseErrorsWhitelist.UnknownFlags causes pflag to silently drop each
// unknown flag together with its value; the BOF argument parser never saw them
// unless the operator injected cobra's "--" separator (#2309). Running with
// flag parsing disabled hands us the raw, unstripped slice, so we separate the
// Sliver-owned flags ourselves here.
//
// A literal "--" stops Sliver-flag scanning; every token after it is passed
// through verbatim, preserving the long-standing workaround syntax. Unknown
// tokens (the extension's named flags, their values and positional arguments)
// are never consumed or reordered, so both forms work:
//
// delegationbof --timeout 30 --type 6 --fqdn child.htb.local
// delegationbof -- --type 6 --fqdn child.htb.local
func splitExtensionArgs(raw []string) (save bool, timeout int64, help bool, extArgs []string) {
timeout = defaultTimeout
extArgs = make([]string, 0, len(raw))
passthrough := false
for i := 0; i < len(raw); i++ {
tok := raw[i]
// Once we have seen "--", copy everything through untouched so the
// existing `ext -- --foo bar` syntax keeps working identically.
if passthrough {
extArgs = append(extArgs, tok)
continue
}
switch {
case tok == "--":
passthrough = true
case tok == "--help" || tok == "-h":
help = true
case tok == "--save" || tok == "-s":
save = true
case strings.HasPrefix(tok, "--save="):
save = parseBoolValue(tok[len("--save="):], true)
case tok == "--timeout" || tok == "-t":
// Consume the following token as the value, mirroring pflag. If no
// token follows, leave the default in place rather than erroring.
if i+1 < len(raw) {
if n, err := strconv.ParseInt(raw[i+1], 10, 64); err == nil {
timeout = n
}
i++
}
case strings.HasPrefix(tok, "--timeout="):
if n, err := strconv.ParseInt(tok[len("--timeout="):], 10, 64); err == nil {
timeout = n
}
default:
// Not a Sliver-owned flag: hand it to the extension parser untouched.
extArgs = append(extArgs, tok)
}
}
return save, timeout, help, extArgs
}
// applyOwnedFlags republishes the manually parsed Sliver-owned flag values back
// onto the command's flag set. DisableFlagParsing prevents pflag from populating
// them, so helpers that read them via cmd.Flags() (notably ActiveTarget.Request,
// which derives the gRPC timeout from --timeout) would otherwise observe only the
// defaults.
func applyOwnedFlags(cmd *cobra.Command, save bool, timeout int64) {
if flag := cmd.Flags().Lookup("timeout"); flag != nil {
_ = flag.Value.Set(strconv.FormatInt(timeout, 10))
}
if flag := cmd.Flags().Lookup("save"); flag != nil {
_ = flag.Value.Set(strconv.FormatBool(save))
}
}
// parseBoolValue parses a --flag=value bool, defaulting to defVal when the value
// is empty (the bare-flag form). It accepts the same spellings pflag does.
func parseBoolValue(v string, defVal bool) bool {
switch strings.ToLower(v) {
case "", "true", "t", "1":
return true
case "false", "f", "0":
return false
}
return defVal
}
-149
View File
@@ -1,149 +0,0 @@
package extensions
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"reflect"
"testing"
)
// TestSplitExtensionArgs covers the argument-passthrough regression from #2309:
// the extension's own named flags must reach the BOF parser intact regardless of
// the cobra "--" separator, while Sliver-owned flags are still honoured.
func TestSplitExtensionArgs(t *testing.T) {
cases := []struct {
name string
in []string
save bool
timeout int64
help bool
extArgs []string
}{
{
name: "named extension flags without -- separator (#2309 regression)",
in: []string{"--type", "6", "--fqdn", "child.htb.local"},
timeout: defaultTimeout,
extArgs: []string{"--type", "6", "--fqdn", "child.htb.local"},
},
{
name: "named extension flags with -- separator (existing workaround)",
in: []string{"--", "--type", "6", "--fqdn", "child.htb.local"},
timeout: defaultTimeout,
extArgs: []string{"--type", "6", "--fqdn", "child.htb.local"},
},
{
name: "sliver-owned flags precede extension arguments",
in: []string{"--timeout", "30", "--save", "--type", "6"},
save: true,
timeout: 30,
extArgs: []string{"--type", "6"},
},
{
name: "quoted value containing spaces stays a single argument",
in: []string{"--name", "hello world", "--type", "6"},
timeout: defaultTimeout,
extArgs: []string{"--name", "hello world", "--type", "6"},
},
{
name: "positional extension arguments are preserved",
in: []string{"foo", "bar", "baz"},
timeout: defaultTimeout,
extArgs: []string{"foo", "bar", "baz"},
},
{
name: "single-dash and double-dash forms are preserved",
in: []string{"-type", "6", "--fqdn", "child.htb.local"},
timeout: defaultTimeout,
extArgs: []string{"-type", "6", "--fqdn", "child.htb.local"},
},
{
name: "equals form for timeout",
in: []string{"--timeout=120", "--type", "6"},
timeout: 120,
extArgs: []string{"--type", "6"},
},
{
name: "short -s save flag",
in: []string{"-s", "--type", "6"},
save: true,
timeout: defaultTimeout,
extArgs: []string{"--type", "6"},
},
{
name: "help flag is detected",
in: []string{"--help"},
help: true,
timeout: defaultTimeout,
extArgs: []string{},
},
{
name: "empty input",
in: nil,
timeout: defaultTimeout,
extArgs: []string{},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
save, timeout, help, extArgs := splitExtensionArgs(tc.in)
if save != tc.save {
t.Errorf("save: got %v, want %v", save, tc.save)
}
if timeout != tc.timeout {
t.Errorf("timeout: got %d, want %d", timeout, tc.timeout)
}
if help != tc.help {
t.Errorf("help: got %v, want %v", help, tc.help)
}
if !reflect.DeepEqual(extArgs, tc.extArgs) {
t.Errorf("extArgs: got %#v, want %#v", extArgs, tc.extArgs)
}
})
}
}
// TestParseBoolValue checks the --flag=value helper used for --save=value.
func TestParseBoolValue(t *testing.T) {
cases := []struct {
in string
want bool
}{
{"", true},
{"true", true},
{"TRUE", true},
{"1", true},
{"t", true},
{"false", false},
{"0", false},
{"no", false}, // unsupported spellings keep the default
}
for _, tc := range cases {
got := parseBoolValue(tc.in, true)
// "no" is not a pflag spelling, so it falls through to the default (true).
want := tc.want
if tc.in == "no" {
want = true
}
if got != want {
t.Errorf("parseBoolValue(%q): got %v, want %v", tc.in, got, want)
}
}
}
+8 -14
View File
@@ -3,8 +3,6 @@ package extensions
import (
"encoding/json"
"testing"
"github.com/bishopfox/sliver/util"
)
/*
@@ -116,8 +114,6 @@ 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)
@@ -154,8 +150,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 != expectedPath {
t.Errorf("Expected path '%s', got '%s'", expectedPath, extCmd.Files[0].Path)
if extCmd.Files[0].Path != "/foo/test1.dll" {
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extCmd.Files[0].Path)
}
}
@@ -182,16 +178,14 @@ 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 != expectedPath {
t.Errorf("Expected path '%s', got '%s'", expectedPath, extManifest2.Files[0].Path)
if extManifest2.Files[0].Path != "/foo/test1.dll" {
t.Errorf("Expected path '/foo/test1.dll', got '%s'", 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)
@@ -212,11 +206,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 != expectedPath { // path normalization is platform-specific
t.Errorf("expected path %s, got %s", expectedPath, mextManifest.ExtCommand[0].Files[0].Path)
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[1].Files[0].Path != expectedPath { // path normalization is platform-specific
t.Errorf("expected path %s, got %s", expectedPath, mextManifest.ExtCommand[1].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)
}
//maybe some more? args?
}
+4 -19
View File
@@ -370,19 +370,16 @@ func ExtensionRegisterCommand(extCmd *ExtCommand, cmd *cobra.Command, con *conso
Run: func(cmd *cobra.Command, args []string) {
runExtensionCmd(cmd, con, args)
},
GroupID: consts.ExtensionHelpGroup,
// DisableFlagParsing keeps the extension's own flags (which are unknown to
// pflag) from being silently stripped together with their values. We split
// Sliver-owned flags out of the raw slice ourselves in runExtensionCmd (#2309).
DisableFlagParsing: true,
Annotations: makeCommandPlatformFilters(extCmd),
GroupID: consts.ExtensionHelpGroup,
Annotations: makeCommandPlatformFilters(extCmd),
}
// Flags
f := pflag.NewFlagSet(extCmd.CommandName, pflag.ContinueOnError)
f.BoolP("save", "s", false, "Save output to disk")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
extensionCmd.Flags().AddFlagSet(f)
extensionCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
// Completions
comps := carapace.Gen(extensionCmd)
@@ -532,18 +529,6 @@ func loadDep(goos string, goarch string, depName string, cmd *cobra.Command, con
}
func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// The command runs with DisableFlagParsing, so args is the raw, unstripped
// slice. Separate Sliver-owned flags from the extension's own arguments and
// re-publish them on the flag set (so Request()'s --timeout still works),
// then feed only the extension arguments to the BOF/DLL parsers (#2309).
save, timeout, helpRequested, extArgs := splitExtensionArgs(args)
if helpRequested {
_ = cmd.Help()
return
}
applyOwnedFlags(cmd, save, timeout)
args = extArgs
var (
err error
extensionArgs []byte
+3 -10
View File
@@ -36,7 +36,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.PayloadsHelpGroup,
}
flags.Bind("generate", true, generateCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
// Session flags and completions.
@@ -159,7 +159,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("save", "s", "", "directory/file to the binary to")
f.String("shellcode-encoder", "", "shellcode encoder to apply (optional; see `shellcode-encoders`)")
f.StringP("name", "n", "", "Implant name")
f.BoolP("external-builder", "E", false, "use an external builder")
})
flags.BindFlagCompletions(profilesGenerateCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
@@ -340,13 +340,6 @@ 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")
@@ -367,7 +360,7 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
f.StringP("limit-locale", "L", "", "limit execution to hosts that match this locale")
bindSpoofMetadataFlag(f)
f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'archive' (for Go c-archives), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)")
f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)")
// Shellcode generation options:
// - Windows: Donut
-24
View File
@@ -24,30 +24,6 @@ func TestGenerateSaveFlagCompletionRegistered(t *testing.T) {
assertFlagHasDefaultFileCompletion(t, beaconCmd, "save")
}
func TestProfilesGenerateExternalBuilderFlagRegistered(t *testing.T) {
cmds := Commands(&console.SliverClient{})
profilesCmd := commandByUse(cmds, consts.ProfilesStr)
if profilesCmd == nil {
t.Fatalf("missing %q command", consts.ProfilesStr)
}
profilesGenerateCmd := commandByUse(profilesCmd.Commands(), consts.GenerateStr)
if profilesGenerateCmd == nil {
t.Fatalf("missing %q subcommand under %q", consts.GenerateStr, consts.ProfilesStr)
}
flag := profilesGenerateCmd.Flags().Lookup("external-builder")
if flag == nil {
t.Fatalf("%q missing %q flag", profilesGenerateCmd.Use, "external-builder")
}
if flag.Shorthand != "E" {
t.Fatalf("%q %q flag shorthand mismatch: got=%q want=%q", profilesGenerateCmd.Use, "external-builder", flag.Shorthand, "E")
}
if flag.DefValue != "false" {
t.Fatalf("%q %q flag default mismatch: got=%q want=%q", profilesGenerateCmd.Use, "external-builder", flag.DefValue, "false")
}
}
func assertFlagHasDefaultFileCompletion(t *testing.T, cmd *cobra.Command, flag string) {
t.Helper()
-29
View File
@@ -254,8 +254,6 @@ func nameOfOutputFormat(value clientpb.OutputFormat) string {
return "Service"
case clientpb.OutputFormat_SHARED_LIB:
return "Shared Library"
case clientpb.OutputFormat_GO_ARCHIVE:
return "Go Archive"
case clientpb.OutputFormat_SHELLCODE:
return "Shellcode"
default:
@@ -391,9 +389,6 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
configFormat = clientpb.OutputFormat_SHARED_LIB
isSharedLib = true
runAtLoad, _ = cmd.Flags().GetBool("run-at-load")
case "archive":
configFormat = clientpb.OutputFormat_GO_ARCHIVE
isSharedLib = true
case "shellcode":
configFormat = clientpb.OutputFormat_SHELLCODE
isShellcode = true
@@ -488,23 +483,6 @@ 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{
@@ -549,13 +527,6 @@ 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
+1 -1
View File
@@ -197,7 +197,7 @@ func OSCompleter(con *console.SliverClient) carapace.Action {
func FormatCompleter() carapace.Action {
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
return carapace.ActionValues([]string{
"exe", "shared", "archive", "service", "shellcode",
"exe", "shared", "service", "shellcode",
}...).Tag("implant format")
})
}
@@ -1,13 +0,0 @@
package generate
import (
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
func TestNameOfOutputFormatGoArchive(t *testing.T) {
if got := nameOfOutputFormat(clientpb.OutputFormat_GO_ARCHIVE); got != "Go Archive" {
t.Fatalf("nameOfOutputFormat(GO_ARCHIVE) = %q, want %q", got, "Go Archive")
}
}
+3 -22
View File
@@ -19,7 +19,6 @@ package generate
*/
import (
"fmt"
"os"
"strings"
@@ -82,27 +81,9 @@ func ProfilesGenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []s
profile.Config.SGNEnabled = encoder == clientpb.ShellcodeEncoder_SHIKATA_GA_NAI
}
}
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
_, err := compile(implantName, profile.Config, nil, save, con)
if err != nil {
return
}
} else {
_, err := externalBuild(implantName, profile.Config, nil, save, con)
if err != nil {
switch err {
case ErrNoExternalBuilder:
con.PrintErrorf("There are no external builders currently connected to the server\n")
con.PrintErrorf("See 'builders' command for more information\n")
case ErrNoValidBuilders:
con.PrintErrorf("There are external builders connected to the server, but none can build the target you specified\n")
con.PrintErrorf("Invalid target %s\n", fmt.Sprintf("%s:%s/%s", profile.Config.Format, profile.Config.GOOS, profile.Config.GOARCH))
con.PrintErrorf("See 'builders' command for more information\n")
default:
con.PrintErrorf("%s\n", err)
}
return
}
_, err := compile(implantName, profile.Config, nil, save, con)
if err != nil {
return
}
} else {
con.PrintErrorf("No profile with name '%s'", name)
-2
View File
@@ -263,8 +263,6 @@ func populateProfileProperties(config *clientpb.ImplantConfig) map[string]string
properties["format"] = "Executable"
case clientpb.OutputFormat_SHARED_LIB:
properties["format"] = "Shared Library"
case clientpb.OutputFormat_GO_ARCHIVE:
properties["format"] = "Go Archive"
case clientpb.OutputFormat_SERVICE:
properties["format"] = "Service"
case clientpb.OutputFormat_SHELLCODE:
+2 -29
View File
@@ -56,9 +56,6 @@ 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,
@@ -170,29 +167,6 @@ _=/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).`
@@ -463,9 +437,8 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
mountHelp = `[[.Bold]]Command:[[.Normal]] mount
[[.Bold]]About:[[.Normal]] Displays information about mounted drives on the system, including mount point, space metrics, and filesystem.`
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode or PE]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process. On Windows targets, PE input can be
converted with Donut before execution and tuned with [[.Bold]]--shellcode-*[[.Normal]] flags.
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process.
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
+2 -2
View File
@@ -43,12 +43,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.NetworkHelpGroup,
}
flags.Bind("jobs", true, jobsCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("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.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(jobsCmd, func(comp *carapace.ActionMap) {
(*comp)["kill"] = JobsIDCompleter(con)
+1 -1
View File
@@ -24,7 +24,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("loot", true, lootCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("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")
-1
View File
@@ -66,7 +66,6 @@ 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")
+4 -29
View File
@@ -6,14 +6,12 @@ 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"
)
@@ -38,23 +36,7 @@ func McpConsoleCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
}
}
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)
client, err := newConsoleClient(transport, targetURL)
if err != nil {
con.PrintErrorf("%s\n", err)
return
@@ -86,19 +68,12 @@ func McpConsoleCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
runConsoleREPL(ctx, client, cmd.InOrStdin(), cmd.OutOrStdout())
}
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,
}
func newConsoleClient(transport clientmcp.Transport, targetURL string) (*mcpclient.Client, error) {
switch transport {
case clientmcp.TransportHTTP:
return mcpclient.NewStreamableHttpClient(targetURL, mcptransport.WithHTTPHeaders(headers))
return mcpclient.NewStreamableHttpClient(targetURL)
case clientmcp.TransportSSE:
return mcpclient.NewSSEMCPClient(targetURL, mcpclient.WithHeaders(headers))
return mcpclient.NewSSEMCPClient(targetURL)
default:
return nil, fmt.Errorf("unsupported transport %q", transport)
}
-6
View File
@@ -26,12 +26,6 @@ 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)
-7
View File
@@ -43,11 +43,4 @@ 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)
}
}
+1 -1
View File
@@ -81,7 +81,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
pivotsCmd.AddCommand(pivotDetailsCmd)
flags.Bind("", false, pivotDetailsCmd, func(f *pflag.FlagSet) {
f.Uint32P("id", "i", 0, "id of the pivot listener to get details for")
f.IntP("id", "i", 0, "id of the pivot listener to get details for")
})
flags.BindFlagCompletions(pivotDetailsCmd, func(comp *carapace.ActionMap) {
(*comp)["id"] = PivotIDCompleter(con)
-1
View File
@@ -39,7 +39,6 @@ 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()
+1 -11
View File
@@ -68,23 +68,13 @@ 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: keepAlivePeriod,
KeepAlivePeriod: 60 * time.Second,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
+1 -1
View File
@@ -74,7 +74,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GetSystemCmd(cmd, con, args)
},
GroupID: consts.PrivilegesHelpGroup,
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter, consts.SessionCmdsFilter),
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
}
flags.Bind("", false, getSystemCmd, func(f *pflag.FlagSet) {
f.StringP("process", "p", "spoolsv.exe", "SYSTEM process to inject into")
-8
View File
@@ -37,10 +37,6 @@ 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")
@@ -49,10 +45,6 @@ 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
+1 -1
View File
@@ -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.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int32P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles()
+2 -2
View File
@@ -55,7 +55,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
return
}
timeout, _ := cmd.Flags().GetInt64("timeout")
timeout, _ := cmd.Flags().GetInt32("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: int32(timeout),
Timeout: timeout,
})
ctrl <- true
<-ctrl
+4 -13
View File
@@ -24,7 +24,6 @@ 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"
@@ -95,24 +94,16 @@ 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 {
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",
con.PrintErrorf("Cannot switch protocol from %s to %s (protocol must match compiled implant)\n",
currentURI.Scheme, newURI.Scheme)
_ = forms.Confirm("Do you want to continue?", &confirm)
if !confirm {
return
}
return
}
// Warning: Switching to a different host typically fails because the implant and server must share the same crypto keys.
// for now only support the same Host/C2 Server (becuase the implant crypto keys problem)
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
}
}
}
}
+1 -1
View File
@@ -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.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
registryReadCmd := &cobra.Command{
-1
View File
@@ -39,7 +39,6 @@ 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()
-3
View File
@@ -53,13 +53,10 @@ 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)
-4
View File
@@ -22,7 +22,6 @@ 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"
@@ -33,7 +32,6 @@ 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"
@@ -100,8 +98,6 @@ 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,
+11 -11
View File
@@ -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 {
_ = transport.CloseGRPCConnection(conn)
_ = conn.Close()
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 {
_ = transport.CloseGRPCConnection(conn)
_ = conn.Close()
con.PrintErrorf("Switch failed: %s\n", err)
return
}
@@ -347,6 +347,7 @@ type switchTUIModel struct {
result switchResult
width int
height int
theme *huh.Theme
styles switchStyles
activeID string
counts map[string]int
@@ -366,7 +367,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)
prog := tea.NewProgram(m, tea.WithAltScreen())
finalModel, err := prog.Run()
if err != nil {
return switchResult{}, err
@@ -464,6 +465,7 @@ func newSwitchTUIModel(instances []*consoleInstance, activeID string, configKeys
tab: tabExisting,
existing: existingForm,
newConn: newForm,
theme: huhTheme,
styles: styles,
activeID: activeID,
counts: counts,
@@ -489,7 +491,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.KeyPressMsg:
case tea.KeyMsg:
switch msg.String() {
case "tab":
if m.tab == tabExisting {
@@ -546,7 +548,7 @@ func (m switchTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m switchTUIModel) View() tea.View {
func (m switchTUIModel) View() string {
header := m.renderHeader()
body := ""
@@ -560,9 +562,7 @@ func (m switchTUIModel) View() tea.View {
footer := m.renderFooter()
content := lipgloss.JoinVertical(lipgloss.Left, header, body, footer)
view := tea.NewView(m.styles.frame.Render(content))
view.AltScreen = true
return view
return m.styles.frame.Render(content)
}
func (m switchTUIModel) renderHeader() string {
+1 -1
View File
@@ -27,7 +27,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("sessions", true, sessionsCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("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")
+1 -5
View File
@@ -22,19 +22,16 @@ 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"
@@ -52,6 +49,7 @@ 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"
@@ -80,8 +78,6 @@ func SliverCommands(con *client.SliverClient) console.Commands {
// [ Core ]
bind(consts.SliverCoreHelpGroup,
ai.Commands,
docscmd.Commands,
reconfig.Commands,
// sessions.Commands,
sessions.SliverCommands,
+1 -1
View File
@@ -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.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("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)")
+1 -1
View File
@@ -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.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
(*comp)["proxy"] = completers.LocalProxyCompleter()
+1 -1
View File
@@ -60,7 +60,7 @@ const (
func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
VerboseVersionsCmd(cmd, con, args)
timeoutF, _ := cmd.Flags().GetInt64("timeout")
timeoutF, _ := cmd.Flags().GetInt("timeout")
timeout := time.Duration(timeoutF) * time.Second
insecure, _ := cmd.Flags().GetBool("insecure")
+1 -1
View File
@@ -41,7 +41,7 @@ func TestWebsiteRemoveConfirmationPrompt(t *testing.T) {
func TestWebsiteRemoveSuccessMessage(t *testing.T) {
got := websiteRemoveSuccessMessage("test", 2)
want := "Removed test and 2 content items"
want := "removed test and 2 content items"
if got != want {
t.Fatalf("unexpected message: got %q want %q", got, want)
}
+18 -86
View File
@@ -11,7 +11,6 @@ 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"
@@ -46,9 +45,6 @@ func (con *SliverClient) SetConnection(rpc rpcpb.SliverRPCClient, grpcConn *grpc
con.Rpc = rpc
con.grpcConn = grpcConn
con.connDetails = details
con.backgroundRPC = nil
con.backgroundConn = nil
con.backgroundDedicated = false
if con.Rpc == nil || con.grpcConn == nil {
return nil
@@ -61,49 +57,31 @@ func (con *SliverClient) SetConnection(rpc rpcpb.SliverRPCClient, grpcConn *grpc
con.BeaconTaskCallbacksMutex.Unlock()
con.ActiveTarget.Set(nil, nil)
if details != nil && details.Config != nil && details.Config.WG != nil {
con.backgroundDedicated = true
commandRPC, commandConn, err := transport.MTLSConnect(details.Config)
if err != nil {
log.Printf("Dedicated command WireGuard connection unavailable, async console streams disabled to preserve command reliability: %v", err)
} else {
con.backgroundRPC = con.Rpc
con.backgroundConn = con.grpcConn
con.Rpc = commandRPC
con.grpcConn = commandConn
}
}
ctx, cancel := context.WithCancel(context.Background())
con.connCancel = cancel
wg := &sync.WaitGroup{}
con.connWg = wg
backgroundRPC := con.backgroundRPCClientLocked()
if backgroundRPC != nil {
wg.Add(1)
go func(wg *sync.WaitGroup, rpc rpcpb.SliverRPCClient) {
defer wg.Done()
con.startEventLoop(ctx, rpc)
}(wg, backgroundRPC)
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
con.startEventLoop(ctx)
}(wg)
wg.Add(1)
go func(wg *sync.WaitGroup, rpc rpcpb.SliverRPCClient) {
defer wg.Done()
if err := core.TunnelLoop(ctx, rpc); err != nil && !errors.Is(err, context.Canceled) {
log.Printf("TunnelLoop error: %v", err)
}
}(wg, backgroundRPC)
}
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
if err := core.TunnelLoop(ctx, con.Rpc); err != nil && !errors.Is(err, context.Canceled) {
log.Printf("TunnelLoop error: %v", err)
}
}(wg)
if !con.backgroundDedicated || con.backgroundConn == nil {
wg.Add(1)
go func(wg *sync.WaitGroup, conn *grpc.ClientConn) {
defer wg.Done()
con.monitorConnectionLost(ctx, conn)
}(wg, con.grpcConn)
}
wg.Add(1)
go func(wg *sync.WaitGroup, conn *grpc.ClientConn) {
defer wg.Done()
con.monitorConnectionLost(ctx, conn)
}(wg, con.grpcConn)
con.refreshRemoteLogStreamsLocked()
@@ -145,62 +123,16 @@ func (con *SliverClient) detachConnectionLocked() {
}
if con.grpcConn != nil {
_ = transport.CloseGRPCConnection(con.grpcConn)
_ = con.grpcConn.Close()
con.grpcConn = nil
}
if con.backgroundConn != nil {
_ = transport.CloseGRPCConnection(con.backgroundConn)
con.backgroundConn = nil
}
con.Rpc = nil
con.backgroundRPC = nil
con.backgroundDedicated = false
con.connDetails = nil
// Tear down any singleton network tooling that was tied to the previous server.
core.ResetClientState()
}
func (con *SliverClient) backgroundRPCClientLocked() rpcpb.SliverRPCClient {
if con.backgroundRPC != nil {
return con.backgroundRPC
}
if con.backgroundDedicated {
return nil
}
return con.Rpc
}
func (con *SliverClient) refreshDedicatedCommandConnectionHook(args []string) ([]string, error) {
if err := con.refreshDedicatedCommandConnection(); err != nil {
return args, err
}
return args, nil
}
func (con *SliverClient) refreshDedicatedCommandConnection() error {
con.connMu.Lock()
defer con.connMu.Unlock()
if !con.backgroundDedicated || con.backgroundConn == nil || con.connDetails == nil || con.connDetails.Config == nil {
return nil
}
rpc, conn, err := transport.MTLSConnect(con.connDetails.Config)
if err != nil {
return fmt.Errorf("refresh command connection: %w", err)
}
oldConn := con.grpcConn
con.Rpc = rpc
con.grpcConn = conn
if oldConn != nil {
_ = transport.CloseGRPCConnection(oldConn)
}
return nil
}
func (con *SliverClient) monitorConnectionLost(ctx context.Context, conn *grpc.ClientConn) {
if conn == nil {
return
+14 -115
View File
@@ -29,7 +29,6 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/bishopfox/sliver/client/assets"
@@ -87,23 +86,14 @@ 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
backgroundRPC rpcpb.SliverRPCClient
backgroundConn *grpc.ClientConn
backgroundDedicated bool
connDetails *ConnectionDetails
connCancel context.CancelFunc
connWg *sync.WaitGroup
connectionHooksApplied bool
logCommandHookApplied bool
suppressedEventNotifs atomic.Int32
// These writers are always safe to use (never error); the underlying stream
// can be swapped on server switch to avoid breaking io.MultiWriter/io.Copy.
@@ -327,9 +317,6 @@ 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.refreshDedicatedCommandConnectionHook)
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.allowServerRootCommands)
if shell := con.App.Shell(); shell != nil && shell.Completer != nil {
baseCompleter := shell.Completer
@@ -340,12 +327,8 @@ func (con *SliverClient) applyConnectionHooksOnce() {
}
}
func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPCClient) {
if rpc == nil {
return
}
eventStream, err := rpc.Events(ctx, &commonpb.Empty{})
func (con *SliverClient) startEventLoop(ctx context.Context) {
eventStream, err := con.Rpc.Events(ctx, &commonpb.Empty{})
if err != nil {
fmt.Printf("%s%s\n", Warn, err)
return
@@ -362,20 +345,8 @@ func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPC
switch event.EventType {
case consts.CanaryEvent:
sessions := con.GetSessionsByName(event.Session.Name)
if con.eventNotificationsSuppressed() {
affected := make([]string, 0, len(sessions))
for _, session := range sessions {
affected = append(affected, strings.Split(session.ID, "-")[0])
}
message := fmt.Sprintf("%s %s has been burned (DNS Canary)", StyleBold.Render("WARNING:"), event.Session.Name)
if len(affected) > 0 {
message += " Affected sessions: " + strings.Join(affected, ", ")
}
con.emitToastEvent("error", message)
break
}
con.PrintEventErrorf("%s %s has been burned (DNS Canary)", StyleBold.Render("WARNING:"), event.Session.Name)
sessions := con.GetSessionsByName(event.Session.Name)
for _, session := range sessions {
shortID := strings.Split(session.ID, "-")[0]
con.PrintErrorf("\t🔥 Session %s is affected", shortID)
@@ -383,20 +354,8 @@ func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPC
case consts.WatchtowerEvent:
msg := string(event.Data)
sessions := con.GetSessionsByName(event.Session.Name)
if con.eventNotificationsSuppressed() {
affected := make([]string, 0, len(sessions))
for _, session := range sessions {
affected = append(affected, strings.Split(session.ID, "-")[0])
}
message := fmt.Sprintf("%s %s has been burned (seen on %s)", StyleBold.Render("WARNING:"), event.Session.Name, msg)
if len(affected) > 0 {
message += " Affected sessions: " + strings.Join(affected, ", ")
}
con.emitToastEvent("error", message)
break
}
con.PrintEventErrorf("%s %s has been burned (seen on %s)", StyleBold.Render("WARNING:"), event.Session.Name, msg)
sessions := con.GetSessionsByName(event.Session.Name)
for _, session := range sessions {
shortID := strings.Split(session.ID, "-")[0]
con.PrintErrorf("\t🔥 Session %s is affected", shortID)
@@ -404,42 +363,42 @@ func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPC
case consts.JoinedEvent:
if con.Settings.UserConnect {
con.emitConsoleNotification("info", false, "%s has joined the game", event.Client.Operator.Name)
con.PrintInfof("%s has joined the game", event.Client.Operator.Name)
}
case consts.LeftEvent:
if con.Settings.UserConnect {
con.emitConsoleNotification("info", false, "%s left the game", event.Client.Operator.Name)
con.PrintInfof("%s left the game", event.Client.Operator.Name)
}
case consts.JobStoppedEvent:
job := event.Job
con.emitConsoleNotification("error", false, "Job #%d stopped (%s/%s)", job.ID, job.Protocol, job.Name)
con.PrintErrorf("Job #%d stopped (%s/%s)", job.ID, job.Protocol, job.Name)
case consts.SessionOpenedEvent:
session := event.Session
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(session.ID, "-")[0]
con.emitConsoleNotification("info", true, "Session %s %s - %s (%s) - %s/%s - %v",
con.PrintEventInfof("Session %s %s - %s (%s) - %s/%s - %v",
shortID, session.Name, session.RemoteAddress, session.Hostname, session.OS, session.Arch, currentTime)
case consts.SessionUpdateEvent:
session := event.Session
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(session.ID, "-")[0]
con.emitConsoleNotification("info", false, "Session %s has been updated - %v", shortID, currentTime)
con.PrintInfof("Session %s has been updated - %v", shortID, currentTime)
case consts.SessionClosedEvent:
session := event.Session
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(session.ID, "-")[0]
con.emitConsoleNotification("error", true, "Lost session %s %s - %s (%s) - %s/%s - %v",
con.PrintEventErrorf("Lost session %s %s - %s (%s) - %s/%s - %v",
shortID, session.Name, session.RemoteAddress, session.Hostname, session.OS, session.Arch, currentTime)
activeSession := con.ActiveTarget.GetSession()
core.GetTunnels().CloseForSession(session.ID)
core.CloseCursedProcesses(session.ID)
if activeSession != nil && activeSession.ID == session.ID {
con.ActiveTarget.Set(nil, nil)
con.emitConsoleNotification("error", false, "Active session disconnected")
con.PrintErrorf("Active session disconnected")
}
case consts.BeaconRegisteredEvent:
@@ -447,7 +406,7 @@ func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPC
proto.Unmarshal(event.Data, beacon)
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(beacon.ID, "-")[0]
con.emitConsoleNotification("info", true, "Beacon %s %s - %s (%s) - %s/%s - %v",
con.PrintEventInfof("Beacon %s %s - %s (%s) - %s/%s - %v",
shortID, beacon.Name, beacon.RemoteAddress, beacon.Hostname, beacon.OS, beacon.Arch, currentTime)
case consts.BeaconTaskResultEvent:
@@ -467,66 +426,6 @@ func (con *SliverClient) CreateEventListener() (string, <-chan *clientpb.Event)
return listenerID.String(), listener
}
// SuppressEventNotifications redirects console event messages to local listeners instead of stdout.
func (con *SliverClient) SuppressEventNotifications() func() {
if con == nil {
return func() {}
}
con.suppressedEventNotifs.Add(1)
return func() {
if con == nil {
return
}
if con.suppressedEventNotifs.Add(-1) < 0 {
con.suppressedEventNotifs.Store(0)
}
}
}
func (con *SliverClient) eventNotificationsSuppressed() bool {
return con != nil && con.suppressedEventNotifs.Load() > 0
}
func (con *SliverClient) emitToastEvent(level string, message string) {
if con == nil || con.EventListeners == nil || strings.TrimSpace(message) == "" {
return
}
con.triggerEventListeners(&clientpb.Event{
EventType: consts.ClientToastEvent,
Data: []byte(message),
Err: strings.TrimSpace(level),
})
}
func (con *SliverClient) emitConsoleNotification(level string, emphasized bool, format string, args ...any) {
message := fmt.Sprintf(format, args...)
if con.eventNotificationsSuppressed() {
con.emitToastEvent(level, message)
return
}
switch strings.ToLower(strings.TrimSpace(level)) {
case "error", "warn", "warning":
if emphasized {
con.PrintEventErrorf("%s", message)
} else {
con.PrintErrorf("%s", message)
}
case "success":
if emphasized {
con.PrintEventSuccessf("%s", message)
} else {
con.PrintSuccessf("%s", message)
}
default:
if emphasized {
con.PrintEventInfof("%s", message)
} else {
con.PrintInfof("%s", message)
}
}
}
// RemoveEventListener - removes an event listener given its id.
func (con *SliverClient) RemoveEventListener(listenerID string) {
value, ok := con.EventListeners.LoadAndDelete(listenerID)
@@ -571,10 +470,10 @@ func (con *SliverClient) triggerReactions(event *clientpb.Event) {
for _, reaction := range reactions {
for _, line := range reaction.Commands {
con.emitConsoleNotification("info", false, "%s '%s'", StyleBold.Render("Execute reaction:"), line)
con.PrintInfof("%s '%s'", StyleBold.Render("Execute reaction:"), line)
err := con.App.ActiveMenu().RunCommandLine(context.Background(), line)
if err != nil {
con.emitConsoleNotification("error", false, "Reaction command error: %s", err)
con.PrintErrorf("Reaction command error: %s\n", err)
}
}
}
+8 -171
View File
@@ -19,10 +19,7 @@ package console
*/
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"log"
@@ -41,11 +38,6 @@ 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
@@ -107,8 +99,7 @@ func (con *SliverClient) refreshRemoteLogStreamsLocked() {
if con.jsonRemoteWriter == nil && con.asciicastRemoteWriter == nil {
return
}
rpc := con.backgroundRPCClientLocked()
if rpc == nil {
if con.Rpc == nil {
con.setRemoteLogStreamsLocked(nil, nil)
return
}
@@ -117,7 +108,7 @@ func (con *SliverClient) refreshRemoteLogStreamsLocked() {
var asciicastStream *ConsoleClientLogger
if con.jsonRemoteWriter != nil {
s, err := con.clientLogStream(rpc, "json")
s, err := con.ClientLogStream("json")
if err != nil {
log.Printf("Could not get client json log stream: %s", err)
} else {
@@ -125,7 +116,7 @@ func (con *SliverClient) refreshRemoteLogStreamsLocked() {
}
}
if con.asciicastRemoteWriter != nil {
s, err := con.clientLogStream(rpc, "asciicast")
s, err := con.ClientLogStream("asciicast")
if err != nil {
log.Printf("Could not get client asciicast log stream: %s", err)
} else {
@@ -167,17 +158,7 @@ func (con *SliverClient) setRemoteLogStreamsLocked(jsonStream, asciicastStream *
// ClientLogStream requires a log stream name, used to save the logs
// going through this stream in a specific log subdirectory/file.
func (con *SliverClient) ClientLogStream(name string) (*ConsoleClientLogger, error) {
con.connMu.Lock()
rpc := con.backgroundRPCClientLocked()
con.connMu.Unlock()
if rpc == nil {
return nil, fmt.Errorf("no RPC connection available for client log stream %q", name)
}
return con.clientLogStream(rpc, name)
}
func (con *SliverClient) clientLogStream(rpc rpcpb.SliverRPCClient, name string) (*ConsoleClientLogger, error) {
stream, err := rpc.ClientLog(context.Background())
stream, err := con.Rpc.ClientLog(context.Background())
if err != nil {
return nil, err
}
@@ -252,158 +233,14 @@ 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 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)
}
go func() {
_, _ = io.Copy(mw, r)
close(done)
}()
}
func getConsoleLogFile() *os.File {
-137
View File
@@ -1,137 +0,0 @@
package console
import (
"bytes"
"os"
"sync"
"testing"
"time"
consts "github.com/bishopfox/sliver/client/constants"
)
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())
}
}
func TestSuppressEventNotificationsRedirectsMessagesToListeners(t *testing.T) {
con := &SliverClient{
EventListeners: &sync.Map{},
printf: func(string, ...any) (int, error) { return 0, nil },
}
listenerID, listener := con.CreateEventListener()
defer con.RemoveEventListener(listenerID)
restore := con.SuppressEventNotifications()
defer restore()
con.emitConsoleNotification("info", true, "Session %s connected", "alpha")
select {
case event := <-listener:
if event.GetEventType() != consts.ClientToastEvent {
t.Fatalf("expected toast event, got %q", event.GetEventType())
}
if got := string(event.GetData()); got != "Session alpha connected" {
t.Fatalf("unexpected toast payload %q", got)
}
if got := event.GetErr(); got != "info" {
t.Fatalf("unexpected toast level %q", got)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for redirected event notification")
}
}
+1 -12
View File
@@ -113,15 +113,7 @@ const (
CrackStatusEvent = "crack-status"
// WireGuardNewPeer - New Wireguard peer added.
WireGuardNewPeer = "wireguard-newpeer"
MultiplayerWireGuardNewPeer = "multiplayer-wireguard-newpeer"
MultiplayerWireGuardRemoved = "multiplayer-wireguard-removed"
// AIConversationEvent - Fires whenever an AI conversation or message changes.
AIConversationEvent = "ai-conversation"
// ClientToastEvent - Local-only event used to surface console notifications inside TUIs.
ClientToastEvent = "client-toast"
WireGuardNewPeer = "wireguard-newpeer"
)
// Commands.
@@ -228,9 +220,6 @@ const (
MkdirStr = "mkdir"
CdStr = "cd"
PwdStr = "pwd"
AIStr = "ai"
AIConfigStr = "ai-config"
DocsStr = "docs"
CatStr = "cat"
EditStr = "edit"
HexEditStr = "hex-edit"
-1
View File
@@ -216,7 +216,6 @@ 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
+2 -2
View File
@@ -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 := runForm(form); err != nil {
if err := form.Run(); err != nil {
return nil, err
}
+2 -2
View File
@@ -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 := runForm(form); err != nil {
if err := form.Run(); err != nil {
return nil, err
}
+2 -2
View File
@@ -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 := runForm(form); err != nil {
if err := form.Run(); err != nil {
return nil, err
}
+2 -2
View File
@@ -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 := runForm(form); err != nil {
if err := form.Run(); err != nil {
return nil, err
}
+2 -3
View File
@@ -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.
@@ -79,7 +79,6 @@ func GenerateProfilesNewForm() (*GenerateProfilesNewFormResult, error) {
options := []huh.Option[string]{
huh.NewOption("Executable", "exe"),
huh.NewOption("Shared library", "shared"),
huh.NewOption("Go archive", "archive"),
}
if result.OS == "windows" {
options = append(options,
@@ -137,7 +136,7 @@ func GenerateProfilesNewForm() (*GenerateProfilesNewFormResult, error) {
)
form = form.WithTheme(theme.HuhTheme())
if err := runForm(form); err != nil {
if err := form.Run(); err != nil {
return nil, err
}
+2 -3
View File
@@ -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.
@@ -84,7 +84,6 @@ func GenerateProfilesNewBeaconForm() (*GenerateProfilesNewBeaconFormResult, erro
options := []huh.Option[string]{
huh.NewOption("Executable", "exe"),
huh.NewOption("Shared library", "shared"),
huh.NewOption("Go archive", "archive"),
}
if result.OS == "windows" {
options = append(options,
@@ -169,7 +168,7 @@ func GenerateProfilesNewBeaconForm() (*GenerateProfilesNewBeaconFormResult, erro
)
form = form.WithTheme(theme.HuhTheme())
if err := runForm(form); err != nil {
if err := form.Run(); err != nil {
return nil, err
}
+1 -3
View File
@@ -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 {
@@ -22,7 +22,6 @@ type targetOptionBindings struct {
var formatOptionOrder = []formatOption{
{format: clientpb.OutputFormat_EXECUTABLE, label: "Executable", value: "exe"},
{format: clientpb.OutputFormat_SHARED_LIB, label: "Shared library", value: "shared"},
{format: clientpb.OutputFormat_GO_ARCHIVE, label: "Go archive", value: "archive"},
{format: clientpb.OutputFormat_SERVICE, label: "Service", value: "service"},
{format: clientpb.OutputFormat_SHELLCODE, label: "Shellcode", value: "shellcode"},
}
@@ -60,7 +59,6 @@ func commonFormatOptions(goos string) []huh.Option[string] {
options := []huh.Option[string]{
huh.NewOption("Executable", "exe"),
huh.NewOption("Shared library", "shared"),
huh.NewOption("Go archive", "archive"),
}
if goos == "windows" {
options = append(options,
+6 -6
View File
@@ -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 runForm(form)
return form.Run()
}
// Input prompts for a single-line string.
@@ -52,7 +52,7 @@ func Input(title string, value *string) error {
),
).WithTheme(theme.HuhTheme())
return runForm(form)
return form.Run()
}
// Text prompts for multi-line input.
@@ -70,7 +70,7 @@ func Text(title string, value *string) error {
),
).WithTheme(theme.HuhTheme())
return runForm(form)
return form.Run()
}
// 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 runForm(form)
return form.Run()
}
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 := runForm(form)
err := form.Run()
// On error restore the originalValue and return err
if err != nil {
-76
View File
@@ -1,76 +0,0 @@
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
}
-49
View File
@@ -1,49 +0,0 @@
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)
}
})
}
}
+2 -2
View File
@@ -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 := runForm(form); err != nil {
if err := form.Run(); err != nil {
return nil, err
}
-191
View File
@@ -1,191 +0,0 @@
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)
}
-97
View File
@@ -1,97 +0,0 @@
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)
}
})
}
}
+11 -24
View File
@@ -20,13 +20,10 @@ var (
// Status describes the running state of the MCP server.
type Status struct {
Running bool
StartedAt time.Time
Config Config
LastError string
AuthHeader string
AuthToken string
AuthConfigPath string
Running bool
StartedAt time.Time
Config Config
LastError string
}
type serverTransport interface {
@@ -45,7 +42,6 @@ type mcpManager struct {
done chan struct{}
logger *log.Logger
logFile *os.File
authInfo AuthInfo
}
func newManager() *mcpManager {
@@ -97,13 +93,10 @@ 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,
AuthHeader: m.authInfo.Header,
AuthToken: m.authInfo.Token,
AuthConfigPath: m.authInfo.ConfigPath,
Running: m.running,
StartedAt: m.startedAt,
Config: m.cfg,
LastError: m.lastErr,
}
}
@@ -112,10 +105,6 @@ 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 {
@@ -131,9 +120,9 @@ func (m *mcpManager) start(cfg Config, rpc rpcpb.SliverRPCClient) error {
var transport serverTransport
switch cfg.Transport {
case TransportHTTP:
transport = newAuthenticatedTransport(cfg, mcpserver.NewStreamableHTTPServer(mcpServer.server), authInfo.Token)
transport = mcpserver.NewStreamableHTTPServer(mcpServer.server)
case TransportSSE:
transport = newAuthenticatedTransport(cfg, mcpserver.NewSSEServer(mcpServer.server), authInfo.Token)
transport = mcpserver.NewSSEServer(mcpServer.server)
default:
m.mu.Unlock()
logFile.Close()
@@ -149,12 +138,11 @@ 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 auth_header=%s auth_config=%s", cfg.Transport, cfg.ListenAddress, authInfo.Header, authInfo.ConfigPath)
logger.Printf("starting mcp server transport=%s listen=%s", cfg.Transport, cfg.ListenAddress)
go m.run(addr, transport, done)
return nil
}
@@ -173,7 +161,6 @@ 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
-443
View File
@@ -1,443 +0,0 @@
// 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
}
-20
View File
@@ -1,20 +0,0 @@
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
}
+52 -56
View File
@@ -1,88 +1,84 @@
package theme
import (
"image/color"
"charm.land/huh/v2"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
// 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 {
return huh.ThemeFunc(func(isDark bool) *huh.Styles {
t := huh.ThemeBase(isDark)
func HuhTheme() *huh.Theme {
t := huh.ThemeBase()
current := Current()
border := paletteMod(current.Default, 300)
text := paletteMod(current.Default, 900)
dim := paletteMod(current.Default, 500)
faint := paletteMod(current.Default, 400)
current := Current()
border := paletteModV1(current.Default, 300)
text := paletteModV1(current.Default, 900)
dim := paletteModV1(current.Default, 500)
faint := paletteModV1(current.Default, 400)
primary := paletteColor(current.Primary)
accent := paletteColor(current.Secondary)
primary := paletteColorV1(current.Primary)
accent := paletteColorV1(current.Secondary)
success := paletteColor(current.Success)
danger := paletteColor(current.Danger)
success := paletteColorV1(current.Success)
danger := paletteColorV1(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(paletteMod(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(paletteModV1(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 paletteColor(p Palette) color.Color {
func paletteColorV1(p Palette) lipgloss.TerminalColor {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
return lipgloss.Color("#ffffff")
}
func paletteMod(p Palette, mod int) color.Color {
func paletteModV1(p Palette, mod int) lipgloss.TerminalColor {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
}
}
return paletteColor(p)
return paletteColorV1(p)
}
-72
View File
@@ -1,72 +0,0 @@
package transport
import (
"errors"
"sync"
"google.golang.org/grpc"
)
// MultiplayerConnectMode controls how the client reaches multiplayer.
type MultiplayerConnectMode int
const (
MultiplayerConnectDirect MultiplayerConnectMode = iota
MultiplayerConnectEnableWG
)
type connectionCloser interface {
Close() error
}
var (
multiplayerConnectModeMu sync.RWMutex
multiplayerConnectMode = MultiplayerConnectDirect
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
errs = append(errs, conn.Close())
if closer := unregisterConnCloser(conn); closer != nil {
errs = append(errs, closer.Close())
}
return errors.Join(errs...)
}
+3 -78
View File
@@ -22,7 +22,6 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log"
"os"
@@ -51,22 +50,6 @@ type TokenAuth struct {
token string
}
type multiplayerDialStrategy int
const (
multiplayerDialDirect multiplayerDialStrategy = iota
multiplayerDialWireGuard
)
const (
connectStatusGRPCMTLS = "grpc/mtls"
connectStatusWireGuard = "wireguard"
connectStatusGRPCMTLSOverWireGuard = "grpc/mtls over wireguard"
)
// ConnectStatusFn receives human-readable connection phase updates.
type ConnectStatusFn func(string)
// Return value is mapped to request headers.
func (t TokenAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
return map[string]string{
@@ -80,60 +63,9 @@ func (TokenAuth) RequireTransportSecurity() bool {
// MTLSConnect - Connect to the sliver server
func MTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
return MTLSConnectWithStatus(config, nil)
}
// MTLSConnectWithStatus connects to the sliver server and optionally reports
// transport phase updates, such as WireGuard setup and gRPC dialing.
func MTLSConnectWithStatus(config *assets.ClientConfig, statusFn ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
strategy, err := selectMultiplayerDialStrategy(config)
if err != nil {
return nil, nil, err
}
switch strategy {
case multiplayerDialWireGuard:
return wireGuardMTLSConnect(config, statusFn)
default:
return directMTLSConnect(config, statusFn)
}
}
func selectMultiplayerDialStrategy(config *assets.ClientConfig) (multiplayerDialStrategy, error) {
if config == nil {
return multiplayerDialDirect, errors.New("client config is required")
}
switch getMultiplayerConnectMode() {
case MultiplayerConnectEnableWG:
if err := validateWireGuardConfig(config); err != nil {
return multiplayerDialDirect, err
}
return multiplayerDialWireGuard, nil
default:
return multiplayerDialDirect, nil
}
}
func directMTLSConnect(config *assets.ClientConfig, statusFn ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
notifyConnectStatus(statusFn, connectStatusGRPCMTLS)
options, err := newMTLSDialOptions(config)
if err != nil {
return nil, nil, err
}
return dialRPCClient(fmt.Sprintf("%s:%d", config.LHost, config.LPort), options, nil)
}
func notifyConnectStatus(statusFn ConnectStatusFn, status string) {
if statusFn != nil {
statusFn(status)
}
}
func newMTLSDialOptions(config *assets.ClientConfig) ([]grpc.DialOption, error) {
tlsConfig, err := getTLSConfig(config.CACertificate, config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
return nil, nil, err
}
transportCreds := credentials.NewTLS(tlsConfig)
callCreds := credentials.PerRPCCredentials(TokenAuth{token: config.Token})
@@ -146,18 +78,11 @@ func newMTLSDialOptions(config *assets.ClientConfig) ([]grpc.DialOption, error)
if kp, ok := getKeepaliveParams(); ok {
options = append(options, grpc.WithKeepaliveParams(kp))
}
return options, nil
}
func dialRPCClient(target string, options []grpc.DialOption, closer connectionCloser) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
connection, err := grpc.NewClient(target, options...)
connection, err := grpc.NewClient(fmt.Sprintf("%s:%d", config.LHost, config.LPort), 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.
@@ -172,7 +97,7 @@ func dialRPCClient(target string, options []grpc.DialOption, closer connectionCl
break
}
if !connection.WaitForStateChange(ctx, state) {
_ = CloseGRPCConnection(connection)
_ = connection.Close()
return nil, nil, ctx.Err()
}
}
Loaded 100 of 2277 files, more files were not shown because too many files have changed in this diff. Show more