2023-09-05 19:14:09 +00:00
package main
import (
"errors"
2024-01-03 19:39:53 +00:00
"fmt"
2023-09-05 19:14:09 +00:00
"html/template"
"net/http"
"os"
"path/filepath"
2024-03-05 14:53:17 +00:00
"strings"
2023-09-05 19:14:09 +00:00
"unicode/utf8"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/urfave/cli/v2"
)
2024-05-07 15:10:22 +00:00
// Helper function to convert a boolean to an integer
func boolToInt ( b bool ) int {
if b {
return 1
}
return 0
}
2023-09-05 19:14:09 +00:00
func runScriptCommand ( ) * cli . Command {
return & cli . Command {
Name : "run-script" ,
Aliases : [ ] string { "run_script" } ,
2024-03-05 14:53:17 +00:00
Usage : ` Run a live script on one host and get results back (5 minute timeout). ` ,
2023-09-05 19:14:09 +00:00
UsageText : ` fleetctl run-script [options] ` ,
Flags : [ ] cli . Flag {
& cli . StringFlag {
Name : "script-path" ,
Usage : "The path to the script." ,
2024-03-05 14:53:17 +00:00
Required : false ,
2023-09-05 19:14:09 +00:00
} ,
& cli . StringFlag {
Name : "host" ,
2023-11-29 14:39:35 +00:00
Usage : "A host, specified by hostname, serial number, UUID, osquery host ID, or node key." ,
2023-09-05 19:14:09 +00:00
Required : true ,
} ,
2024-03-05 14:53:17 +00:00
& cli . StringFlag {
Name : "script-name" ,
Usage : "Name of saved script to run." ,
Required : false ,
} ,
& cli . UintFlag {
2024-03-05 17:54:26 +00:00
Name : "team" ,
2024-03-05 14:53:17 +00:00
Usage : ` Available in Fleet Premium. ID of the team that the saved script belongs to. 0 targets hosts assigned to “No team” (default: 0). ` ,
Required : false ,
} ,
2024-05-07 15:10:22 +00:00
& cli . BoolFlag {
Name : "async" ,
Usage : ` Queue the script and don't wait for the return. ` ,
Required : false ,
} ,
& cli . BoolFlag {
Name : "quiet" ,
Usage : ` Suppress messages that are not the script output / error ` ,
Required : false ,
} ,
2023-09-05 19:14:09 +00:00
configFlag ( ) ,
contextFlag ( ) ,
debugFlag ( ) ,
} ,
Action : func ( c * cli . Context ) error {
client , err := clientFromCLI ( c )
if err != nil {
return err
}
2024-01-08 21:28:45 +00:00
appCfg , err := client . GetAppConfig ( )
if err != nil {
return err
}
if appCfg . ServerSettings . ScriptsDisabled {
return errors . New ( fleet . RunScriptScriptsDisabledGloballyErrMsg )
}
2024-05-07 15:10:22 +00:00
async := c . Bool ( "async" )
quiet := c . Bool ( "quiet" )
// Require 1 and only 1 of these 3 options
2023-09-05 19:14:09 +00:00
path := c . String ( "script-path" )
2024-03-05 14:53:17 +00:00
name := c . String ( "script-name" )
2024-05-07 15:10:22 +00:00
args := c . Args ( ) . Len ( )
notEmpty := boolToInt ( path != "" ) + boolToInt ( name != "" ) + boolToInt ( args > 0 )
2024-03-05 14:53:17 +00:00
2024-05-07 15:10:22 +00:00
if notEmpty < 1 {
return errors . New ( "One of '--script-path' or '--script-name' or '-- <contents>' must be specified." )
2024-03-05 14:53:17 +00:00
}
2024-05-07 15:10:22 +00:00
if notEmpty > 1 {
return errors . New ( "Only one of '--script-path' or '--script-name' or '-- <contents>' is allowed." )
2024-03-05 14:53:17 +00:00
}
if path != "" {
if err := validateScriptPath ( path ) ; err != nil {
return err
}
2023-09-05 19:14:09 +00:00
}
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 )
}
2024-03-05 14:53:17 +00:00
var b [ ] byte
2024-05-07 15:10:22 +00:00
if path != "" || args > 0 {
if path != "" {
b , err = os . ReadFile ( path )
if err != nil {
return err
}
}
if args > 0 {
commandString := strings . Join ( c . Args ( ) . Slice ( ) , " " )
b = [ ] byte ( commandString )
2024-03-05 14:53:17 +00:00
}
2023-09-05 19:14:09 +00:00
2024-03-05 14:53:17 +00:00
// validate script contents with isSavedScript flag set to false so that we check
// for the shorter
if err := fleet . ValidateHostScriptContents ( string ( b ) , false ) ; err != nil {
if err . Error ( ) == fleet . RunScripUnsavedMaxLenErrMsg {
return errors . New ( "Script is too large. Script referenced by '--script-path' is limited to 10,000 characters. To run larger script save it to Fleet and use '--script-name'." )
}
return err
}
2023-09-05 19:14:09 +00:00
}
2024-05-07 15:10:22 +00:00
if async {
res , err := client . RunHostScriptAsync ( h . ID , b , name , c . Uint ( "team" ) )
if err != nil {
if strings . Contains ( err . Error ( ) , ` Only one of 'script_contents' or 'team_id' is allowed ` ) {
return errors . New ( "Only one of '--script-path' or '--team' is allowed." )
}
return err
}
fmt . Fprintf ( c . App . Writer , "%s\n" , res . ExecutionID )
return nil
}
if ! quiet {
fmt . Println ( "\nScript is running. Please wait for it to finish..." )
}
2024-01-03 19:39:53 +00:00
2024-03-05 17:54:26 +00:00
res , err := client . RunHostScriptSync ( h . ID , b , name , c . Uint ( "team" ) )
2023-09-05 19:14:09 +00:00
if err != nil {
2024-03-05 14:53:17 +00:00
if strings . Contains ( err . Error ( ) , ` Only one of 'script_contents' or 'team_id' is allowed ` ) {
2024-03-05 17:54:26 +00:00
return errors . New ( "Only one of '--script-path' or '--team' is allowed." )
2024-03-05 14:53:17 +00:00
}
2023-09-05 19:14:09 +00:00
return err
}
2024-05-07 15:10:22 +00:00
if ! quiet {
if err := renderScriptResult ( c , res ) ; err != nil {
return err
}
} else {
fmt . Fprintf ( c . App . Writer , "%s" , res . Output )
2023-09-05 19:14:09 +00:00
}
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."
}
2024-03-05 14:53:17 +00:00
if len ( res . Output ) >= fleet . UnsavedScriptMaxRuneLen && utf8 . RuneCountInString ( res . Output ) >= fleet . UnsavedScriptMaxRuneLen {
2023-09-05 19:14:09 +00:00
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 )
}