mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
205338bfa3
> Related issue: #16019 # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Manual QA for all new/changed functionality
163 lines
3.7 KiB
Go
163 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"unicode/utf8"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/service"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
func runScriptCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "run-script",
|
|
Aliases: []string{"run_script"},
|
|
Usage: `Run a live script on one host and get results back.`,
|
|
UsageText: `fleetctl run-script [options]`,
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "script-path",
|
|
Usage: "The path to the script.",
|
|
Required: true,
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "host",
|
|
Usage: "A host, specified by hostname, serial number, UUID, osquery host ID, or node key.",
|
|
Required: true,
|
|
},
|
|
configFlag(),
|
|
contextFlag(),
|
|
debugFlag(),
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
client, err := clientFromCLI(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
appCfg, err := client.GetAppConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if appCfg.ServerSettings.ScriptsDisabled {
|
|
return errors.New(fleet.RunScriptScriptsDisabledGloballyErrMsg)
|
|
}
|
|
|
|
path := c.String("script-path")
|
|
if err := validateScriptPath(path); err != nil {
|
|
return err
|
|
}
|
|
|
|
ident := c.String("host")
|
|
h, err := client.HostByIdentifier(ident)
|
|
if err != nil {
|
|
var nfe service.NotFoundErr
|
|
if errors.As(err, &nfe) {
|
|
return errors.New(fleet.RunScriptHostNotFoundErrMsg)
|
|
}
|
|
var sce fleet.ErrWithStatusCode
|
|
if errors.As(err, &sce) {
|
|
if sce.StatusCode() == http.StatusForbidden {
|
|
return errors.New(fleet.RunScriptForbiddenErrMsg)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
if h.Status != fleet.StatusOnline {
|
|
return errors.New(fleet.RunScriptHostOfflineErrMsg)
|
|
}
|
|
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := fleet.ValidateHostScriptContents(string(b)); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("\nScript is running. Please wait for it to finish...")
|
|
|
|
res, err := client.RunHostScriptSync(h.ID, b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := renderScriptResult(c, res); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func renderScriptResult(c *cli.Context, res *fleet.HostScriptResult) error {
|
|
tmpl := template.Must(template.New("").Parse(`
|
|
{{ if .ErrorMsg -}}
|
|
Error: {{ .ErrorMsg }}
|
|
{{- else -}}
|
|
Exit code: {{ .ExitCode }} ({{ .ExitMessage }})
|
|
{{- end }}
|
|
{{ if .ShowOutput }}
|
|
Output {{- if .ExecTimeout }} before timeout {{- end }}:
|
|
|
|
-------------------------------------------------------------------------------------
|
|
|
|
{{ .Output }}
|
|
|
|
-------------------------------------------------------------------------------------
|
|
{{- end }}
|
|
`))
|
|
|
|
data := struct {
|
|
ExecTimeout bool
|
|
ErrorMsg string
|
|
ExitCode *int64
|
|
ExitMessage string
|
|
Output string
|
|
ShowOutput bool
|
|
}{
|
|
ExitCode: res.ExitCode,
|
|
ExitMessage: "Script failed.",
|
|
ShowOutput: true,
|
|
}
|
|
|
|
switch {
|
|
case res.ExitCode == nil:
|
|
data.ErrorMsg = res.Message
|
|
case *res.ExitCode == -2:
|
|
data.ShowOutput = false
|
|
data.ErrorMsg = res.Message
|
|
case *res.ExitCode == -1:
|
|
data.ExecTimeout = true
|
|
data.ErrorMsg = res.Message
|
|
case *res.ExitCode == 0:
|
|
data.ExitMessage = "Script ran successfully."
|
|
}
|
|
|
|
if len(res.Output) >= fleet.MaxScriptRuneLen && utf8.RuneCountInString(res.Output) >= fleet.MaxScriptRuneLen {
|
|
data.Output = "Fleet records the last 10,000 characters to prevent downtime.\n\n" + res.Output
|
|
} else {
|
|
data.Output = res.Output
|
|
}
|
|
|
|
return tmpl.Execute(c.App.Writer, data)
|
|
}
|
|
|
|
func validateScriptPath(path string) error {
|
|
extension := filepath.Ext(path)
|
|
if extension == ".sh" || extension == ".ps1" {
|
|
return nil
|
|
}
|
|
return errors.New(fleet.RunScriptInvalidTypeErrMsg)
|
|
}
|