14753 windows ps1 api (#15113)

# 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] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Jahziel Villasana-Espinoza
2023-11-14 09:23:51 -05:00
committed by GitHub
parent 965a78d2de
commit 70048f5f91
10 changed files with 86 additions and 34 deletions
+4
View File
@@ -0,0 +1,4 @@
- Updated `POST /scripts` to allow `.ps1` scripts for Windows
- Updated `fleetctl` output to reflect support for `.ps1` scripts
- Updated `GET /hosts/{id}/scripts` to return `.sh` scripts for MacOS hosts and `.ps1` scripts for
Windows hosts.
+3 -3
View File
@@ -365,8 +365,8 @@ func (svc *Service) GetHostScriptDetails(ctx context.Context, hostID uint, opt f
return nil, nil, err
}
if h.Platform != "darwin" {
// only darwin is supported for now, all other platforms return empty results
if h.Platform != "darwin" && h.Platform != "windows" {
// darwin and windows are supported for now, all other platforms return empty results
level.Debug(svc.logger).Log("msg", "unsupported platform for host script details", "platform", h.Platform, "host_id", h.ID)
return []*fleet.HostScriptDetail{}, &fleet.PaginationMetadata{}, nil
}
@@ -381,7 +381,7 @@ func (svc *Service) GetHostScriptDetails(ctx context.Context, hostID uint, opt f
// always include metadata for scripts
opt.IncludeMetadata = true
return svc.ds.GetHostScriptDetails(ctx, h.ID, h.TeamID, opt)
return svc.ds.GetHostScriptDetails(ctx, h.ID, h.TeamID, opt, h.Platform)
}
func (svc *Service) BatchSetScripts(ctx context.Context, maybeTmID *uint, maybeTmName *string, payloads []fleet.ScriptPayload, dryRun bool) error {
+18 -3
View File
@@ -243,12 +243,21 @@ WHERE
return scripts, metaData, nil
}
func (ds *Datastore) GetHostScriptDetails(ctx context.Context, hostID uint, teamID *uint, opt fleet.ListOptions) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
func (ds *Datastore) GetHostScriptDetails(ctx context.Context, hostID uint, teamID *uint, opt fleet.ListOptions, hostPlatform string) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
var globalOrTeamID uint
if teamID != nil {
globalOrTeamID = *teamID
}
var extension string
switch hostPlatform {
case "darwin":
extension = `%.sh`
break
case "windows":
extension = `%.ps1`
}
type row struct {
ScriptID uint `db:"script_id"`
Name string `db:"name"`
@@ -295,9 +304,16 @@ FROM
ON s.id = hsr.script_id
WHERE
(hsr.host_id IS NULL OR hsr.host_id = ?)
AND s.global_or_team_id = ?`
AND s.global_or_team_id = ?
`
args := []any{hostID, hostID, hostID, globalOrTeamID}
if len(extension) > 0 {
args = append(args, extension)
sql += `
AND s.name LIKE ?
`
}
stmt, args := appendListOptionsWithCursorToSQL(sql, args, &opt)
var rows []*row
@@ -424,5 +440,4 @@ ON DUPLICATE KEY UPDATE
}
return nil
})
}
+24 -6
View File
@@ -363,7 +363,7 @@ func testListScripts(t *testing.T, ds *Datastore) {
func testGetHostScriptDetails(t *testing.T, ds *Datastore) {
ctx := context.Background()
names := []string{"script-1", "script-2", "script-3", "script-4", "script-5"}
names := []string{"script-1.sh", "script-2.sh", "script-3.sh", "script-4.sh", "script-5.sh"}
for _, r := range append(names[1:], names[0]) {
_, err := ds.NewScript(ctx, &fleet.Script{
Name: r,
@@ -372,9 +372,16 @@ func testGetHostScriptDetails(t *testing.T, ds *Datastore) {
require.NoError(t, err)
}
// create a windows script as well
_, err := ds.NewScript(ctx, &fleet.Script{
Name: "script-6.ps1",
ScriptContents: `Write-Host "Hello, World!"`,
})
require.NoError(t, err)
scripts, _, err := ds.ListScripts(ctx, nil, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, scripts, 5)
require.Len(t, scripts, 6)
insertResults := func(t *testing.T, hostID uint, script *fleet.Script, createdAt time.Time, execID string, exitCode *int64) {
stmt := `
@@ -421,9 +428,9 @@ VALUES
insertResults(t, 42, &fleet.Script{Name: "script-6", ScriptContents: "echo script-6"}, now.Add(-1*time.Minute), "execution-6-1", ptr.Int64(0))
t.Run("results match expected formatting and filtering", func(t *testing.T) {
res, _, err := ds.GetHostScriptDetails(ctx, 42, nil, fleet.ListOptions{})
res, _, err := ds.GetHostScriptDetails(ctx, 42, nil, fleet.ListOptions{}, "")
require.NoError(t, err)
require.Len(t, res, 5)
require.Len(t, res, 6)
for _, r := range res {
switch r.ScriptID {
case scripts[0].ID:
@@ -453,6 +460,9 @@ VALUES
case scripts[4].ID:
require.Equal(t, scripts[4].Name, r.Name)
require.Nil(t, r.LastExecution)
case scripts[5].ID:
require.Equal(t, scripts[5].Name, r.Name)
require.Nil(t, r.LastExecution)
default:
t.Errorf("unexpected script id: %d", r.ScriptID)
}
@@ -460,7 +470,7 @@ VALUES
})
t.Run("empty slice returned if no scripts", func(t *testing.T) {
res, _, err := ds.GetHostScriptDetails(ctx, 42, ptr.Uint(1), fleet.ListOptions{}) // team 1 has no scripts
res, _, err := ds.GetHostScriptDetails(ctx, 42, ptr.Uint(1), fleet.ListOptions{}, "") // team 1 has no scripts
require.NoError(t, err)
require.NotNil(t, res)
require.Len(t, res, 0)
@@ -500,7 +510,7 @@ VALUES
c.opts.IncludeMetadata = true
// custom ordering is not supported, always by name
c.opts.OrderKey = "name"
results, meta, err := ds.GetHostScriptDetails(ctx, 42, nil, c.opts)
results, meta, err := ds.GetHostScriptDetails(ctx, 42, nil, c.opts, "darwin")
require.NoError(t, err)
require.Equal(t, len(c.wantNames), len(results))
@@ -517,6 +527,14 @@ VALUES
})
}
})
t.Run("windows ps1 scripts are supported", func(t *testing.T) {
res, _, err := ds.GetHostScriptDetails(ctx, 42, nil, fleet.ListOptions{}, "windows")
require.NoError(t, err)
require.NotNil(t, res)
require.Len(t, res, 1)
require.Equal(t, "script-6.ps1", res[0].Name)
})
}
func testBatchSetScripts(t *testing.T, ds *Datastore) {
+1 -1
View File
@@ -1159,7 +1159,7 @@ type Datastore interface {
// GetHostScriptDetails returns the list of host script details for saved scripts applicable to
// a given host.
GetHostScriptDetails(ctx context.Context, hostID uint, teamID *uint, opts ListOptions) ([]*HostScriptDetail, *PaginationMetadata, error)
GetHostScriptDetails(ctx context.Context, hostID uint, teamID *uint, opts ListOptions, hostPlatform string) ([]*HostScriptDetail, *PaginationMetadata, error)
// BatchSetScripts sets the scripts for the given team or no team.
BatchSetScripts(ctx context.Context, tmID *uint, scripts []*Script) error
+2 -2
View File
@@ -34,8 +34,8 @@ func (s *Script) Validate() error {
if s.Name == "" {
return errors.New("The file name must not be empty.")
}
if filepath.Ext(s.Name) != ".sh" {
return errors.New("The file should be a .sh file.")
if filepath.Ext(s.Name) != ".sh" && filepath.Ext(s.Name) != ".ps1" {
return errors.New("File type not supported. Only .sh and .ps1 file type is allowed.")
}
if err := ValidateHostScriptContents(s.ScriptContents); err != nil {
+1 -1
View File
@@ -37,7 +37,7 @@ func TestScriptValidate(t *testing.T) {
Name: "test.txt",
ScriptContents: "valid",
},
wantErr: errors.New("The file should be a .sh file."),
wantErr: errors.New("File type not supported. Only .sh and .ps1 file type is allowed."),
},
{
name: "invalid script content",
+3 -3
View File
@@ -738,7 +738,7 @@ type DeleteScriptFunc func(ctx context.Context, id uint) error
type ListScriptsFunc func(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]*fleet.Script, *fleet.PaginationMetadata, error)
type GetHostScriptDetailsFunc func(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error)
type GetHostScriptDetailsFunc func(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions, hostPlatform string) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error)
type BatchSetScriptsFunc func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error
@@ -4352,11 +4352,11 @@ func (s *DataStore) ListScripts(ctx context.Context, teamID *uint, opt fleet.Lis
return s.ListScriptsFunc(ctx, teamID, opt)
}
func (s *DataStore) GetHostScriptDetails(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
func (s *DataStore) GetHostScriptDetails(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions, hostPlatform string) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
s.mu.Lock()
s.GetHostScriptDetailsFuncInvoked = true
s.mu.Unlock()
return s.GetHostScriptDetailsFunc(ctx, hostID, teamID, opts)
return s.GetHostScriptDetailsFunc(ctx, hostID, teamID, opts, hostPlatform)
}
func (s *DataStore) BatchSetScripts(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
+26 -11
View File
@@ -4316,7 +4316,7 @@ func (s *integrationEnterpriseTestSuite) TestSavedScripts() {
"not_sh.txt", []byte(`echo "hello"`), s.token)
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "The file should be a .sh file")
require.Contains(t, errMsg, "Validation Failed: File type not supported. Only .sh and .ps1 file type is allowed.")
// file content is empty
body, headers = generateNewScriptMultipartRequest(t, nil,
@@ -4368,6 +4368,16 @@ func (s *integrationEnterpriseTestSuite) TestSavedScripts() {
tmScriptID := newScriptResp.ScriptID
s.lastActivityMatches("added_script", fmt.Sprintf(`{"script_name": %q, "team_name": %q, "team_id": %d}`, "script1.sh", tm.Name, tm.ID), 0)
// create a windows script
body, headers = generateNewScriptMultipartRequest(t, &tm.ID,
"script2.ps1", []byte(`Write-Host "Hello, World!"`), s.token)
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
err = json.NewDecoder(res.Body).Decode(&newScriptResp)
require.NoError(t, err)
require.NotZero(t, newScriptResp.ScriptID)
require.NotEqual(t, noTeamScriptID, newScriptResp.ScriptID)
s.lastActivityMatches("added_script", fmt.Sprintf(`{"script_name": %q, "team_name": %q, "team_id": %d}`, "script2.ps1", tm.Name, tm.ID), 0)
// get team's script
getScriptResp = getScriptResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/scripts/%d", tmScriptID), nil, http.StatusOK, &getScriptResp)
@@ -4542,17 +4552,23 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptDetails() {
require.NoError(t, err)
tm3, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "test-script-details-team3"})
require.NoError(t, err)
tm4, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "test-script-details-team4-windows"})
require.NoError(t, err)
// create 5 scripts for no team and team 1
for i := 0; i < 5; i++ {
_, err = s.ds.NewScript(ctx, &fleet.Script{Name: fmt.Sprintf("test-script-details-%d", i), ScriptContents: "echo"})
_, err = s.ds.NewScript(ctx, &fleet.Script{Name: fmt.Sprintf("test-script-details-%d.sh", i), ScriptContents: "echo"})
require.NoError(t, err)
_, err = s.ds.NewScript(ctx, &fleet.Script{Name: fmt.Sprintf("test-script-details-%d", i), TeamID: &tm1.ID, ScriptContents: "echo"})
_, err = s.ds.NewScript(ctx, &fleet.Script{Name: fmt.Sprintf("test-script-details-%d.sh", i), TeamID: &tm1.ID, ScriptContents: "echo"})
require.NoError(t, err)
}
// add a windows script to team 4
_, err = s.ds.NewScript(ctx, &fleet.Script{Name: "test-script-details-windows.ps1", TeamID: &tm4.ID, ScriptContents: `Write-Host "Hello, World!"`})
require.NoError(t, err)
// create a single script for team 2
_, err = s.ds.NewScript(ctx, &fleet.Script{Name: "test-script-details-team-2", TeamID: &tm2.ID, ScriptContents: "echo"})
_, err = s.ds.NewScript(ctx, &fleet.Script{Name: "test-script-details-team-2.sh", TeamID: &tm2.ID, ScriptContents: "echo"})
require.NoError(t, err)
// create a host without a team
@@ -4599,7 +4615,7 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptDetails() {
})
require.NoError(t, err)
// create a Windows host (unsupported)
// create a Windows host
host3, err := s.ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
@@ -4610,7 +4626,7 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptDetails() {
UUID: uuid.New().String(),
Hostname: "host3",
Platform: "windows",
TeamID: nil,
TeamID: &tm4.ID,
})
require.NoError(t, err)
@@ -4811,16 +4827,15 @@ VALUES
require.Len(t, resp.Scripts, 0)
})
t.Run("unsupported platform windows", func(t *testing.T) {
require.Nil(t, host3.TeamID)
noTeamScripts, _, err := s.ds.ListScripts(ctx, nil, fleet.ListOptions{})
t.Run("windows", func(t *testing.T) {
team4Scripts, _, err := s.ds.ListScripts(ctx, &tm4.ID, fleet.ListOptions{})
require.NoError(t, err)
require.True(t, len(noTeamScripts) > 0)
require.Len(t, team4Scripts, 1)
var resp getHostScriptDetailsResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/scripts", host3.ID), nil, http.StatusOK, &resp)
require.NotNil(t, resp.Scripts)
require.Len(t, resp.Scripts, 0)
require.Len(t, resp.Scripts, 1)
})
t.Run("unsupported platform linux", func(t *testing.T) {
+4 -4
View File
@@ -796,7 +796,7 @@ func TestHostScriptDetailsAuth(t *testing.T) {
require.Equal(t, uint(42), hostID)
return &fleet.Host{ID: hostID}, nil
}
ds.GetHostScriptDetailsFunc = func(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
ds.GetHostScriptDetailsFunc = func(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions, hostPlatform string) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
require.Nil(t, teamID)
return []*fleet.HostScriptDetail{}, nil, nil
}
@@ -809,7 +809,7 @@ func TestHostScriptDetailsAuth(t *testing.T) {
require.Equal(t, uint(42), hostID)
return &fleet.Host{ID: hostID, TeamID: ptr.Uint(1)}, nil
}
ds.GetHostScriptDetailsFunc = func(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
ds.GetHostScriptDetailsFunc = func(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions, hostPlatform string) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
require.NotNil(t, teamID)
require.Equal(t, uint(1), *teamID)
return []*fleet.HostScriptDetail{}, nil, nil
@@ -844,7 +844,7 @@ func TestHostScriptDetailsSupportedPlatform(t *testing.T) {
return &fleet.AppConfig{}, nil
}
ds.GetHostScriptDetailsFunc = func(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
ds.GetHostScriptDetailsFunc = func(ctx context.Context, hostID uint, teamID *uint, opts fleet.ListOptions, hostPlatform string) ([]*fleet.HostScriptDetail, *fleet.PaginationMetadata, error) {
return []*fleet.HostScriptDetail{{HostID: hostID, ScriptID: 1337, Name: "some-script.sh"}}, nil, nil
}
@@ -857,7 +857,7 @@ func TestHostScriptDetailsSupportedPlatform(t *testing.T) {
{"centos", false},
{"rhel", false},
{"debian", false},
{"windows", false},
{"windows", true},
} {
t.Run(tt.platform, func(t *testing.T) {
ds.GetHostScriptDetailsFuncInvoked = false