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

Clear policy results and stats when setting or changing an installer #22053

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 16 additions & 6 deletions server/datastore/mysql/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -753,9 +753,10 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs

// Get the query and platforms of the current policies so that we can check if query or platform changed later, if needed
type policyLite struct {
Name string `db:"name"`
Query string `db:"query"`
Platforms string `db:"platforms"`
Name string `db:"name"`
Query string `db:"query"`
Platforms string `db:"platforms"`
SoftwareInstallerID *uint `db:"software_installer_id"`
}
teamIDToPoliciesByName := make(map[*uint]map[string]policyLite, len(teamIDToPolicies))
for teamID, teamPolicySpecs := range teamIDToPolicies {
Expand All @@ -769,10 +770,10 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
var args []interface{}
var err error
if teamID == nil {
query, args, err = sqlx.In("SELECT name, query, platforms FROM policies WHERE team_id IS NULL AND name IN (?)", policyNames)
query, args, err = sqlx.In("SELECT name, query, platforms, software_installer_id FROM policies WHERE team_id IS NULL AND name IN (?)", policyNames)
} else {
query, args, err = sqlx.In(
"SELECT name, query, platforms FROM policies WHERE team_id = ? AND name IN (?)", *teamID, policyNames,
"SELECT name, query, platforms, software_installer_id FROM policies WHERE team_id = ? AND name IN (?)", *teamID, policyNames,
)
}
if err != nil {
Expand Down Expand Up @@ -838,12 +839,21 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
shouldRemoveAllPolicyMemberships bool
removePolicyStats bool
)
// Figure out if the query or platform changed
// Figure out if the query, platform or software installer changed.
var softwareInstallerID *uint
if spec.SoftwareTitleID != nil {
softwareInstallerID = softwareInstallerIDs[teamID][*spec.SoftwareTitleID]
}
if prev, ok := teamIDToPoliciesByName[teamID][spec.Name]; ok {
switch {
case prev.Query != spec.Query:
shouldRemoveAllPolicyMemberships = true
removePolicyStats = true
case teamID != nil &&
((prev.SoftwareInstallerID == nil && spec.SoftwareTitleID != nil) ||
(prev.SoftwareInstallerID != nil && softwareInstallerID != nil && *prev.SoftwareInstallerID != *softwareInstallerID)):
shouldRemoveAllPolicyMemberships = true
removePolicyStats = true
case prev.Platforms != spec.Platform:
removePolicyStats = true
}
Expand Down
118 changes: 118 additions & 0 deletions server/datastore/mysql/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4061,6 +4061,24 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
require.NoError(t, err)
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
require.NoError(t, err)
newHost := func(name string, teamID *uint, platform string) *fleet.Host {
h, err := ds.NewHost(ctx, &fleet.Host{
OsqueryHostID: ptr.String(uuid.New().String()),
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String(uuid.New().String()),
UUID: uuid.New().String(),
Hostname: name,
TeamID: teamID,
Platform: platform,
})
require.NoError(t, err)
return h
}

host1Team1 := newHost("host1Team1", &team1.ID, "darwin")

installer1ID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
Expand Down Expand Up @@ -4113,6 +4131,24 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
installer3, err := ds.GetSoftwareInstallerMetadataByID(ctx, installer3ID)
require.NoError(t, err)
require.NotNil(t, installer3.TitleID)
// Another installer on team1 to test changing installers.
installer5ID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello5",
PreInstallQuery: "SELECT 5;",
PostInstallScript: "world5",
InstallerFile: bytes.NewReader([]byte("hello5")),
StorageID: "storage5",
Filename: "file5",
Title: "file5",
Version: "1.0",
Source: "programs",
UserID: user1.ID,
TeamID: &team1.ID,
})
require.NoError(t, err)
installer5, err := ds.GetSoftwareInstallerMetadataByID(ctx, installer5ID)
require.NoError(t, err)
require.NotNil(t, installer5.TitleID)

// Installers cannot be assigned to global policies.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
Expand Down Expand Up @@ -4164,6 +4200,7 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, team1Policies, 1)
require.NotNil(t, team1Policies[0].SoftwareInstallerID)
policy1Team1 := team1Policies[0]
require.Equal(t, installer1.InstallerID, *team1Policies[0].SoftwareInstallerID)
team2Policies, _, err := ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
Expand All @@ -4176,6 +4213,14 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
require.NotNil(t, noTeamPolicies[0].SoftwareInstallerID)
require.Equal(t, installer3.InstallerID, *noTeamPolicies[0].SoftwareInstallerID)

// Record policy execution on policy1Team1.
err = ds.RecordPolicyQueryExecutions(ctx, host1Team1, map[uint]*bool{
policy1Team1.ID: ptr.Bool(false),
}, time.Now(), false)
require.NoError(t, err)
err = ds.UpdateHostPolicyCounts(ctx)
require.NoError(t, err)

// Unset software installer from "Team policy 1".
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Expand All @@ -4193,6 +4238,8 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, team1Policies, 1)
require.Nil(t, team1Policies[0].SoftwareInstallerID)
// Should not clear results because we've cleared not changed/set-new installer.
require.Equal(t, uint(1), team1Policies[0].FailingHostCount)

// Set "Team policy 1" to a software installer on team2.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
Expand Down Expand Up @@ -4317,11 +4364,82 @@ func testApplyPolicySpecWithInstallers(t *testing.T, ds *Datastore) {
require.Len(t, team1Policies, 1)
require.NotNil(t, team1Policies[0].SoftwareInstallerID)
require.Equal(t, installer1.InstallerID, *team1Policies[0].SoftwareInstallerID)
// Should clear results because we've are setting an installer.
require.Equal(t, uint(0), team1Policies[0].FailingHostCount)
countBiggerThanZero := true
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q,
&countBiggerThanZero,
`SELECT COUNT(*) > 0 FROM policy_membership WHERE policy_id = ?`,
team1Policies[0].ID,
)
})
require.False(t, countBiggerThanZero)
team2Policies, _, err = ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team2Policies, 1)
require.NotNil(t, team2Policies[0].SoftwareInstallerID)
require.Equal(t, installer4.InstallerID, *team2Policies[0].SoftwareInstallerID)

// Record policy execution on policy1Team1 to test that setting the same installer won't clear results.
err = ds.RecordPolicyQueryExecutions(ctx, host1Team1, map[uint]*bool{
policy1Team1.ID: ptr.Bool(false),
}, time.Now(), false)
require.NoError(t, err)
err = ds.UpdateHostPolicyCounts(ctx)
require.NoError(t, err)
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Name: "Team policy 1",
Query: "SELECT 1;",
Description: "Description 1",
Resolution: "Resolution 1",
Team: "team1",
Platform: "darwin",
SoftwareTitleID: installer1.TitleID,
},
})
require.NoError(t, err)
team1Policies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team1Policies, 1)
require.Equal(t, uint(1), team1Policies[0].FailingHostCount)
countBiggerThanZero = false
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q,
&countBiggerThanZero,
`SELECT COUNT(*) > 0 FROM policy_membership WHERE policy_id = ?`,
team1Policies[0].ID,
)
})
require.True(t, countBiggerThanZero)

// Now change the installer, should clear results.
err = ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Name: "Team policy 1",
Query: "SELECT 1;",
Description: "Description 1",
Resolution: "Resolution 1",
Team: "team1",
Platform: "darwin",
SoftwareTitleID: installer5.TitleID,
},
})
require.NoError(t, err)
team1Policies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, team1Policies, 1)
require.Equal(t, uint(0), team1Policies[0].FailingHostCount)
countBiggerThanZero = true
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q,
&countBiggerThanZero,
`SELECT COUNT(*) > 0 FROM policy_membership WHERE policy_id = ?`,
team1Policies[0].ID,
)
})
require.False(t, countBiggerThanZero)
}

func testTeamPoliciesNoTeam(t *testing.T, ds *Datastore) {
Expand Down
106 changes: 101 additions & 5 deletions server/service/integration_enterprise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13553,6 +13553,35 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
policy1Team1, err = s.ds.Policy(ctx, policy1Team1.ID)
require.NoError(t, err)
require.Nil(t, policy1Team1.SoftwareInstallerID)

host1LastInstall, err := s.ds.GetHostLastInstallData(ctx, host1Team1.ID, dummyInstallerPkgInstallerID)
require.NoError(t, err)
require.Nil(t, host1LastInstall)

// Add some results and stats that should be cleared after setting an installer again.
distributedResp := submitDistributedQueryResultsResponse{}
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host1Team1,
map[uint]*bool{
policy1Team1.ID: ptr.Bool(false),
},
), http.StatusOK, &distributedResp)
err = s.ds.UpdateHostPolicyCounts(ctx)
require.NoError(t, err)
policy1Team1, err = s.ds.Policy(ctx, policy1Team1.ID)
require.NoError(t, err)
require.Equal(t, uint(0), policy1Team1.PassingHostCount)
require.Equal(t, uint(1), policy1Team1.FailingHostCount)
passes := true
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q,
&passes,
`SELECT passes FROM policy_membership WHERE policy_id = ? AND host_id = ?`,
policy1Team1.ID, host1Team1.ID,
)
})
require.False(t, passes)

// Back to associating dummy_installer.pkg to policy1Team1.
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID), modifyTeamPolicyRequest{
Expand All @@ -13564,18 +13593,85 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
require.NoError(t, err)
require.NotNil(t, policy1Team1.SoftwareInstallerID)
require.Equal(t, dummyInstallerPkgInstallerID, *policy1Team1.SoftwareInstallerID)
// Policy stats and membership should be cleared from policy1Team1.
require.Equal(t, uint(0), policy1Team1.PassingHostCount)
require.Equal(t, uint(0), policy1Team1.FailingHostCount)
countBiggerThanZero := true
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q,
&countBiggerThanZero,
`SELECT COUNT(*) > 0 FROM policy_membership WHERE policy_id = ?`,
policy1Team1.ID,
)
})
require.False(t, countBiggerThanZero)

// Associate ruby.deb to policy2Team1.
// Add (again) some results and stats that should be cleared after changing an existing installer.
distributedResp = submitDistributedQueryResultsResponse{}
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host1Team1,
map[uint]*bool{
policy1Team1.ID: ptr.Bool(false),
},
), http.StatusOK, &distributedResp)
err = s.ds.UpdateHostPolicyCounts(ctx)
require.NoError(t, err)
policy1Team1, err = s.ds.Policy(ctx, policy1Team1.ID)
require.NoError(t, err)
require.Equal(t, uint(0), policy1Team1.PassingHostCount)
require.Equal(t, uint(1), policy1Team1.FailingHostCount)
passes = true
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q,
&passes,
`SELECT passes FROM policy_membership WHERE policy_id = ? AND host_id = ?`,
policy1Team1.ID, host1Team1.ID,
)
})
require.False(t, passes)

// Change the installer (temporarily to test that changing an installer will clear results)
// Associate ruby.deb to policy1Team1.
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy2Team1.ID), modifyTeamPolicyRequest{
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
SoftwareTitleID: &rubyDebTitleID,
},
}, http.StatusOK, &mtplr)

host1LastInstall, err := s.ds.GetHostLastInstallData(ctx, host1Team1.ID, dummyInstallerPkgInstallerID)
// After changing the installer, membership and stats should be cleared.
policy1Team1, err = s.ds.Policy(ctx, policy1Team1.ID)
require.NoError(t, err)
require.Nil(t, host1LastInstall)
require.NotNil(t, policy1Team1.SoftwareInstallerID)
require.Equal(t, rubyDebInstallerID, *policy1Team1.SoftwareInstallerID)
// Policy stats and membership should be cleared from policy1Team1.
require.Equal(t, uint(0), policy1Team1.PassingHostCount)
require.Equal(t, uint(0), policy1Team1.FailingHostCount)
countBiggerThanZero = true
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q,
&countBiggerThanZero,
`SELECT COUNT(*) > 0 FROM policy_membership WHERE policy_id = ?`,
policy1Team1.ID,
)
})
require.False(t, countBiggerThanZero)

// Back to (again) associating dummy_installer.pkg to policy1Team1.
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
SoftwareTitleID: &dummyInstallerPkgTitleID,
},
}, http.StatusOK, &mtplr)

// Associate ruby.deb to policy2Team1.
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy2Team1.ID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
SoftwareTitleID: &rubyDebTitleID,
},
}, http.StatusOK, &mtplr)

// We use DoJSONWithoutAuth for distributed/write because we want the requests to not have the
// current user's "Authorization: Bearer <API_TOKEN>" header.
Expand All @@ -13584,7 +13680,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
// Failing policy1Team1 means an install request must be generated.
// Failing policy2Team1 should not trigger a install request because it has a .deb attached to it (does not apply to macOS hosts).
// Failing policy3Team1 should do nothing because it doesn't have any installers associated to it.
distributedResp := submitDistributedQueryResultsResponse{}
distributedResp = submitDistributedQueryResultsResponse{}
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host1Team1,
map[uint]*bool{
Expand Down
7 changes: 7 additions & 0 deletions server/service/team_policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,13 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
if err != nil {
return nil, err
}
// If the associated installer is changed (or it's set and the policy didn't have an associated installer)
// then we clear the results of the policy so that automation can be triggered upon failure
// (automation is currently triggered on the first failure or when it goes from passing to failure).
if softwareInstallerID != nil && (policy.SoftwareInstallerID == nil || *policy.SoftwareInstallerID != *softwareInstallerID) {
removeAllMemberships = true
removeStats = true
}
policy.SoftwareInstallerID = softwareInstallerID
}

Expand Down
Loading