├── .github └── workflows │ └── bump-version.yaml ├── .gitignore ├── LICENSE ├── README.md ├── cmdexec └── exec.go ├── errors.go ├── examples ├── simple_darwin │ └── main.go ├── simple_linux │ └── main.go └── simple_windows │ └── main.go ├── go.mod ├── go.sum ├── gozero.go ├── gozero_test.go ├── options.go ├── sandbox ├── error.go ├── sandbox.go ├── sandbox_darwin.go ├── sandbox_linux.go ├── sandbox_other.go └── sandbox_window.go ├── source.go ├── source_test.go └── types ├── result.go └── vars.go /.github/workflows/bump-version.yaml: -------------------------------------------------------------------------------- 1 | name: ⏫ Bump Version 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 0' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | bump: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - uses: projectdiscovery/actions/setup/git@v1 16 | - uses: projectdiscovery/actions/svu-next@v1 17 | with: 18 | v0: true 19 | release-create: true 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ProjectDiscovery 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gozero 2 | 3 | gozero: the wannabe zero dependency [language-here] runtime for Go developers 4 | 5 | ## Isolation 6 | 7 | ### Windows 8 | 9 | Native isolation on windows is supported only with the PRO version and is implemented via Windows Sandbox (which needs to be [activated](https://www.makeuseof.com/enable-set-up-windows-sandbox-windows-11/)). 10 | 11 | ### Darwin 12 | 13 | OSX implements native isolation via the command `sandbox-exec`. The command line interface is marked as deprecated, but the system functionality is actively supported, and profiles are still used in well-known software like chrome, firefox. 14 | 15 | ### Linux 16 | 17 | On Linux, the functionality is implemented with the default command `systemd-run`, which should be available on most systems and allow a vast fine-grained sandbox configuration via SecComp and EBPF 18 | 19 | 20 | ## Note: 21 | 22 | Sandbox is not enabled by default and needs to be used manually through sdk -------------------------------------------------------------------------------- /cmdexec/exec.go: -------------------------------------------------------------------------------- 1 | // cmdexec package is wrapper around os/exec and handles common use cases while executing commands. 2 | package cmdexec 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "io" 8 | "os/exec" 9 | 10 | "github.com/projectdiscovery/gozero/types" 11 | errorutil "github.com/projectdiscovery/utils/errors" 12 | ) 13 | 14 | // Command is a command to execute. 15 | type Command struct { 16 | Binary string // Full path to the binary to execute 17 | Args []string 18 | Env []string 19 | stdin io.Reader 20 | debugMode bool 21 | } 22 | 23 | // NewCommand creates a new command with the provided binary and arguments. 24 | func NewCommand(binary string, args ...string) (*Command, error) { 25 | execpath, err := exec.LookPath(binary) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &Command{Binary: execpath, Args: args}, nil 30 | } 31 | 32 | // SetEnv sets the environment variables for the command. 33 | func (c *Command) SetEnv(env []string) { 34 | c.Env = env 35 | } 36 | 37 | // AddVars adds variables to the command. 38 | func (c *Command) AddVars(vars ...types.Variable) { 39 | for _, v := range vars { 40 | c.Env = append(c.Env, v.String()) 41 | } 42 | } 43 | 44 | // SetStdin sets the stdin for the command. 45 | func (c *Command) SetStdin(stdin io.Reader) { 46 | c.stdin = stdin 47 | } 48 | 49 | // EnableDebugMode enables the debug mode for the command. 50 | func (c *Command) EnableDebugMode() { 51 | c.debugMode = true 52 | } 53 | 54 | // Execute executes the command and returns the output. 55 | func (c *Command) Execute(ctx context.Context) (*types.Result, error) { 56 | cmd := exec.CommandContext(ctx, c.Binary, c.Args...) 57 | if len(c.Env) > 0 { 58 | // by default we allow existing environment variables to be inherited 59 | cmd.Env = append(cmd.Environ(), c.Env...) 60 | } 61 | res := &types.Result{Command: cmd.String()} 62 | if c.debugMode { 63 | res.DebugData = &bytes.Buffer{} 64 | cmd.Stdout = io.MultiWriter(&res.Stdout, res.DebugData) 65 | cmd.Stderr = io.MultiWriter(&res.Stderr, res.DebugData) 66 | } else { 67 | cmd.Stdout = &res.Stdout 68 | cmd.Stderr = &res.Stderr 69 | } 70 | if c.stdin != nil { 71 | cmd.Stdin = c.stdin 72 | } 73 | 74 | if err := cmd.Start(); err != nil { 75 | // this error indicates that command did not start at all (e.g. binary not found) 76 | // or something similar 77 | return res, errorutil.NewWithErr(err).Msgf("failed to start command got: %v", res.Stderr.String()) 78 | } 79 | 80 | if err := cmd.Wait(); err != nil { 81 | if execErr, ok := err.(*exec.ExitError); ok { 82 | res.SetExitError(execErr) 83 | } 84 | // this error indicates that command started but exited with non-zero exit code 85 | return res, errorutil.NewWithErr(err).Msgf("failed to exec command got: %v", res.Stderr.String()) 86 | } 87 | return res, nil 88 | } 89 | 90 | // Extra Notes: 91 | // go before 1.21 did not follow symlinks when executing binaries and python installed from ms store creates a symlink 92 | // this is fixed https://github.com/golang/go/issues/42919 but just in case a workaround is to execute using low level api i.e os.startprocess 93 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package gozero 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNoValidEngine is returned when no valid engine is found 7 | ErrNoValidEngine = errors.New("no valid engine found") 8 | 9 | // ErrNoEngines is returned when no engines are provided 10 | ErrNoEngines = errors.New("no engines provided") 11 | ) 12 | -------------------------------------------------------------------------------- /examples/simple_darwin/main.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "time" 10 | 11 | "github.com/projectdiscovery/gozero/sandbox" 12 | ) 13 | 14 | func main() { 15 | command := "hostname" 16 | rules := []sandbox.Rule{ 17 | {Action: sandbox.Deny, Scope: sandbox.FileWrite}, 18 | {Action: sandbox.Allow, Scope: sandbox.FileWrite, Args: []sandbox.Arg{ 19 | {Type: sandbox.SubPath, Params: []any{"/tmp"}}, 20 | }}, 21 | } 22 | cfg := sandbox.Configuration{ 23 | Rules: rules, 24 | } 25 | 26 | instance, err := sandbox.New(context.Background(), &cfg) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | if _, err := instance.Run(context.Background(), command); err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | time.Sleep(60 * time.Second) 36 | instance.Clear() 37 | } 38 | -------------------------------------------------------------------------------- /examples/simple_linux/main.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "time" 10 | 11 | "github.com/projectdiscovery/gozero/sandbox" 12 | ) 13 | 14 | func main() { 15 | command := "hostname" 16 | rules := []sandbox.Rule{ 17 | {Filter: sandbox.DynamicUser, Arg: sandbox.Arg{Type: sandbox.Bool, Params: "yes"}}, 18 | {Filter: sandbox.ReadOnlyDirectories, Arg: sandbox.Arg{Type: sandbox.Folders, Params: []string{"/etc", "/home"}}}, 19 | } 20 | cfg := sandbox.Configuration{ 21 | Rules: rules, 22 | } 23 | 24 | instance, err := sandbox.New(context.Background(), &cfg) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | if _, err := instance.Run(context.Background(), command); err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | time.Sleep(60 * time.Second) 34 | } 35 | -------------------------------------------------------------------------------- /examples/simple_windows/main.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "time" 10 | 11 | "github.com/projectdiscovery/gozero/sandbox" 12 | ) 13 | 14 | func main() { 15 | commands := []string{ 16 | "ipconfig", 17 | } 18 | cfg := sandbox.Configuration{ 19 | Networking: sandbox.Enable, 20 | VirtualGPU: sandbox.Default, 21 | MemoryInMB: 500, 22 | } 23 | cfg.LogonCommands.Command = commands 24 | 25 | instance, err := sandbox.New(context.Background(), &cfg) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | if err := instance.Start(); err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | time.Sleep(60 * time.Second) 35 | instance.Clear() 36 | } 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/projectdiscovery/gozero 2 | 3 | go 1.21.0 4 | toolchain go1.24.1 5 | 6 | require ( 7 | github.com/projectdiscovery/utils v0.0.55 8 | github.com/stretchr/testify v1.8.4 9 | ) 10 | 11 | require ( 12 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect 13 | github.com/aymerick/douceur v0.2.0 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/gorilla/css v1.0.0 // indirect 16 | github.com/microcosm-cc/bluemonday v1.0.25 // indirect 17 | github.com/pkg/errors v0.9.1 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/projectdiscovery/blackrock v0.0.1 // indirect 20 | github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect 21 | golang.org/x/net v0.38.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= 2 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 3 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 4 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= 8 | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= 9 | github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= 10 | github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= 11 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 12 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= 16 | github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= 17 | github.com/projectdiscovery/utils v0.0.55 h1:QcJhedFVr13ZIJwr81fdXVxylhrub6oQCTFqweDjxe8= 18 | github.com/projectdiscovery/utils v0.0.55/go.mod h1:WhzbWSyGkTDn4Jvw+7jM2yP675/RARegNjoA6S7zYcc= 19 | github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= 20 | github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= 21 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 22 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 23 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 24 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /gozero.go: -------------------------------------------------------------------------------- 1 | package gozero 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | 7 | "github.com/projectdiscovery/gozero/cmdexec" 8 | "github.com/projectdiscovery/gozero/types" 9 | ) 10 | 11 | // Gozero is executor for gozero 12 | type Gozero struct { 13 | Options *Options 14 | } 15 | 16 | // New creates a new gozero executor 17 | func New(options *Options) (*Gozero, error) { 18 | if len(options.Engines) == 0 { 19 | return nil, ErrNoEngines 20 | } 21 | // attempt to locate the interpreter by executing it 22 | for _, engine := range options.Engines { 23 | // use lookpath to check if engine is available 24 | // this ignores path confusion issues where binary with same name exists in current path 25 | fpath, err := exec.LookPath(engine) 26 | if err != nil { 27 | continue 28 | } else { 29 | options.engine = fpath 30 | break 31 | } 32 | } 33 | if options.engine == "" { 34 | return nil, ErrNoValidEngine 35 | } 36 | return &Gozero{Options: options}, nil 37 | } 38 | 39 | // Eval evaluates the source code and returns the output 40 | // input = stdin , src = source code , args = arguments 41 | func (g *Gozero) Eval(ctx context.Context, src, input *Source, args ...string) (*types.Result, error) { 42 | if g.Options.EarlyCloseFileDescriptor { 43 | src.File.Close() 44 | } 45 | allargs := []string{} 46 | allargs = append(allargs, g.Options.Args...) 47 | allargs = append(allargs, src.Filename) 48 | allargs = append(allargs, args...) 49 | gcmd, err := cmdexec.NewCommand(g.Options.engine, allargs...) 50 | if err != nil { 51 | // returns error if binary(engine) does not exist 52 | return nil, err 53 | } 54 | if g.Options.DebugMode { 55 | gcmd.EnableDebugMode() 56 | } 57 | gcmd.SetStdin(input.File) // stdin 58 | // add both input and src variables if any 59 | gcmd.AddVars(src.Variables...) // variables as environment variables 60 | gcmd.AddVars(input.Variables...) 61 | return gcmd.Execute(ctx) 62 | } 63 | -------------------------------------------------------------------------------- /gozero_test.go: -------------------------------------------------------------------------------- 1 | package gozero 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | osutils "github.com/projectdiscovery/utils/os" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestEval(t *testing.T) { 13 | opts := &Options{} 14 | if osutils.IsWindows() { 15 | opts.Engines = []string{"python3.exe"} 16 | } else { 17 | opts.Engines = []string{"python3"} 18 | } 19 | pyzero, err := New(opts) 20 | require.Nil(t, err) 21 | src, err := NewSourceWithString(`print(1)`, "", "") 22 | require.Nil(t, err) 23 | // empty input 24 | input, err := NewSource() 25 | require.Nil(t, err) 26 | out, err := pyzero.Eval(context.Background(), src, input) 27 | require.Nil(t, err) 28 | output := out.Stdout.String() 29 | require.Equal(t, "1", strings.TrimSpace(string(output))) 30 | err = src.Cleanup() 31 | require.Nil(t, err) 32 | err = input.Cleanup() 33 | require.Nil(t, err) 34 | } 35 | 36 | func TestErr(t *testing.T) { 37 | opts := &Options{} 38 | if osutils.IsWindows() { 39 | opts.Engines = []string{"nonexistent.exe"} 40 | } else { 41 | opts.Engines = []string{"nonexistent"} 42 | } 43 | gozero, err := New(opts) 44 | require.NotNil(t, err) 45 | require.Nil(t, gozero) 46 | require.ErrorIs(t, err, ErrNoValidEngine) 47 | 48 | opts.Engines = []string{} 49 | gozero, err = New(opts) 50 | require.NotNil(t, err) 51 | require.Nil(t, gozero) 52 | require.ErrorIs(t, err, ErrNoEngines) 53 | 54 | opts.Engines = []string{"python3"} 55 | gozero, err = New(opts) 56 | require.Nil(t, err) 57 | require.NotNil(t, gozero) 58 | } 59 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package gozero 2 | 3 | type Options struct { 4 | Engines []string 5 | Args []string 6 | engine string 7 | PreferStartProcess bool 8 | Sandbox bool 9 | EarlyCloseFileDescriptor bool 10 | // When Debug Mode is set to true, Output result will contain 11 | // more debug information 12 | DebugMode bool 13 | } 14 | -------------------------------------------------------------------------------- /sandbox/error.go: -------------------------------------------------------------------------------- 1 | package sandbox 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotImplemented = errors.New("not implemented") 7 | ErrAgentRequired = errors.New("requires agent installed on the sandbox") 8 | ) 9 | -------------------------------------------------------------------------------- /sandbox/sandbox.go: -------------------------------------------------------------------------------- 1 | package sandbox 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/projectdiscovery/gozero/types" 7 | ) 8 | 9 | func IsEnabled(ctx context.Context) (bool, error) { 10 | return isEnabled(ctx) 11 | } 12 | 13 | func IsInstalled(ctx context.Context) (bool, error) { 14 | return isInstalled(ctx) 15 | } 16 | 17 | func Activate(ctx context.Context) (bool, error) { 18 | return activate(ctx) 19 | } 20 | 21 | func Deactivate(ctx context.Context) (bool, error) { 22 | return deactivate(ctx) 23 | } 24 | 25 | type Sandbox interface { 26 | Run(ctx context.Context, cmd string) (*types.Result, error) 27 | Start() error 28 | Wait() error 29 | Stop() error 30 | Clear() error 31 | } 32 | -------------------------------------------------------------------------------- /sandbox/sandbox_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package sandbox 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "strings" 15 | 16 | "github.com/projectdiscovery/gozero/cmdexec" 17 | "github.com/projectdiscovery/gozero/types" 18 | ) 19 | 20 | type Configuration struct { 21 | Rules []Rule 22 | } 23 | 24 | type Action string 25 | 26 | const ( 27 | Allow Action = "allow" 28 | Deny Action = "deny" 29 | ) 30 | 31 | type Scope string 32 | 33 | const ( 34 | Network Scope = "network" 35 | FileWrite Scope = "file-write" 36 | FileRead Scope = "file-read" 37 | Process Scope = "process" 38 | Default Scope = "default" 39 | ) 40 | 41 | type ArgsType string 42 | 43 | const ( 44 | LocalIP = `local ip "%s"` 45 | RemoteIP = `local ip "%s"` 46 | SubPath = `subpath "%s"` 47 | ) 48 | 49 | type Rule struct { 50 | Action Action 51 | Scope Scope 52 | Args []Arg 53 | } 54 | 55 | type Arg struct { 56 | Type ArgsType 57 | Params []any 58 | } 59 | 60 | // Sandbox native on windows 61 | type SandboxDarwin struct { 62 | Config *Configuration 63 | confFile string 64 | } 65 | 66 | // New sandbox with the given configuration 67 | func New(ctx context.Context, config *Configuration) (Sandbox, error) { 68 | if ok, err := IsInstalled(context.Background()); err != nil || !ok { 69 | return nil, errors.New("sandbox feature not installed") 70 | } 71 | 72 | sharedFolder, err := os.MkdirTemp("", "") 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | sharedFolder = filepath.Join(sharedFolder, "gozero") 78 | 79 | if err := os.MkdirAll(sharedFolder, 0755); err != nil { 80 | return nil, err 81 | } 82 | 83 | confFile := filepath.Join(sharedFolder, "config.sb") 84 | var confData bytes.Buffer 85 | confData.WriteString("(version 1)\n") 86 | confData.WriteString("(allow default)\n") 87 | for _, rule := range config.Rules { 88 | if rule.Action != "" { 89 | confData.WriteString("(" + string(rule.Action) + " ") 90 | } 91 | if rule.Scope != "" { 92 | confData.WriteString(string(rule.Scope) + "* ") 93 | } 94 | for _, arg := range rule.Args { 95 | confData.WriteString(fmt.Sprintf("("+string(arg.Type)+")", arg.Params...)) 96 | } 97 | if rule.Action != "" { 98 | confData.WriteString(")") 99 | } 100 | } 101 | if err := os.WriteFile(confFile, confData.Bytes(), 0600); err != nil { 102 | return nil, err 103 | } 104 | 105 | log.Println(confData.String()) 106 | 107 | s := &SandboxDarwin{Config: config, confFile: confFile} 108 | return s, nil 109 | } 110 | 111 | func (s *SandboxDarwin) Run(ctx context.Context, cmd string) (*types.Result, error) { 112 | params := []string{"-f", s.confFile} 113 | params = append(params, strings.Split(cmd, " ")...) 114 | cmdContext, err := cmdexec.NewCommand("sandbox-exec", params...) 115 | if err != nil { 116 | return nil, err 117 | } 118 | return cmdContext.Execute(ctx) 119 | } 120 | 121 | // Start the instance 122 | func (s *SandboxDarwin) Start() error { 123 | return ErrNotImplemented 124 | } 125 | 126 | // Wait for the instance 127 | func (s *SandboxDarwin) Wait() error { 128 | return ErrNotImplemented 129 | } 130 | 131 | // Stop the instance 132 | func (s *SandboxDarwin) Stop() error { 133 | return ErrNotImplemented 134 | } 135 | 136 | // Clear the instance after stop 137 | func (s *SandboxDarwin) Clear() error { 138 | return os.RemoveAll(s.confFile) 139 | } 140 | 141 | func isEnabled(ctx context.Context) (bool, error) { 142 | return isInstalled(ctx) 143 | } 144 | 145 | func isInstalled(ctx context.Context) (bool, error) { 146 | _, err := exec.LookPath("sandbox-exec") 147 | if err != nil { 148 | return false, err 149 | } 150 | return true, nil 151 | } 152 | 153 | func activate(ctx context.Context) (bool, error) { 154 | return false, errors.New("sandbox is a darwin native functionality") 155 | } 156 | 157 | func deactivate(ctx context.Context) (bool, error) { 158 | return false, errors.New("sandbox can't be disabled") 159 | } 160 | -------------------------------------------------------------------------------- /sandbox/sandbox_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package sandbox 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/projectdiscovery/gozero/cmdexec" 12 | "github.com/projectdiscovery/gozero/types" 13 | stringsutil "github.com/projectdiscovery/utils/strings" 14 | ) 15 | 16 | type Configuration struct { 17 | Rules []Rule 18 | } 19 | 20 | type Filter string 21 | 22 | const ( 23 | PrivateTmp Filter = "PrivateTmp" 24 | PrivateNetwork Filter = "PrivateNetwork" 25 | SELinuxContext Filter = "SELinuxContext" 26 | NoNewPrivileges Filter = "NoNewPrivileges" 27 | ProtectSystem Filter = "ProtectSystem" 28 | ProtectHome Filter = "ProtectHome" 29 | ProtectDevices Filter = "ProtectDevices" 30 | CapabilityBoundingSet Filter = "CapabilityBoundingSet" 31 | ReadWriteDirectories Filter = "ReadWriteDirectories" 32 | ReadOnlyDirectories Filter = "ReadOnlyDirectories" 33 | InaccessibleDirectories Filter = "InaccessibleDirectories" 34 | ProtectKernelTunables Filter = "InaccessibleDirectories" 35 | ProtectKernelModules Filter = "ProtectKernelModules" 36 | ProtectControlGroups Filter = "ProtectControlGroups" 37 | RestrictNamespaces Filter = "RestrictNamespaces" 38 | MemoryDenyWriteExecute Filter = "MemoryDenyWriteExecute" 39 | RestrictRealtime Filter = "RestrictRealtime" 40 | PrivateMounts Filter = "PrivateMounts" 41 | DynamicUser Filter = "DynamicUser" 42 | SystemCallFilter Filter = "SystemCallFilter" 43 | ) 44 | 45 | type ArgsType uint8 46 | 47 | const ( 48 | Bool ArgsType = iota 49 | Folders 50 | Capabilities 51 | Namespaces 52 | SystemCalls 53 | ) 54 | 55 | type Arg struct { 56 | Type ArgsType 57 | Params interface{} 58 | } 59 | 60 | type Rule struct { 61 | Filter Filter 62 | Arg Arg 63 | } 64 | 65 | // Sandbox native on linux 66 | type SandboxLinux struct { 67 | Config *Configuration 68 | conf []string 69 | } 70 | 71 | // New sandbox with the given configuration 72 | func New(ctx context.Context, config *Configuration) (Sandbox, error) { 73 | if ok, err := IsInstalled(context.Background()); err != nil || !ok { 74 | return nil, errors.New("sandbox feature not installed") 75 | } 76 | 77 | conf := []string{"--pipe", "--pty", "--user"} 78 | for _, rule := range config.Rules { 79 | var actionArgs []string 80 | if rule.Filter == "" { 81 | return nil, errors.New("empty action") 82 | } 83 | actionArgs = append(actionArgs, "-p") 84 | switch rule.Arg.Type { 85 | case Bool: 86 | v, ok := rule.Arg.Params.(string) 87 | if !ok { 88 | return nil, errors.New("invalid string value") 89 | } 90 | if !stringsutil.EqualFoldAny(v, "yes", "no") { 91 | return nil, errors.New("invalid value (yes/no)") 92 | } 93 | actionArgs = append(actionArgs, string(rule.Filter)+"="+v) 94 | case Folders, Capabilities, Namespaces, SystemCalls: 95 | v, ok := rule.Arg.Params.([]string) 96 | if !ok { 97 | return nil, errors.New("invalid string value") 98 | } 99 | actionArgs = append(actionArgs, string(rule.Filter)+"="+strings.Join(v, ",")) 100 | default: 101 | return nil, errors.New("unsupported type") 102 | } 103 | conf = append(conf, actionArgs...) 104 | } 105 | 106 | s := &SandboxLinux{Config: config, conf: conf} 107 | return s, nil 108 | } 109 | 110 | func (s *SandboxLinux) Run(ctx context.Context, cmd string) (*types.Result, error) { 111 | var params []string 112 | params = append(params, s.conf...) 113 | params = append(params, strings.Split(cmd, " ")...) 114 | cmdContext, err := cmdexec.NewCommand("systemd-run", params...) 115 | return cmdContext.Execute(ctx) 116 | } 117 | 118 | // Start the instance 119 | func (s *SandboxLinux) Start() error { 120 | return ErrNotImplemented 121 | } 122 | 123 | // Wait for the instance 124 | func (s *SandboxLinux) Wait() error { 125 | return ErrNotImplemented 126 | } 127 | 128 | // Stop the instance 129 | func (s *SandboxLinux) Stop() error { 130 | return ErrNotImplemented 131 | } 132 | 133 | // Clear the instance after stop 134 | func (s *SandboxLinux) Clear() error { 135 | return ErrNotImplemented 136 | } 137 | 138 | func isEnabled(ctx context.Context) (bool, error) { 139 | return isInstalled(ctx) 140 | } 141 | 142 | func isInstalled(ctx context.Context) (bool, error) { 143 | _, err := exec.LookPath("systemd-run") 144 | if err != nil { 145 | return false, err 146 | } 147 | return true, nil 148 | } 149 | 150 | func activate(ctx context.Context) (bool, error) { 151 | return false, errors.New("sandbox is a linux native functionality") 152 | } 153 | 154 | func deactivate(ctx context.Context) (bool, error) { 155 | return false, errors.New("can't be disabled") 156 | } 157 | -------------------------------------------------------------------------------- /sandbox/sandbox_other.go: -------------------------------------------------------------------------------- 1 | //go:build !(darwin || linux || windows) 2 | 3 | package sandbox 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/projectdiscovery/gozero/types" 9 | ) 10 | 11 | type Configuration struct { 12 | Rules []Rule 13 | } 14 | 15 | type Rule struct{} 16 | 17 | // Sandbox native on other platforms 18 | type SandboxOther struct { 19 | Config *Configuration 20 | conf []string 21 | } 22 | 23 | // New sandbox with the given configuration 24 | func New(ctx context.Context, config *Configuration) (Sandbox, error) { 25 | return nil, ErrNotImplemented 26 | } 27 | 28 | func (s *SandboxOther) Run(ctx context.Context, cmd string) (*types.Result, error) { 29 | return nil, ErrNotImplemented 30 | } 31 | 32 | // Start the instance 33 | func (s *SandboxOther) Start() error { 34 | return ErrNotImplemented 35 | } 36 | 37 | // Wait for the instance 38 | func (s *SandboxOther) Wait() error { 39 | return ErrNotImplemented 40 | } 41 | 42 | // Stop the instance 43 | func (s *SandboxOther) Stop() error { 44 | return ErrNotImplemented 45 | } 46 | 47 | // Clear the instance after stop 48 | func (s *SandboxOther) Clear() error { 49 | return ErrNotImplemented 50 | } 51 | 52 | func isEnabled(ctx context.Context) (bool, error) { 53 | return false, ErrNotImplemented 54 | } 55 | 56 | func isInstalled(ctx context.Context) (bool, error) { 57 | return false, ErrNotImplemented 58 | } 59 | 60 | func activate(ctx context.Context) (bool, error) { 61 | return false, ErrNotImplemented 62 | } 63 | 64 | func deactivate(ctx context.Context) (bool, error) { 65 | return false, ErrNotImplemented 66 | } 67 | -------------------------------------------------------------------------------- /sandbox/sandbox_window.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package sandbox 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "encoding/xml" 9 | "errors" 10 | "net" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "regexp" 15 | 16 | "github.com/projectdiscovery/gozero/types" 17 | ) 18 | 19 | const DefaultMountPoint = `C:\Users\WDAGUtilityAccount\Desktop` 20 | 21 | type Value string 22 | 23 | const ( 24 | Enable Value = "Enable" 25 | Disable Value = "Disable" 26 | Default Value = "Default" 27 | ) 28 | 29 | type Configuration struct { 30 | MappedFolders MappedFolders `xml:"MappedFolders"` 31 | Networking Value `xml:"Networking"` 32 | LogonCommands LogonCommands `xml:"LogonCommand,omitempty"` 33 | VirtualGPU Value `xml:"vGPU"` 34 | ProtectedClient Value `xml:"ProtectedClient"` 35 | MemoryInMB int `xml:"MemoryInMB"` 36 | IPs IPS `xml:"Ips,omitempty"` 37 | DisableFirewall bool `xml:"-"` 38 | } 39 | 40 | type MappedFolders struct { 41 | MappedFolder []MappedFolder `xml:"MappedFolder"` 42 | } 43 | 44 | type LogonCommands struct { 45 | Command []string `xml:"Command,omitempty"` 46 | } 47 | 48 | type IPS struct { 49 | IP []string `xml:"IP,omitempty"` 50 | } 51 | 52 | type MappedFolder struct { 53 | HostFolder string `xml:"HostFolder"` 54 | SandboxFolder string `xml:"SandboxFolder,omitempty"` 55 | ReadOnly bool `xml:"ReadOnly,omitempty"` 56 | } 57 | 58 | // Sandbox native on windows 59 | type SandboxWindows struct { 60 | Config *Configuration 61 | confFile string 62 | instance *exec.Cmd 63 | stdout bytes.Buffer 64 | stderr bytes.Buffer 65 | } 66 | 67 | // New sandbox with the given configuration 68 | func New(ctx context.Context, config *Configuration) (Sandbox, error) { 69 | if ok, err := IsInstalled(context.Background()); err != nil || !ok { 70 | return nil, errors.New("sandbox feature not installed") 71 | } 72 | 73 | sharedFolder, err := os.MkdirTemp("", "") 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | sharedFolder = filepath.Join(sharedFolder, "gozero") 79 | 80 | if err := os.MkdirAll(sharedFolder, 0600); err != nil { 81 | return nil, err 82 | } 83 | 84 | config.MappedFolders.MappedFolder = append(config.MappedFolders.MappedFolder, MappedFolder{ 85 | HostFolder: sharedFolder, 86 | }) 87 | 88 | if config.DisableFirewall { 89 | config.LogonCommands.Command = append(config.LogonCommands.Command, 90 | "netsh advfirewall set allprofiles state off", 91 | ) 92 | } 93 | // collect all the callback ips 94 | addresses, err := net.InterfaceAddrs() 95 | if err != nil { 96 | return nil, err 97 | } 98 | for _, addr := range addresses { 99 | config.IPs.IP = append(config.IPs.IP, addr.String()) 100 | } 101 | 102 | data, err := xml.Marshal(config) 103 | if err != nil { 104 | return nil, err 105 | } 106 | confFile := filepath.Join(sharedFolder, "config.wsb") 107 | if err := os.WriteFile(confFile, data, 0600); err != nil { 108 | return nil, err 109 | } 110 | 111 | s := &SandboxWindows{Config: config, confFile: confFile} 112 | s.instance = exec.CommandContext(ctx, "WindowsSandbox.exe", s.confFile) 113 | s.instance.Stdout = &s.stdout 114 | s.instance.Stderr = &s.stderr 115 | 116 | return s, nil 117 | } 118 | 119 | func (s *SandboxWindows) Run(ctx context.Context, cmd string) (*types.Result, error) { 120 | return nil, ErrAgentRequired 121 | } 122 | 123 | // Start the instance 124 | func (s *SandboxWindows) Start() error { 125 | return s.instance.Start() 126 | } 127 | 128 | // Wait for the instance 129 | func (s *SandboxWindows) Wait() error { 130 | return s.instance.Wait() 131 | } 132 | 133 | // Stop the instance 134 | func (s *SandboxWindows) Stop() error { 135 | err := s.instance.Cancel() 136 | if err != nil { 137 | return err 138 | } 139 | s.instance = nil 140 | return nil 141 | } 142 | 143 | // Clear the instance after stop 144 | func (s *SandboxWindows) Clear() error { 145 | if err := s.Stop(); err != nil { 146 | return err 147 | } 148 | if err := os.RemoveAll(s.confFile); err != nil { 149 | return err 150 | } 151 | return nil 152 | } 153 | 154 | func shellExec(ctx context.Context, args ...string) (string, string, error) { 155 | powershellPath, err := exec.LookPath("powershell") 156 | if err != nil { 157 | return "", "", err 158 | } 159 | cmd := exec.CommandContext(ctx, powershellPath, args...) 160 | var stdout, stderr bytes.Buffer 161 | cmd.Stdout = &stdout 162 | cmd.Stderr = &stderr 163 | 164 | err = cmd.Run() 165 | return stdout.String(), stderr.String(), err 166 | } 167 | 168 | func isEnabled(ctx context.Context) (bool, error) { 169 | stdout, _, err := shellExec(ctx, "Get-WindowsOptionalFeature", "-FeatureName", `"Containers-DisposableClientVM"`, "-Online") 170 | if err != nil { 171 | return false, err 172 | } 173 | 174 | return regexp.MatchString(`(?m)State\s*:\s*Enabled`, stdout) 175 | } 176 | 177 | func isInstalled(ctx context.Context) (bool, error) { 178 | _, err := exec.LookPath("WindowsSandbox.exe") 179 | if err != nil { 180 | return false, err 181 | } 182 | return true, nil 183 | } 184 | 185 | func activate(ctx context.Context) (bool, error) { 186 | _, _, err := shellExec(ctx, "Enable-WindowsOptionalFeature", "-FeatureName", `"Containers-DisposableClientVM"`, "-NoRestart", "True") 187 | if err != nil { 188 | return false, err 189 | } 190 | 191 | return true, nil 192 | } 193 | 194 | func deactivate(ctx context.Context) (bool, error) { 195 | _, _, err := shellExec(ctx, "Disable-WindowsOptionalFeature", "-FeatureName", `"Containers-DisposableClientVM"`, "-Online") 196 | if err != nil { 197 | return false, err 198 | } 199 | 200 | return true, nil 201 | } 202 | -------------------------------------------------------------------------------- /source.go: -------------------------------------------------------------------------------- 1 | package gozero 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/projectdiscovery/gozero/types" 11 | fileutil "github.com/projectdiscovery/utils/file" 12 | ) 13 | 14 | // Source is a source file for gozero and is meant to 15 | // contain i/o for code execution 16 | type Source struct { 17 | Variables []types.Variable 18 | Temporary bool 19 | CloseAfterWrite bool 20 | Filename string 21 | File *os.File 22 | } 23 | 24 | func NewSource() (*Source, error) { 25 | return NewSourceWithString("", "", "") 26 | } 27 | 28 | func NewSourceWithFile(src string) (*Source, error) { 29 | if fileutil.FileExists(src) { 30 | file, err := os.Open(src) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &Source{Filename: src, File: file}, nil 35 | } 36 | return nil, errors.New("file does not exist") 37 | } 38 | 39 | func NewSourceWithBytes(src []byte, wantedPattern, dir string) (*Source, error) { 40 | return NewSourceWithReader(bytes.NewReader(src), wantedPattern, dir) 41 | } 42 | 43 | func NewSourceWithString(src, wantedPattern, dir string) (*Source, error) { 44 | return NewSourceWithReader(strings.NewReader(src), wantedPattern, dir) 45 | } 46 | 47 | func NewSourceWithReader(src io.Reader, wantedPattern, dir string) (*Source, error) { 48 | srcFile, err := os.CreateTemp(dir, wantedPattern) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | gfileName := srcFile.Name() 54 | if _, err := io.Copy(srcFile, src); err != nil { 55 | return nil, err 56 | } 57 | 58 | if err := srcFile.Sync(); err != nil { 59 | return nil, err 60 | } 61 | 62 | if _, err := srcFile.Seek(0, 0); err != nil { 63 | return nil, err 64 | } 65 | 66 | return &Source{Filename: gfileName, Temporary: true, File: srcFile}, nil 67 | } 68 | 69 | func (s *Source) Close() error { 70 | if s.File != nil { 71 | return s.File.Close() 72 | } 73 | return nil 74 | } 75 | 76 | func (s *Source) Cleanup() error { 77 | if err := s.Close(); err != nil { 78 | return err 79 | } 80 | if s.Temporary { 81 | return os.RemoveAll(s.Filename) 82 | } 83 | return nil 84 | } 85 | 86 | func (s *Source) ReadAll() ([]byte, error) { 87 | return os.ReadFile(s.Filename) 88 | } 89 | 90 | func (s *Source) AddVariable(vars ...types.Variable) { 91 | s.Variables = append(s.Variables, vars...) 92 | } 93 | -------------------------------------------------------------------------------- /source_test.go: -------------------------------------------------------------------------------- 1 | package gozero 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestNewSourceWithFile(t *testing.T) { 12 | tempFile, err := os.CreateTemp("", "testsource") 13 | if err != nil { 14 | t.Fatalf("Failed to create temporary file: %v", err) 15 | } 16 | defer os.Remove(tempFile.Name()) // clean up 17 | 18 | content := []byte("temporary file's content") 19 | if _, err := tempFile.Write(content); err != nil { 20 | t.Fatalf("Failed to write to temporary file: %v", err) 21 | } 22 | if err := tempFile.Close(); err != nil { 23 | t.Fatalf("Failed to close temporary file: %v", err) 24 | } 25 | 26 | source, err := NewSourceWithFile(tempFile.Name()) 27 | if err != nil { 28 | t.Fatalf("Failed to create new source with file: %v", err) 29 | } 30 | if source == nil { 31 | t.Fatal("NewSourceWithFile returned nil source") 32 | } 33 | if source.Filename != tempFile.Name() { 34 | t.Errorf("Expected filename to be %v, got %v", tempFile.Name(), source.Filename) 35 | } 36 | if source.File == nil { 37 | t.Error("Expected non-nil File in new source") 38 | } 39 | if source.Temporary { 40 | t.Error("Expected new source not to be temporary") 41 | } 42 | 43 | readContent, err := os.ReadFile(source.Filename) 44 | if err != nil { 45 | t.Fatalf("Failed to read from source file: %v", err) 46 | } 47 | if !bytes.Equal(content, readContent) { 48 | t.Errorf("Read content does not match written content") 49 | } 50 | 51 | // Clean up 52 | if err := source.Cleanup(); err != nil { 53 | t.Errorf("Failed to cleanup new source with file: %v", err) 54 | } 55 | } 56 | 57 | func TestNewSourceWithReader(t *testing.T) { 58 | content := []byte("content from reader") 59 | buffer := bytes.NewBuffer(content) 60 | 61 | pattern := "testsource-*" 62 | tempDir := t.TempDir() 63 | 64 | source, err := NewSourceWithReader(buffer, pattern, tempDir) 65 | if err != nil { 66 | t.Fatalf("Failed to create new source with reader: %v", err) 67 | } 68 | defer func() { 69 | if err := source.Cleanup(); err != nil { 70 | t.Errorf("Failed to cleanup new source with reader: %v", err) 71 | } 72 | }() 73 | 74 | if !source.Temporary { 75 | t.Error("Expected source to be marked as temporary") 76 | } 77 | 78 | if !strings.HasPrefix(filepath.Base(source.Filename), "testsource-") { 79 | t.Errorf("Expected file to have prefix 'testsource-', got %s", filepath.Base(source.Filename)) 80 | } 81 | if !strings.Contains(source.Filename, tempDir) { 82 | t.Errorf("Expected file to be created in directory %s, got %s", tempDir, filepath.Dir(source.Filename)) 83 | } 84 | 85 | readContent, err := os.ReadFile(source.Filename) 86 | if err != nil { 87 | t.Fatalf("Failed to read from source file: %v", err) 88 | } 89 | if !bytes.Equal(content, readContent) { 90 | t.Errorf("Read content does not match content from reader") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /types/result.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "os/exec" 6 | ) 7 | 8 | // Note: 9 | // Current use-case does not return a >1mb output from the command 10 | // so we use bytes.Buffer instead of os.File (temporary file) 11 | // with use case changes we can change this to os.File later 12 | 13 | // Result contains the result of a command execution. 14 | type Result struct { 15 | Command string // Include final command that was executed 16 | Stdout bytes.Buffer 17 | Stderr bytes.Buffer 18 | exitErr *exec.ExitError // return exit error this includes exit code , command sysusage and more 19 | DebugData *bytes.Buffer // only available when debug mode is enabled 20 | } 21 | 22 | // GetExitError returns the exit error if any. 23 | func (r *Result) GetExitError() *exec.ExitError { 24 | return r.exitErr 25 | } 26 | 27 | // SetExitError sets the exit error (internal use only). 28 | func (r *Result) SetExitError(err *exec.ExitError) { 29 | r.exitErr = err 30 | } 31 | 32 | // GetExitCode returns the exit code of the command. 33 | func (r *Result) GetExitCode() int { 34 | if r.exitErr == nil { 35 | return 0 36 | } 37 | return r.exitErr.ExitCode() 38 | } 39 | -------------------------------------------------------------------------------- /types/vars.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "fmt" 4 | 5 | type Variable struct { 6 | Name string 7 | Value string 8 | } 9 | 10 | func (v *Variable) String() string { 11 | return fmt.Sprintf("%s=%s", v.Name, v.Value) 12 | } 13 | --------------------------------------------------------------------------------