2023-09-05 19:14:09 +00:00
package main
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/stretchr/testify/require"
)
func TestRunScriptCommand ( t * testing . T ) {
2024-01-08 21:28:45 +00:00
_ , ds := runServerWithMockedDS ( t ,
& service . TestServerOpts {
License : & fleet . LicenseInfo {
Tier : fleet . TierPremium ,
} ,
} ,
& service . TestServerOpts {
HTTPServerConfig : & http . Server { WriteTimeout : 90 * time . Second } , // nolint:gosec
2023-09-05 19:14:09 +00:00
} ,
2024-01-08 21:28:45 +00:00
)
2023-09-05 19:14:09 +00:00
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
return nil
}
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return nil , nil
}
ds . ListPoliciesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
return nil , nil
}
ds . ListHostBatteriesFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
2024-01-08 21:28:45 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { ServerSettings : fleet . ServerSettings { ScriptsDisabled : false } } , nil
}
2024-03-05 14:53:17 +00:00
ds . GetScriptIDByNameFunc = func ( ctx context . Context , name string , teamID * uint ) ( uint , error ) {
return 1 , nil
}
ds . IsExecutionPendingForHostFunc = func ( ctx context . Context , hid uint , scriptID uint ) ( [ ] * uint , error ) {
return [ ] * uint { } , nil
}
2023-09-05 19:14:09 +00:00
generateValidPath := func ( ) string {
return writeTmpScriptContents ( t , "echo hello world" , ".sh" )
}
2024-03-05 17:54:26 +00:00
exceedsMaxCharsUnsaved := strings . Repeat ( "a" , fleet . UnsavedScriptMaxRuneLen + 1 )
exceedsMaxCharsSaved := strings . Repeat ( "a" , fleet . SavedScriptMaxRuneLen + 1 )
expectedOutputSuccess := `
Exit code : 0 ( Script ran successfully . )
Output :
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
hello world
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
`
2023-09-05 19:14:09 +00:00
2024-05-07 15:10:22 +00:00
expectedQuietOutputSuccess := ` hello world
`
2023-09-05 19:14:09 +00:00
type testCase struct {
2024-03-05 17:54:26 +00:00
name string
scriptPath func ( ) string
scriptName string
teamID * uint
savedScriptContents func ( ) ( [ ] byte , error )
scriptResult * fleet . HostScriptResult
2024-05-07 15:10:22 +00:00
quiet bool
async bool
2024-03-05 17:54:26 +00:00
expectOutput string
expectErrMsg string
expectNotFound bool
expectOffline bool
expectPending bool
2023-09-05 19:14:09 +00:00
}
cases := [ ] testCase {
{
name : "host offline" ,
scriptPath : generateValidPath ,
expectErrMsg : fleet . RunScriptHostOfflineErrMsg ,
expectOffline : true ,
} ,
{
name : "host not found" ,
scriptPath : generateValidPath ,
expectErrMsg : fleet . RunScriptHostNotFoundErrMsg ,
expectNotFound : true ,
} ,
{
name : "invalid file type" ,
scriptPath : func ( ) string { return writeTmpScriptContents ( t , "echo hello world" , ".txt" ) } ,
expectErrMsg : fleet . RunScriptInvalidTypeErrMsg ,
} ,
{
name : "invalid hashbang" ,
scriptPath : func ( ) string { return writeTmpScriptContents ( t , "#! /foo/bar" , ".sh" ) } ,
2024-04-30 18:38:56 +00:00
expectErrMsg : ` Interpreter not supported. Shell scripts must run in "#!/bin/sh" or "#!/bin/zsh." ` ,
} ,
{
name : "unsupported hashbang" ,
scriptPath : func ( ) string { return writeTmpScriptContents ( t , "#!/bin/ksh" , ".sh" ) } ,
expectErrMsg : ` Interpreter not supported. Shell scripts must run in "#!/bin/sh" or "#!/bin/zsh." ` ,
} ,
{
name : "posix shell hashbang" ,
scriptPath : func ( ) string { return writeTmpScriptContents ( t , "#!/bin/sh" , ".sh" ) } ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 0 ) ,
Output : "hello world" ,
} ,
expectOutput : expectedOutputSuccess ,
} ,
{
name : "zsh hashbang" ,
scriptPath : func ( ) string { return writeTmpScriptContents ( t , "#!/bin/zsh" , ".sh" ) } ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 0 ) ,
Output : "hello world" ,
} ,
expectOutput : expectedOutputSuccess ,
} ,
{
name : "usr zsh hashbang" ,
scriptPath : func ( ) string { return writeTmpScriptContents ( t , "#!/usr/bin/zsh" , ".sh" ) } ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 0 ) ,
Output : "hello world" ,
} ,
expectOutput : expectedOutputSuccess ,
} ,
{
name : "zsh hashbang with arguments" ,
scriptPath : func ( ) string { return writeTmpScriptContents ( t , "#!/bin/zsh -x" , ".sh" ) } ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 0 ) ,
Output : "hello world" ,
} ,
expectOutput : expectedOutputSuccess ,
2023-09-05 19:14:09 +00:00
} ,
{
2024-03-05 14:53:17 +00:00
name : "script too long (unsaved)" ,
2023-09-05 19:14:09 +00:00
scriptPath : func ( ) string {
2024-03-05 17:54:26 +00:00
return writeTmpScriptContents ( t , exceedsMaxCharsUnsaved , ".sh" )
2023-09-05 19:14:09 +00:00
} ,
2024-03-05 14:53:17 +00:00
expectErrMsg : "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'." ,
} ,
2024-03-05 17:54:26 +00:00
{
name : "script not too long (unsaved)" ,
scriptPath : func ( ) string {
return writeTmpScriptContents ( t , exceedsMaxCharsUnsaved [ : fleet . UnsavedScriptMaxRuneLen ] , ".sh" )
} ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 0 ) ,
Output : "hello world" ,
} ,
expectOutput : expectedOutputSuccess ,
} ,
{
name : "script too long (saved)" ,
scriptName : "foo" ,
savedScriptContents : func ( ) ( [ ] byte , error ) {
return [ ] byte ( exceedsMaxCharsSaved ) , nil
} ,
expectErrMsg : "Script is too large. It's limited to 500,000 characters (approximately 10,000 lines)." ,
} ,
{
name : "script not too long (saved)" ,
scriptName : "foo" ,
savedScriptContents : func ( ) ( [ ] byte , error ) {
return [ ] byte ( exceedsMaxCharsUnsaved ) , nil
} ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 0 ) ,
Output : "hello world" ,
} ,
expectOutput : expectedOutputSuccess ,
} ,
2024-03-05 14:53:17 +00:00
{
name : "script-path and script-name disallowed" ,
scriptPath : generateValidPath ,
scriptName : "foo" ,
2024-05-07 15:10:22 +00:00
expectErrMsg : ` Only one of '--script-path' or '--script-name' or '-- <contents>' is allowed. ` ,
2024-03-05 14:53:17 +00:00
} ,
{
name : "missing one of script-path and script-nqme" ,
2024-05-07 15:10:22 +00:00
expectErrMsg : ` One of '--script-path' or '--script-name' or '-- <contents>' must be specified. ` ,
2024-03-05 14:53:17 +00:00
} ,
{
2024-03-05 17:54:26 +00:00
name : "script-path and team disallowed" ,
2024-03-05 14:53:17 +00:00
scriptPath : generateValidPath ,
teamID : ptr . Uint ( 1 ) ,
2024-03-05 17:54:26 +00:00
expectErrMsg : ` Only one of '--script-path' or '--team' is allowed. ` ,
2023-09-05 19:14:09 +00:00
} ,
{
name : "script empty" ,
scriptPath : func ( ) string { return writeTmpScriptContents ( t , "" , ".sh" ) } ,
expectErrMsg : ` Script contents must not be empty. ` ,
} ,
{
name : "invalid utf8" ,
scriptPath : func ( ) string { return writeTmpScriptContents ( t , "\xff\xfa" , ".sh" ) } ,
expectErrMsg : ` Wrong data format. Only plain text allowed. ` ,
} ,
{
name : "script already running" ,
scriptPath : generateValidPath ,
expectErrMsg : fleet . RunScriptAlreadyRunningErrMsg ,
expectPending : true ,
} ,
{
name : "script successful" ,
scriptPath : generateValidPath ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 0 ) ,
Output : "hello world" ,
} ,
2024-03-05 17:54:26 +00:00
expectOutput : expectedOutputSuccess ,
2023-09-05 19:14:09 +00:00
} ,
2024-05-07 15:10:22 +00:00
{
name : "script quiet" ,
scriptPath : generateValidPath ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 0 ) ,
Output : "hello world\n" ,
} ,
expectOutput : expectedQuietOutputSuccess ,
quiet : true ,
} ,
2023-09-05 19:14:09 +00:00
{
name : "script failed" ,
scriptPath : generateValidPath ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 1 ) ,
Output : "" ,
} ,
expectOutput : `
Exit code : 1 ( Script failed . )
Output :
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
` ,
} ,
{
name : "script killed" ,
scriptPath : generateValidPath ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( - 1 ) ,
Output : "Oh no!" ,
2024-01-03 19:39:53 +00:00
Message : fleet . RunScriptScriptTimeoutErrMsg ,
2023-09-05 19:14:09 +00:00
} ,
expectOutput : `
2024-01-03 19:39:53 +00:00
Error : Timeout . Fleet stopped the script after 5 minutes to protect host performance .
2023-09-05 19:14:09 +00:00
Output before timeout :
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
Oh no !
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
` ,
} ,
{
name : "scripts disabled" ,
scriptPath : generateValidPath ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( - 2 ) ,
Output : "" ,
2024-01-10 21:54:41 +00:00
Message : fleet . RunScriptDisabledErrMsg ,
2023-09-05 19:14:09 +00:00
} ,
expectOutput : `
2024-01-31 22:39:18 +00:00
Error : Scripts are disabled for this host . To run scripts , deploy the fleetd agent with scripts enabled .
2023-09-05 19:14:09 +00:00
` ,
} ,
{
name : "output truncated" ,
scriptPath : generateValidPath ,
scriptResult : & fleet . HostScriptResult {
ExitCode : ptr . Int64 ( 0 ) ,
2024-03-05 17:54:26 +00:00
Output : exceedsMaxCharsUnsaved ,
2023-09-05 19:14:09 +00:00
} ,
expectOutput : fmt . Sprintf ( `
Exit code : 0 ( Script ran successfully . )
Output :
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
Fleet records the last 10 , 000 characters to prevent downtime .
% s
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
2024-03-05 17:54:26 +00:00
` , exceedsMaxCharsUnsaved ) ,
2023-09-05 19:14:09 +00:00
} ,
2024-01-03 19:39:53 +00:00
// TODO: this would take 5 minutes to run, we don't want that kind of slowdown in our test suite
// but can be useful to have around for manual testing.
//{
// name: "host timeout",
// scriptPath: generateValidPath,
// expectErrMsg: fleet.RunScriptHostTimeoutErrMsg,
//},
2024-01-08 21:28:45 +00:00
{ name : "disabled scripts globally" , scriptPath : generateValidPath , expectErrMsg : fleet . RunScriptScriptsDisabledGloballyErrMsg } ,
2023-09-05 19:14:09 +00:00
}
setupDS := func ( t * testing . T , c testCase ) {
ds . HostByIdentifierFunc = func ( ctx context . Context , ident string ) ( * fleet . Host , error ) {
if ident != "host1" || c . expectNotFound {
return nil , & notFoundError { }
}
2024-02-15 20:34:16 +00:00
return & fleet . Host { ID : 42 , SeenTime : time . Now ( ) , OrbitNodeKey : ptr . String ( "abc" ) } , nil
2023-09-05 19:14:09 +00:00
}
ds . HostFunc = func ( ctx context . Context , hid uint ) ( * fleet . Host , error ) {
if hid != 42 || c . expectNotFound {
return nil , & notFoundError { }
}
2024-02-15 20:34:16 +00:00
h := fleet . Host { ID : hid , SeenTime : time . Now ( ) , OrbitNodeKey : ptr . String ( "abc" ) }
2023-09-05 19:14:09 +00:00
if c . expectOffline {
h . SeenTime = time . Now ( ) . Add ( - time . Hour )
}
return & h , nil
}
2024-01-29 14:37:54 +00:00
ds . ListPendingHostScriptExecutionsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostScriptResult , error ) {
2023-09-05 19:14:09 +00:00
require . Equal ( t , uint ( 42 ) , hid )
if c . expectPending {
return [ ] * fleet . HostScriptResult { { HostID : uint ( 42 ) } } , nil
}
return nil , nil
}
ds . GetHostScriptExecutionResultFunc = func ( ctx context . Context , execID string ) ( * fleet . HostScriptResult , error ) {
if c . scriptResult != nil {
return c . scriptResult , nil
}
return & fleet . HostScriptResult { } , nil
}
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
return & fleet . HostLockWipeStatus { } , nil
}
2023-09-05 19:14:09 +00:00
ds . NewHostScriptExecutionRequestFunc = func ( ctx context . Context , req * fleet . HostScriptRequestPayload ) ( * fleet . HostScriptResult , error ) {
require . Equal ( t , uint ( 42 ) , req . HostID )
return & fleet . HostScriptResult {
Hostname : "host1" ,
HostID : req . HostID ,
ScriptContents : req . ScriptContents ,
} , nil
}
2024-01-08 21:28:45 +00:00
if c . name == "disabled scripts globally" {
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { ServerSettings : fleet . ServerSettings { ScriptsDisabled : true } } , nil
}
2024-03-05 14:53:17 +00:00
} else {
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { ServerSettings : fleet . ServerSettings { ScriptsDisabled : false } } , nil
}
2024-01-08 21:28:45 +00:00
}
2024-03-05 17:54:26 +00:00
if c . savedScriptContents != nil {
ds . GetScriptContentsFunc = func ( ctx context . Context , id uint ) ( [ ] byte , error ) {
return c . savedScriptContents ( )
}
ds . ScriptFunc = func ( ctx context . Context , id uint ) ( * fleet . Script , error ) {
return & fleet . Script { ID : id , Name : "foo" } , nil
}
} else {
ds . GetScriptContentsFunc = func ( ctx context . Context , id uint ) ( [ ] byte , error ) {
return [ ] byte ( "echo hello world" ) , nil
}
ds . ScriptFunc = func ( ctx context . Context , id uint ) ( * fleet . Script , error ) {
return & fleet . Script { ID : id , Name : "foo" } , nil
}
}
2023-09-05 19:14:09 +00:00
}
for _ , c := range cases {
t . Run ( c . name , func ( t * testing . T ) {
setupDS ( t , c )
2024-03-05 14:53:17 +00:00
args := [ ] string { "run-script" , "--host" , "host1" }
if c . scriptPath != nil {
scriptPath := c . scriptPath ( )
defer os . Remove ( scriptPath )
args = append ( args , "--script-path" , scriptPath )
}
if c . scriptName != "" {
args = append ( args , "--script-name" , c . scriptName )
}
2024-05-07 15:10:22 +00:00
if c . quiet {
args = append ( args , "--quiet" )
}
if c . async {
args = append ( args , "--async" )
}
2024-03-05 14:53:17 +00:00
if c . teamID != nil {
2024-03-05 17:54:26 +00:00
args = append ( args , "--team" , fmt . Sprintf ( "%d" , * c . teamID ) )
2024-03-05 14:53:17 +00:00
}
2023-09-05 19:14:09 +00:00
2024-03-05 14:53:17 +00:00
b , err := runAppNoChecks ( args )
2023-09-05 19:14:09 +00:00
if c . expectErrMsg != "" {
require . Error ( t , err )
require . Contains ( t , err . Error ( ) , c . expectErrMsg )
} else {
require . NoError ( t , err )
}
if c . scriptResult != nil {
out := b . String ( )
require . NoError ( t , err )
require . NotEmpty ( t , out )
require . Equal ( t , c . expectOutput , out )
} else {
require . Empty ( t , b . String ( ) )
}
} )
}
}
func writeTmpScriptContents ( t * testing . T , scriptContents string , extension string ) string {
tmpFile , err := os . CreateTemp ( t . TempDir ( ) , "*" + extension )
require . NoError ( t , err )
_ , err = tmpFile . WriteString ( scriptContents )
require . NoError ( t , err )
return tmpFile . Name ( )
}