Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend build for script automation #22472

Merged
merged 45 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
d6a504c
Add endpoints, DB migration, data store, types for script association…
iansltx Sep 27, 2024
2d783b6
fix json struct tag
Sep 27, 2024
91dd433
Always set script id, so `nil` unsets the feature
Sep 27, 2024
689b694
Switch "remove script" signifier to script_id=0 rather than script_id…
iansltx Sep 28, 2024
32ce9a3
Fix script deletion on policy update
iansltx Sep 28, 2024
5409d39
Start building script execution policy failure hook
iansltx Sep 29, 2024
3e621cf
Finish build of script execution on policy failure
iansltx Sep 29, 2024
e03b8e2
Update schema
iansltx Sep 29, 2024
7dfcb5d
Update data store mock, mention in makefile documentation
iansltx Sep 29, 2024
c01b088
Delete no-team scripts in test client teardown
iansltx Sep 29, 2024
a55649e
Start building out assertion that software installer and script team …
iansltx Sep 29, 2024
aebff19
Implement team match assertion for software installers and scripts to…
iansltx Oct 1, 2024
070d14a
Fix copy-pasted comment
iansltx Oct 1, 2024
8dc338b
Return early and don't log on host scripts disabled
iansltx Oct 1, 2024
660f1ee
Revise log levels
iansltx Oct 1, 2024
8396db9
Fix comment
iansltx Oct 1, 2024
428b709
Merge branch 'main' into 22115-script-automation-be
iansltx Oct 1, 2024
09119d9
Re-timestamp migration
iansltx Oct 1, 2024
29fcdf6
Update schema.sql
iansltx Oct 1, 2024
892baa6
Add GitOps script policy modification implementation, start revising …
iansltx Oct 1, 2024
f66ea84
Fix GitOps script policy test
iansltx Oct 2, 2024
52f6219
Fix missing team ID on installer upload caught by new team match check
iansltx Oct 2, 2024
8364720
Fix existing tests
iansltx Oct 2, 2024
9c50833
Merge branch 'main' into 22115-script-automation-be
iansltx Oct 2, 2024
c6375c3
Lint fix
iansltx Oct 2, 2024
4b4b337
Nil ptr fix
iansltx Oct 2, 2024
ab097e0
Fix HTTP status test
iansltx Oct 2, 2024
19efba2
Fix HTTP status test
iansltx Oct 2, 2024
4032cb7
Merge branch 'main' into 22115-script-automation-be
iansltx Oct 2, 2024
6cec915
Renumber migration, update schema dump
iansltx Oct 2, 2024
5a33e26
Merge branch 'main' into 22115-script-automation-be
iansltx Oct 2, 2024
69796af
Update schema
iansltx Oct 2, 2024
66f19f9
Add test coverage, fix bugs found via automated tests
iansltx Oct 3, 2024
aacbea4
Add GitOps and batch script endpoint test coverage
iansltx Oct 3, 2024
f95a6d4
Use 409 rather than 500 for script delete when a policy depends on it
iansltx Oct 3, 2024
990d57c
Add test for script policy associations preventing deletion
iansltx Oct 3, 2024
2dd9eda
Merge branch 'main' into 22115-script-automation-be
iansltx Oct 3, 2024
c12402e
Add tests for software batch set, fix execution order of policy unset…
iansltx Oct 3, 2024
03f4a1e
Merge branch 'main' into 22115-script-automation-be
iansltx Oct 3, 2024
7c5bf4d
Add migration test
iansltx Oct 3, 2024
ad8be25
Tweak global policy script error message
iansltx Oct 3, 2024
24516be
Revise IsExecutionPendingForHost interface for more clarity
iansltx Oct 3, 2024
ce80b1d
Stat instead of Open to avoid leaking file descriptors, fix integrati…
iansltx Oct 3, 2024
af8f30b
Merge branch 'main' into 22115-script-automation-be
iansltx Oct 3, 2024
1235bdb
Fix script mismatch logging, GitOps response marshalling
iansltx Oct 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ define HELP_TEXT
make generate-dev - Generate and bundle required code in a watch loop
make generate-doc - Generate updated API documentation for activities, osquery flags

make clean - Clean all build artifacts
make dump-test-schema - update schema.sql from current migrations
make generate-mock - update mock data store

make clean - Clean all build artifacts
make clean-assets - Clean assets only

make build - Build the code
Expand Down
2 changes: 1 addition & 1 deletion cmd/fleetctl/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func applyCommand() *cli.Command {
opts.TeamForPolicies = policiesTeamName
}
baseDir := filepath.Dir(flFilename)
_, _, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, nil, opts)
_, _, _, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, nil, opts)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/fleetctl/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2306,8 +2306,8 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
return nil
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
return []fleet.ScriptResponse{}, nil
}
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
Expand Down
50 changes: 38 additions & 12 deletions cmd/fleetctl/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ func TestGitOpsBasicGlobalFree(t *testing.T) {
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
return []fleet.ScriptResponse{}, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
Expand Down Expand Up @@ -211,7 +213,9 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) {
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
return []fleet.ScriptResponse{}, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
Expand Down Expand Up @@ -327,7 +331,9 @@ func TestGitOpsBasicTeam(t *testing.T) {
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
return []fleet.ScriptResponse{}, nil
}
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
) (updates fleet.MDMProfilesUpdates, err error) {
Expand Down Expand Up @@ -516,9 +522,18 @@ func TestGitOpsFullGlobal(t *testing.T) {
)

var appliedScripts []*fleet.Script
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
appliedScripts = scripts
return nil
var scriptResponses []fleet.ScriptResponse
for _, script := range scripts {
scriptResponses = append(scriptResponses, fleet.ScriptResponse{
ID: script.ID,
Name: script.Name,
TeamID: script.TeamID,
})
}

return scriptResponses, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
Expand Down Expand Up @@ -704,9 +719,18 @@ func TestGitOpsFullTeam(t *testing.T) {
}

var appliedScripts []*fleet.Script
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
appliedScripts = scripts
return nil
var scriptResponses []fleet.ScriptResponse
for _, script := range scripts {
scriptResponses = append(scriptResponses, fleet.ScriptResponse{
ID: script.ID,
Name: script.Name,
TeamID: script.TeamID,
})
}

return scriptResponses, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
Expand Down Expand Up @@ -1040,9 +1064,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
assert.Empty(t, winProfiles)
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
assert.Empty(t, scripts)
return nil
return []fleet.ScriptResponse{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
Expand Down Expand Up @@ -1318,9 +1342,9 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
assert.Empty(t, winProfiles)
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
assert.Empty(t, scripts)
return nil
return []fleet.ScriptResponse{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
Expand Down Expand Up @@ -2190,7 +2214,9 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
return []fleet.ScriptResponse{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/fleetctl/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
}
// this only applies standard queries, the base directory is not used,
// so pass in the current working directory.
_, _, err = client.ApplyGroup(c.Context, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{})
_, _, _, err = client.ApplyGroup(c.Context, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{})
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/fleetctl/scripts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ func TestRunScriptCommand(t *testing.T) {
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
ds.IsExecutionPendingForHostFunc = func(ctx context.Context, hid uint, scriptID uint) (bool, error) {
return false, nil
}

generateValidPath := func() string {
Expand Down
53 changes: 52 additions & 1 deletion pkg/spec/gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,18 @@ type Policy struct {

type GitOpsPolicySpec struct {
fleet.PolicySpec
RunScript *PolicyRunScript `json:"run_script"`
InstallSoftware *PolicyInstallSoftware `json:"install_software"`
// InstallSoftwareURL is populated after parsing the software installer yaml
// referenced by InstallSoftware.PackagePath.
InstallSoftwareURL string `json:"-"`
// RunScriptName is populated after confirming the script exists on both the file system
// and in the controls scripts list for the same team
RunScriptName *string `json:"-"`
}

type PolicyRunScript struct {
Path string `json:"path"`
}

type PolicyInstallSoftware struct {
Expand Down Expand Up @@ -168,7 +176,7 @@ func GitOpsFromFile(filePath, baseDir string, appConfig *fleet.EnrichedAppConfig
multiError = parseSoftware(top, result, baseDir, multiError)
}

// Policies can reference software installers, thus we parse them after parseSoftware.
// Policies can reference software installers and scripts, thus we parse them after parseSoftware and parseControls.
multiError = parsePolicies(top, result, baseDir, multiError)

return result, multiError.ErrorOrNil()
Expand Down Expand Up @@ -465,6 +473,10 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy install_software %q: %v", item.Name, err))
continue
}
if err := parsePolicyRunScript(baseDir, result.TeamName, &item, result.Controls.Scripts); err != nil {
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy run_script %q: %v", item.Name, err))
continue
}
result.Policies = append(result.Policies, &item.GitOpsPolicySpec)
} else {
fileBytes, err := os.ReadFile(resolveApplyRelativePath(baseDir, *item.Path))
Expand Down Expand Up @@ -496,6 +508,10 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy install_software %q: %v", pp.Name, err))
continue
}
if err := parsePolicyRunScript(baseDir, result.TeamName, pp, result.Controls.Scripts); err != nil {
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy run_script %q: %v", pp.Name, err))
continue
}
result.Policies = append(result.Policies, &pp.GitOpsPolicySpec)
}
}
Expand Down Expand Up @@ -533,6 +549,41 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
return multiError
}

func parsePolicyRunScript(baseDir string, teamName *string, policy *Policy, scripts []BaseItem) error {
if policy.RunScript == nil {
policy.ScriptID = ptr.Uint(0) // unset the script
return nil
}
if policy.RunScript != nil && policy.RunScript.Path != "" && teamName == nil {
return errors.New("run_script can only be set on team policies")
}

if policy.RunScript.Path == "" {
return errors.New("empty run_script path")
}

_, err := os.Stat(resolveApplyRelativePath(baseDir, policy.RunScript.Path))
if err != nil {
return fmt.Errorf("script file does not exist %q: %v", policy.RunScript.Path, err)
}

scriptOnTeamFound := false
for _, script := range scripts {
if policy.RunScript.Path == *script.Path {
scriptOnTeamFound = true
break
}
}
if !scriptOnTeamFound {
return fmt.Errorf("policy script not found on team: %s", policy.RunScript.Path)
}

scriptName := filepath.Base(policy.RunScript.Path)
policy.RunScriptName = &scriptName

return nil
}

func parsePolicyInstallSoftware(baseDir string, teamName *string, policy *Policy, packages []*fleet.SoftwarePackageSpec) error {
if policy.InstallSoftware == nil {
policy.SoftwareTitleID = ptr.Uint(0) // unset the installer
Expand Down
82 changes: 81 additions & 1 deletion pkg/spec/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func TestValidGitOpsYaml(t *testing.T) {
// Check policies
expectedPoliciesCount := 5
if test.isTeam {
expectedPoliciesCount = 6
expectedPoliciesCount = 8
}
require.Len(t, gitops.Policies, expectedPoliciesCount)
assert.Equal(t, "😊 Failing policy", gitops.Policies[0].Name)
Expand All @@ -255,6 +255,14 @@ func TestValidGitOpsYaml(t *testing.T) {
assert.Equal(t, "Microsoft Teams on macOS installed and up to date", gitops.Policies[5].Name)
assert.NotNil(t, gitops.Policies[5].InstallSoftware)
assert.Equal(t, "./microsoft-teams.pkg.software.yml", gitops.Policies[5].InstallSoftware.PackagePath)

assert.Equal(t, "Script run policy", gitops.Policies[6].Name)
assert.NotNil(t, gitops.Policies[6].RunScript)
assert.Equal(t, "./lib/collect-fleetd-logs.sh", gitops.Policies[6].RunScript.Path)

assert.Equal(t, "🔥 Failing policy with script", gitops.Policies[7].Name)
assert.NotNil(t, gitops.Policies[7].RunScript)
assert.Equal(t, "./lib/collect-fleetd-logs.sh", gitops.Policies[7].RunScript.Path)
}
},
)
Expand Down Expand Up @@ -839,6 +847,20 @@ policies:
assert.ErrorContains(t, err, "install_software can only be set on team policies")
}

func TestGitOpsGlobalPolicyWithRunScript(t *testing.T) {
t.Parallel()
config := getGlobalConfig([]string{"policies"})
config += `
policies:
- name: Some policy
query: SELECT 1;
run_script:
path: ./some_path.sh
`
_, err := gitOpsFromString(t, config)
assert.ErrorContains(t, err, "run_script can only be set on team policies")
}

func TestGitOpsTeamPolicyWithInvalidInstallSoftware(t *testing.T) {
t.Parallel()
config := getTeamConfig([]string{"policies"})
Expand Down Expand Up @@ -939,6 +961,64 @@ software:
assert.ErrorContains(t, err, "failed to unmarshal install_software.package_path file")
}

func TestGitOpsTeamPolicyWithInvalidRunScript(t *testing.T) {
t.Parallel()
config := getTeamConfig([]string{"policies"})
config += `
policies:
- name: Some policy
query: SELECT 1;
run_script:
path: ./some_path.sh
`
_, err := gitOpsFromString(t, config)
assert.ErrorContains(t, err, "script file does not exist")

config = getTeamConfig([]string{"policies"})
config += `
policies:
- name: Some policy
query: SELECT 1;
run_script:
path:
`
_, err = gitOpsFromString(t, config)
assert.ErrorContains(t, err, "empty run_script path")

// Policy references a script not present in the team.
config = getTeamConfig([]string{"policies"})
config += `
policies:
- path: ./script-policy.yml
software:
controls:
scripts:
- path: ./top.policies2.yml

`
path, basePath := createTempFile(t, "", config)
err = file.Copy(
filepath.Join("testdata", "script-policy.yml"),
filepath.Join(basePath, "script-policy.yml"),
0o755,
)
require.NoError(t, err)
err = file.Copy(
filepath.Join("testdata", "lib", "collect-fleetd-logs.sh"),
filepath.Join(basePath, "lib", "collect-fleetd-logs.sh"),
0o755,
)
require.NoError(t, err)
appConfig := fleet.EnrichedAppConfig{}
appConfig.License = &fleet.LicenseInfo{
Tier: fleet.TierPremium,
}
_, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf)
assert.ErrorContains(t, err,
"policy script not found on team",
)
}

func getGlobalConfig(optsToExclude []string) string {
return getBaseConfig(topLevelOptions, optsToExclude)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/spec/testdata/lib/collect-fleetd-logs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# collect fleetd logs
7 changes: 7 additions & 0 deletions pkg/spec/testdata/script-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- name: 🔥 Failing policy with script
platform: linux
description: This policy should always fail.
resolution: There is no resolution for this policy.
query: SELECT 1 FROM osquery_info WHERE start_time < 0;
run_script:
path: ./lib/collect-fleetd-logs.sh
7 changes: 7 additions & 0 deletions pkg/spec/testdata/team_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ policies:
resolution: There is no resolution for this policy.
query: SELECT 1 FROM osquery_info WHERE start_time < 0;
- path: ./team_install_software.policies.yml
- name: Script run policy
platform: linux
description: This should run a script on failure
query: SELECT * from osquery_info;
run_script:
path: ./lib/collect-fleetd-logs.sh
- path: ./script-policy.yml
software:
packages:
- path: ./microsoft-teams.pkg.software.yml
Expand Down
Loading
Loading