// Copyright 2020 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package hexec import ( "bytes" "context" "errors" "fmt" "io" "os" "os/exec" "regexp" "strings" "github.com/cli/safeexec" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/security" ) var WithDir = func(dir string) func(c *commandeer) { return func(c *commandeer) { c.dir = dir } } var WithContext = func(ctx context.Context) func(c *commandeer) { return func(c *commandeer) { c.ctx = ctx } } var WithStdout = func(w io.Writer) func(c *commandeer) { return func(c *commandeer) { c.stdout = w } } var WithStderr = func(w io.Writer) func(c *commandeer) { return func(c *commandeer) { c.stderr = w } } var WithStdin = func(r io.Reader) func(c *commandeer) { return func(c *commandeer) { c.stdin = r } } var WithEnviron = func(env []string) func(c *commandeer) { return func(c *commandeer) { setOrAppend := func(s string) { k1, _ := config.SplitEnvVar(s) var found bool for i, v := range c.env { k2, _ := config.SplitEnvVar(v) if k1 == k2 { found = true c.env[i] = s } } if !found { c.env = append(c.env, s) } } for _, s := range env { setOrAppend(s) } } } // New creates a new Exec using the provided security config. func New(cfg security.Config) *Exec { var baseEnviron []string for _, v := range os.Environ() { k, _ := config.SplitEnvVar(v) if cfg.Exec.OsEnv.Accept(k) { baseEnviron = append(baseEnviron, v) } } return &Exec{ sc: cfg, baseEnviron: baseEnviron, } } // IsNotFound reports whether this is an error about a binary not found. func IsNotFound(err error) bool { var notFoundErr *NotFoundError return errors.As(err, ¬FoundErr) } // SafeCommand is a wrapper around os/exec Command which uses a LookPath // implementation that does not search in current directory before looking in PATH. // See https://github.com/cli/safeexec and the linked issues. func SafeCommand(name string, arg ...string) (*exec.Cmd, error) { bin, err := safeexec.LookPath(name) if err != nil { return nil, err } return exec.Command(bin, arg...), nil } // Exec enforces a security policy for commands run via os/exec. type Exec struct { sc security.Config // os.Environ filtered by the Exec.OsEnviron whitelist filter. baseEnviron []string } // New will fail if name is not allowed according to the configured security policy. // Else a configured Runner will be returned ready to be Run. func (e *Exec) New(name string, arg ...any) (Runner, error) { if err := e.sc.CheckAllowedExec(name); err != nil { return nil, err } env := make([]string, len(e.baseEnviron)) copy(env, e.baseEnviron) cm := &commandeer{ name: name, env: env, } return cm.command(arg...) } // Npx will try to run npx, and if that fails, it will // try to run the binary directly. func (e *Exec) Npx(name string, arg ...any) (Runner, error) { r, err := e.npx(name, arg...) if err == nil { return r, nil } return e.New(name, arg...) } // npx is a convenience method to create a Runner running npx --no-install