Add team failing policies webhook (#4633)

* add config to teams
* update api docs
* update tests
This commit is contained in:
Michal Nicpon 2022-03-21 13:16:47 -06:00 committed by GitHub
parent ecdfd627b6
commit 7b671ac2a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 844 additions and 266 deletions

View file

@ -0,0 +1 @@
* Add ability to configure team failing policies webhook

View file

@ -856,7 +856,7 @@ func cronWebhooks(
// We set the db lock durations to match the intervalReload.
maybeTriggerHostStatus(ctx, ds, logger, identifier, appConfig, intervalReload)
maybeTriggerGlobalFailingPoliciesWebhook(ctx, ds, logger, identifier, appConfig, intervalReload, failingPoliciesSet)
maybeTriggerFailingPoliciesWebhook(ctx, ds, logger, identifier, appConfig, intervalReload, failingPoliciesSet)
level.Debug(logger).Log("loop", "done")
}
@ -883,7 +883,7 @@ func maybeTriggerHostStatus(
}
}
func maybeTriggerGlobalFailingPoliciesWebhook(
func maybeTriggerFailingPoliciesWebhook(
ctx context.Context,
ds fleet.Datastore,
logger kitlog.Logger,
@ -897,7 +897,7 @@ func maybeTriggerGlobalFailingPoliciesWebhook(
return
}
if err := webhooks.TriggerGlobalFailingPoliciesWebhook(
if err := webhooks.TriggerFailingPoliciesWebhook(
ctx, ds, kitlog.With(logger, "webhook", "failing_policies"), appConfig, failingPoliciesSet, time.Now(),
); err != nil {
level.Error(logger).Log("err", "triggering failing policies webhook", "details", err)

View file

@ -170,8 +170,8 @@ spec:
newAgentOpts := json.RawMessage(`{"config":{"something":"else"}}`)
require.Equal(t, "[+] applied 2 teams\n", runAppForTest(t, []string{"apply", "-f", tmpFile.Name()}))
assert.JSONEq(t, string(agentOpts), string(*teamsByName["team2"].AgentOptions))
assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].AgentOptions))
assert.JSONEq(t, string(agentOpts), string(*teamsByName["team2"].Config.AgentOptions))
assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions))
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "AAA"}}, enrolledSecretsCalled[uint(42)])
}

View file

@ -86,8 +86,8 @@ spec:
`
assert.Equal(t, expectedText, runAppForTest(t, []string{"get", "user_roles"}))
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "user_roles", "--yaml"}))
assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "user_roles", "--json"}))
assert.YAMLEq(t, expectedYaml, runAppForTest(t, []string{"get", "user_roles", "--yaml"}))
assert.JSONEq(t, expectedJson, runAppForTest(t, []string{"get", "user_roles", "--json"}))
}
func TestGetTeams(t *testing.T) {
@ -130,12 +130,14 @@ func TestGetTeams(t *testing.T) {
UserCount: 99,
},
{
ID: 43,
CreatedAt: created_at,
Name: "team2",
Description: "team2 description",
UserCount: 87,
AgentOptions: &agentOpts,
ID: 43,
CreatedAt: created_at,
Name: "team2",
Description: "team2 description",
UserCount: 87,
Config: fleet.TeamConfig{
AgentOptions: &agentOpts,
},
},
}, nil
}
@ -153,13 +155,18 @@ apiVersion: v1
kind: team
spec:
team:
agent_options: null
created_at: "1999-03-10T02:45:06.371Z"
description: team1 description
host_count: 0
id: 42
name: team1
user_count: 99
webhook_settings:
failing_policies_webhook:
destination_url: ""
enable_failing_policies_webhook: false
host_batch_size: 0
policy_ids: null
---
apiVersion: v1
kind: team
@ -178,9 +185,15 @@ spec:
id: 43
name: team2
user_count: 87
webhook_settings:
failing_policies_webhook:
destination_url: ""
enable_failing_policies_webhook: false
host_batch_size: 0
policy_ids: null
`
expectedJson := `{"kind":"team","apiVersion":"v1","spec":{"team":{"id":42,"created_at":"1999-03-10T02:45:06.371Z","name":"team1","description":"team1 description","agent_options":null,"user_count":99,"host_count":0}}}
{"kind":"team","apiVersion":"v1","spec":{"team":{"id":43,"created_at":"1999-03-10T02:45:06.371Z","name":"team2","description":"team2 description","agent_options":{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}},"user_count":87,"host_count":0}}}
expectedJson := `{"kind":"team","apiVersion":"v1","spec":{"team":{"id":42,"created_at":"1999-03-10T02:45:06.371Z","name":"team1","description":"team1 description","webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"user_count":99,"host_count":0}}}
{"kind":"team","apiVersion":"v1","spec":{"team":{"id":43,"created_at":"1999-03-10T02:45:06.371Z","name":"team2","description":"team2 description","agent_options":{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}},"webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"user_count":87,"host_count":0}}}
`
if tt.shouldHaveExpiredBanner {
expectedJson = expiredBanner.String() + expectedJson

View file

@ -5673,6 +5673,14 @@ _Available in Fleet Premium_
},
"overrides": {}
}
},
"webhook_settings": {
"failing_policies_webhook": {
"enable_failing_policies_webhook": false,
"destination_url": "",
"policy_ids": null,
"host_batch_size": 0
}
}
}
}
@ -5731,13 +5739,21 @@ _Available in Fleet Premium_
},
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
"select uuid as host_uuid from system_info;",
"select hostname as hostname from system_info;"
]
}
},
"overrides": {}
}
},
"webhook_settings": {
"failing_policies_webhook": {
"enable_failing_policies_webhook": false,
"destination_url": "",
"policy_ids": null,
"host_batch_size": 0
}
}
}
]
@ -5752,12 +5768,18 @@ _Available in Fleet Premium_
#### Parameters
| Name | Type | In | Description |
| -------- | ------ | ---- | --------------------------------------------- |
| id | string | body | **Required.** The desired team's ID. |
| name | string | body | The team's name. |
| host_ids | list | body | A list of hosts that belong to the team. |
| user_ids | list | body | A list of users that are members of the team. |
| Name | Type | In | Description |
| --- | --- | --- | --- |
| id | string | body | **Required.** The desired team's ID. |
| name | string | body | The team's name. |
| host_ids | list | body | A list of hosts that belong to the team. |
| user_ids | list | body | A list of users that are members of the team. |
| webhook_settings | object | body | Webhook settings contains for the team. |
|   failing_policies_webhook | object | body | Failing policies webhook settings. |
|     enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. |
|     destination_url | string | body | The URL to deliver the webhook requests to. |
|     policy_ids | array | body | List of policy IDs to enable failing policies webhook. |
|     host_batch_size | integer | body | Maximum number of hosts to batch on failing policy webhook requests. The default, 0, means no batching (all hosts failing a policy are sent on one request). |
#### Example (add users to a team)
@ -5806,6 +5828,14 @@ _Available in Fleet Premium_
},
"overrides": {}
}
},
"webhook_settings": {
"failing_policies_webhook": {
"enable_failing_policies_webhook": false,
"destination_url": "",
"policy_ids": null,
"host_batch_size": 0
}
}
}
}
@ -5858,6 +5888,14 @@ _Available in Fleet Premium_
},
"overrides": {}
}
},
"webhook_settings": {
"failing_policies_webhook": {
"enable_failing_policies_webhook": false,
"destination_url": "",
"policy_ids": null,
"host_batch_size": 0
}
}
}
}
@ -5932,6 +5970,14 @@ _Available in Fleet Premium_
},
"overrides": {}
}
},
"webhook_settings": {
"failing_policies_webhook": {
"enable_failing_policies_webhook": false,
"destination_url": "",
"policy_ids": null,
"host_batch_size": 0
}
}
}
}

View file

@ -26,7 +26,9 @@ func (svc *Service) NewTeam(ctx context.Context, p fleet.TeamPayload) (*fleet.Te
return nil, err
}
team := &fleet.Team{
AgentOptions: globalConfig.AgentOptions,
Config: fleet.TeamConfig{
AgentOptions: globalConfig.AgentOptions,
},
}
if p.Name == nil {
@ -87,6 +89,9 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
if payload.Description != nil {
team.Description = *payload.Description
}
if payload.WebhookSettings != nil {
team.Config.WebhookSettings = *payload.WebhookSettings
}
return svc.ds.SaveTeam(ctx, team)
}
@ -102,9 +107,9 @@ func (svc *Service) ModifyTeamAgentOptions(ctx context.Context, teamID uint, opt
}
if options != nil {
team.AgentOptions = &options
team.Config.AgentOptions = &options
} else {
team.AgentOptions = nil
team.Config.AgentOptions = nil
}
return svc.ds.SaveTeam(ctx, team)
@ -345,9 +350,11 @@ func (svc Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec)
agentOptions = config.AgentOptions
}
_, err = svc.ds.NewTeam(ctx, &fleet.Team{
Name: spec.Name,
AgentOptions: agentOptions,
Secrets: secrets,
Name: spec.Name,
Config: fleet.TeamConfig{
AgentOptions: agentOptions,
},
Secrets: secrets,
})
if err != nil {
return err
@ -359,7 +366,7 @@ func (svc Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec)
}
team.Name = spec.Name
team.AgentOptions = spec.AgentOptions
team.Config.AgentOptions = spec.AgentOptions
team.Secrets = secrets
_, err = svc.ds.SaveTeam(ctx, team)

View file

@ -21,6 +21,10 @@ func TestConfigRoundtrip(t *testing.T) {
// Newly added config values will automatically be tested in this
// function because of the reflection on the config struct.
// viper tries to load config from the environment too, clear it in case
// any config values are set in the environment.
os.Clearenv()
cmd := &cobra.Command{}
// Leaving this flag unset means that no attempt will be made to load
// the config file
@ -60,7 +64,7 @@ func TestConfigRoundtrip(t *testing.T) {
// Marshal the generated config
buf, err := yaml.Marshal(original)
require.Nil(t, err)
require.NoError(t, err)
t.Log(string(buf))
// Manually load the serialized config

View file

@ -236,7 +236,7 @@ func (ds *cachedMysql) SaveTeam(ctx context.Context, team *fleet.Team) (*fleet.T
key := fmt.Sprintf(teamAgentOptionsKey, team.ID)
ds.c.Set(key, team.AgentOptions, ds.teamAgentOptionsExp)
ds.c.Set(key, team.Config.AgentOptions, ds.teamAgentOptionsExp)
return team, nil
}

View file

@ -276,10 +276,12 @@ func TestCachedTeamAgentOptions(t *testing.T) {
`)
testTeam := &fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: "test",
AgentOptions: &testOptions,
ID: 1,
CreatedAt: time.Now(),
Name: "test",
Config: fleet.TeamConfig{
AgentOptions: &testOptions,
},
}
deleted := false
@ -306,10 +308,12 @@ func TestCachedTeamAgentOptions(t *testing.T) {
{}
`)
updateTeam := &fleet.Team{
ID: testTeam.ID,
CreatedAt: testTeam.CreatedAt,
Name: testTeam.Name,
AgentOptions: &updateOptions,
ID: testTeam.ID,
CreatedAt: testTeam.CreatedAt,
Name: testTeam.Name,
Config: fleet.TeamConfig{
AgentOptions: &updateOptions,
},
}
_, err = ds.SaveTeam(context.Background(), updateTeam)

View file

@ -0,0 +1,28 @@
package tables
import (
"database/sql"
"github.com/pkg/errors"
)
func init() {
MigrationClient.AddMigration(Up_20220309133956, Down_20220309133956)
}
func Up_20220309133956(tx *sql.Tx) error {
if _, err := tx.Exec(`ALTER TABLE teams ADD COLUMN config JSON`); err != nil {
return errors.Wrap(err, "add config column to teams table")
}
if _, err := tx.Exec(`UPDATE teams SET config = JSON_SET('{}', '$.agent_options', agent_options)`); err != nil {
return errors.Wrap(err, "migrate agent_options")
}
if _, err := tx.Exec(`ALTER TABLE teams DROP COLUMN agent_options`); err != nil {
return errors.Wrap(err, "drop agent_options column in teams table")
}
return nil
}
func Down_20220309133956(tx *sql.Tx) error {
return nil
}

View file

@ -0,0 +1,70 @@
package tables
import (
"database/sql/driver"
"encoding/json"
"fmt"
"testing"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/require"
)
type Team20220309133956 struct {
Name string `db:"name"`
Config TeamConfig20220309133956 `db:"config"`
}
type TeamConfig20220309133956 struct {
AgentOptions *json.RawMessage `json:"agent_options" db:"agent_options"`
}
// Scan implements the sql.Scanner interface
func (t *TeamConfig20220309133956) Scan(val interface{}) error {
switch v := val.(type) {
case []byte:
return json.Unmarshal(v, t)
case string:
return json.Unmarshal([]byte(v), t)
case nil: // sql NULL
return nil
default:
return fmt.Errorf("unsupported type: %T", v)
}
}
// Value implements the sql.Valuer interface
func (t TeamConfig20220309133956) Value() (driver.Value, error) {
return json.Marshal(t)
}
func TestUp_20220309133956(t *testing.T) {
db := applyUpToPrev(t)
teams := []Team20220309133956{
{
Name: "test1",
},
{
Name: "test2",
Config: TeamConfig20220309133956{
AgentOptions: ptr.RawMessage(json.RawMessage(`{"config": {"options": {"logger_plugin": "tls", "pack_delimiter": "/", "logger_tls_period": 10, "distributed_plugin": "tls", "disable_distributed": false, "logger_tls_endpoint": "/api/v1/osquery/log", "distributed_interval": 10, "distributed_tls_max_attempts": 3}, "decorators": {"load": ["SELECT uuid AS host_uuid FROM system_info;", "SELECT hostname AS hostname FROM system_info;"]}}, "overrides": {}}`)),
},
},
}
_, err := db.Exec(`
INSERT INTO teams (name, agent_options)
VALUES (?, ?), (?, ?)
`, teams[0].Name, teams[0].Config.AgentOptions, teams[1].Name, teams[1].Config.AgentOptions)
require.NoError(t, err)
applyNext(t, db)
var actual []Team20220309133956
err = db.Select(&actual, `SELECT name, config from teams`)
require.NoError(t, err)
require.JSONEq(t, string(*teams[1].Config.AgentOptions), string(*actual[1].Config.AgentOptions))
require.Equal(t, teams, actual)
}

File diff suppressed because one or more lines are too long

View file

@ -20,16 +20,16 @@ func (ds *Datastore) NewTeam(ctx context.Context, team *fleet.Team) (*fleet.Team
query := `
INSERT INTO teams (
name,
agent_options,
description
) VALUES ( ?, ?, ? )
description,
config
) VALUES (?, ?, ?)
`
result, err := tx.ExecContext(
ctx,
query,
team.Name,
team.AgentOptions,
team.Description,
team.Config,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "insert team")
@ -173,13 +173,15 @@ func saveUsersForTeamDB(ctx context.Context, exec sqlx.ExecerContext, team *flee
func (ds *Datastore) SaveTeam(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
query := `
UPDATE teams SET
name = ?,
agent_options = ?,
description = ?
WHERE id = ?
`
_, err := tx.ExecContext(ctx, query, team.Name, team.AgentOptions, team.Description, team.ID)
UPDATE teams
SET
name = ?,
description = ?,
config = ?
WHERE
id = ?
`
_, err := tx.ExecContext(ctx, query, team.Name, team.Description, team.Config, team.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "saving team")
}
@ -291,7 +293,7 @@ func amountTeamsDB(ctx context.Context, db sqlx.QueryerContext) (int, error) {
// TeamAgentOptions loads the agents options of a team.
func (ds *Datastore) TeamAgentOptions(ctx context.Context, tid uint) (*json.RawMessage, error) {
sql := `SELECT agent_options FROM teams WHERE id = ?`
sql := `SELECT config->"$.agent_options" FROM teams WHERE id = ?`
var agentOptions *json.RawMessage
if err := sqlx.GetContext(ctx, ds.reader, &agentOptions, sql, tid); err != nil {
return nil, ctxerr.Wrap(ctx, err, "select team")

View file

@ -307,8 +307,10 @@ func testTeamsAgentOptions(t *testing.T, ds *Datastore) {
agentOptions := json.RawMessage(`{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}}`)
team2, err := ds.NewTeam(context.Background(), &fleet.Team{
Name: "team2",
AgentOptions: &agentOptions,
Name: "team2",
Config: fleet.TeamConfig{
AgentOptions: &agentOptions,
},
})
require.NoError(t, err)

View file

@ -1,7 +1,9 @@
package fleet
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
)
@ -12,9 +14,10 @@ const (
)
type TeamPayload struct {
Name *string `json:"name"`
Description *string `json:"description"`
Secrets []*EnrollSecret `json:"secrets"`
Name *string `json:"name"`
Description *string `json:"description"`
Secrets []*EnrollSecret `json:"secrets"`
WebhookSettings *TeamWebhookSettings `json:"webhook_settings"`
// Note AgentOptions must be set by a separate endpoint.
}
@ -30,9 +33,8 @@ type Team struct {
// Name is the human friendly name of the team.
Name string `json:"name" db:"name"`
// Description is an optional description for the team.
Description string `json:"description" db:"description"`
// AgentOptions is the options for osquery and Orbit.
AgentOptions *json.RawMessage `json:"agent_options" db:"agent_options"`
Description string `json:"description" db:"description"`
Config TeamConfig `json:"-" db:"config"` // see json.MarshalJSON/UnmarshalJSON implementations
// Derived from JOINs
@ -48,6 +50,101 @@ type Team struct {
Secrets []*EnrollSecret `json:"secrets,omitempty"`
}
func (t Team) MarshalJSON() ([]byte, error) {
// The reason for not embedding TeamConfig above, is that it also implements sql.Scanner/Valuer.
// We do not want it be promoted to the parent struct, because it causes issues when using sqlx for scanning.
// Also need to implement json.Marshaler/Unmarshaler on each type that embeds Team so because it will be promoted
// to the parent struct.
x := struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Description string `json:"description"`
TeamConfig // inline this using struct embedding
UserCount int `json:"user_count"`
Users []TeamUser `json:"users,omitempty"`
HostCount int `json:"host_count"`
Hosts []Host `json:"hosts,omitempty"`
Secrets []*EnrollSecret `json:"secrets,omitempty"`
}{
ID: t.ID,
CreatedAt: t.CreatedAt,
Name: t.Name,
Description: t.Description,
TeamConfig: t.Config,
UserCount: t.UserCount,
Users: t.Users,
HostCount: t.HostCount,
Hosts: t.Hosts,
Secrets: t.Secrets,
}
return json.Marshal(x)
}
func (t *Team) UnmarshalJSON(b []byte) error {
var x struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Description string `json:"description"`
TeamConfig // inline this using struct embedding
UserCount int `json:"user_count"`
Users []TeamUser `json:"users,omitempty"`
HostCount int `json:"host_count"`
Hosts []Host `json:"hosts,omitempty"`
Secrets []*EnrollSecret `json:"secrets,omitempty"`
}
if err := json.Unmarshal(b, &x); err != nil {
return err
}
*t = Team{
ID: x.ID,
CreatedAt: x.CreatedAt,
Name: x.Name,
Description: x.Description,
Config: x.TeamConfig,
UserCount: x.UserCount,
Users: x.Users,
HostCount: x.HostCount,
Hosts: x.Hosts,
Secrets: x.Secrets,
}
return nil
}
type TeamConfig struct {
// AgentOptions is the options for osquery and Orbit.
AgentOptions *json.RawMessage `json:"agent_options,omitempty"`
WebhookSettings TeamWebhookSettings `json:"webhook_settings"`
}
type TeamWebhookSettings struct {
FailingPoliciesWebhook FailingPoliciesWebhookSettings `json:"failing_policies_webhook"`
}
// Scan implements the sql.Scanner interface
func (t *TeamConfig) Scan(val interface{}) error {
switch v := val.(type) {
case []byte:
return json.Unmarshal(v, t)
case string:
return json.Unmarshal([]byte(v), t)
case nil: // sql NULL
return nil
default:
return fmt.Errorf("unsupported type: %T", v)
}
}
// Value implements the sql.Valuer interface
func (t TeamConfig) Value() (driver.Value, error) {
return json.Marshal(t)
}
type TeamSummary struct {
ID uint `json:"id"`
Name string `json:"name"`

View file

@ -1,8 +1,10 @@
package fleet
import (
"encoding/json"
"errors"
"fmt"
"time"
"unicode"
"github.com/fleetdm/fleet/v4/server"
@ -47,6 +49,74 @@ type UserTeam struct {
Role string `json:"role" db:"role"`
}
func (u UserTeam) MarshalJSON() ([]byte, error) {
x := struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Description string `json:"description"`
TeamConfig
UserCount int `json:"user_count"`
Users []TeamUser `json:"users,omitempty"`
HostCount int `json:"host_count"`
Hosts []Host `json:"hosts,omitempty"`
Secrets []*EnrollSecret `json:"secrets,omitempty"`
Role string `json:"role"`
}{
ID: u.ID,
CreatedAt: u.CreatedAt,
Name: u.Name,
Description: u.Description,
TeamConfig: u.Config,
UserCount: u.UserCount,
Users: u.Users,
HostCount: u.HostCount,
Hosts: u.Hosts,
Secrets: u.Secrets,
Role: u.Role,
}
return json.Marshal(x)
}
func (u *UserTeam) UnmarshalJSON(b []byte) error {
var x struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Description string `json:"description"`
TeamConfig
UserCount int `json:"user_count"`
Users []TeamUser `json:"users,omitempty"`
HostCount int `json:"host_count"`
Hosts []Host `json:"hosts,omitempty"`
Secrets []*EnrollSecret `json:"secrets,omitempty"`
Role string `json:"role"`
}
if err := json.Unmarshal(b, &x); err != nil {
return err
}
*u = UserTeam{
Team: Team{
ID: x.ID,
CreatedAt: x.CreatedAt,
Name: x.Name,
Description: x.Description,
Config: x.TeamConfig,
UserCount: x.UserCount,
Users: x.Users,
HostCount: x.HostCount,
Hosts: x.Hosts,
Secrets: x.Secrets,
},
Role: x.Role,
}
return nil
}
// UserListOptions is additional options that can be set for listing users.
type UserListOptions struct {
ListOptions

View file

@ -59,7 +59,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
require.NoError(t, err)
assert.Len(t, team.Secrets, 0)
require.JSONEq(t, string(agentOpts), string(*team.AgentOptions))
require.JSONEq(t, string(agentOpts), string(*team.Config.AgentOptions))
// creates a team with default agent options
user, err := s.ds.UserByEmail(context.Background(), "admin1@example.com")
@ -81,8 +81,8 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
defaultOpts := `{"config": {"options": {"logger_plugin": "tls", "pack_delimiter": "/", "logger_tls_period": 10, "distributed_plugin": "tls", "disable_distributed": false, "logger_tls_endpoint": "/api/v1/osquery/log", "distributed_interval": 10, "distributed_tls_max_attempts": 3}, "decorators": {"load": ["SELECT uuid AS host_uuid FROM system_info;", "SELECT hostname AS hostname FROM system_info;"]}}, "overrides": {}}`
assert.Len(t, team.Secrets, 0)
require.NotNil(t, team.AgentOptions)
require.JSONEq(t, defaultOpts, string(*team.AgentOptions))
require.NotNil(t, team.Config.AgentOptions)
require.JSONEq(t, defaultOpts, string(*team.Config.AgentOptions))
// updates secrets
teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "team2", Secrets: []fleet.EnrollSecret{{Secret: "ABC"}}}}}
@ -436,7 +436,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() {
opts := map[string]string{"x": "y"}
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/agent_options", tm1ID), opts, http.StatusOK, &tmResp)
var m map[string]string
require.NoError(t, json.Unmarshal(*tmResp.Team.AgentOptions, &m))
require.NoError(t, json.Unmarshal(*tmResp.Team.Config.AgentOptions, &m))
assert.Equal(t, opts, m)
// modify team agent options - unknown team

View file

@ -837,9 +837,27 @@ func (svc *Service) SubmitDistributedQueryResults(
}
if len(policyResults) > 0 {
// filter policy results for webhooks
var policyIDs []uint
if ac.WebhookSettings.FailingPoliciesWebhook.Enable {
incomingResults := filterPolicyResults(policyResults, ac.WebhookSettings.FailingPoliciesWebhook.PolicyIDs)
if failingPolicies, passingPolicies, err := svc.ds.FlippingPoliciesForHost(ctx, host.ID, incomingResults); err != nil {
policyIDs = append(policyIDs, ac.WebhookSettings.FailingPoliciesWebhook.PolicyIDs...)
}
if host.TeamID != nil {
team, err := svc.ds.Team(ctx, *host.TeamID)
if err != nil {
logging.WithErr(ctx, err)
} else {
if team.Config.WebhookSettings.FailingPoliciesWebhook.Enable {
policyIDs = append(policyIDs, team.Config.WebhookSettings.FailingPoliciesWebhook.PolicyIDs...)
}
}
}
filteredResults := filterPolicyResults(policyResults, policyIDs)
if len(filteredResults) > 0 {
if failingPolicies, passingPolicies, err := svc.ds.FlippingPoliciesForHost(ctx, host.ID, filteredResults); err != nil {
logging.WithErr(ctx, err)
} else {
// Register the flipped policies on a goroutine to not block the hosts on redis requests.

View file

@ -2,6 +2,7 @@ package service
import (
"context"
"encoding/json"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
@ -41,6 +42,78 @@ type teamSearchResult struct {
Count int `json:"count"`
}
func (t teamSearchResult) MarshalJSON() ([]byte, error) {
x := struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Description string `json:"description"`
fleet.TeamConfig
UserCount int `json:"user_count"`
Users []fleet.TeamUser `json:"users,omitempty"`
HostCount int `json:"host_count"`
Hosts []fleet.Host `json:"hosts,omitempty"`
Secrets []*fleet.EnrollSecret `json:"secrets,omitempty"`
DisplayText string `json:"display_text"`
Count int `json:"count"`
}{
ID: t.ID,
CreatedAt: t.CreatedAt,
Name: t.Name,
Description: t.Description,
TeamConfig: t.Config,
UserCount: t.UserCount,
Users: t.Users,
HostCount: t.HostCount,
Hosts: t.Hosts,
Secrets: t.Secrets,
DisplayText: t.DisplayText,
Count: t.Count,
}
return json.Marshal(x)
}
func (t *teamSearchResult) UnmarshalJSON(b []byte) error {
var x struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Description string `json:"description"`
fleet.TeamConfig
UserCount int `json:"user_count"`
Users []fleet.TeamUser `json:"users,omitempty"`
HostCount int `json:"host_count"`
Hosts []fleet.Host `json:"hosts,omitempty"`
Secrets []*fleet.EnrollSecret `json:"secrets,omitempty"`
DisplayText string `json:"display_text"`
Count int `json:"count"`
}
if err := json.Unmarshal(b, &x); err != nil {
return err
}
*t = teamSearchResult{
Team: &fleet.Team{
ID: x.ID,
CreatedAt: x.CreatedAt,
Name: x.Name,
Description: x.Description,
Config: x.TeamConfig,
UserCount: x.UserCount,
Users: x.Users,
HostCount: x.HostCount,
Hosts: x.Hosts,
Secrets: x.Secrets,
},
DisplayText: x.DisplayText,
Count: x.Count,
}
return nil
}
type targetsData struct {
Hosts []hostSearchResult `json:"hosts"`
Labels []labelSearchResult `json:"labels"`

View file

@ -667,12 +667,12 @@ func TestAuthenticatedUser(t *testing.T) {
createTestUsers(t, ds)
svc := newTestService(t, ds, nil, nil)
admin1, err := ds.UserByEmail(context.Background(), "admin1@example.com")
assert.Nil(t, err)
require.NoError(t, err)
admin1Session, err := ds.NewSession(context.Background(), &fleet.Session{
UserID: admin1.ID,
Key: "admin1",
})
assert.Nil(t, err)
require.NoError(t, err)
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin1, Session: admin1Session})

View file

@ -17,8 +17,8 @@ import (
"github.com/go-kit/kit/log/level"
)
// TriggerGlobalFailingPoliciesWebhook performs the webhook requests for failing policies.
func TriggerGlobalFailingPoliciesWebhook(
// TriggerFailingPoliciesWebhook performs the webhook requests for failing policies.
func TriggerFailingPoliciesWebhook(
ctx context.Context,
ds fleet.Datastore,
logger kitlog.Logger,
@ -26,92 +26,203 @@ func TriggerGlobalFailingPoliciesWebhook(
failingPoliciesSet fleet.FailingPolicySet,
now time.Time,
) error {
if !appConfig.WebhookSettings.FailingPoliciesWebhook.Enable {
return nil
}
level.Debug(logger).Log("enabled", "true")
serverURL, err := url.Parse(appConfig.ServerSettings.ServerURL)
if err != nil {
return ctxerr.Wrap(ctx, err, "invalid server url")
}
globalPoliciesURL := appConfig.WebhookSettings.FailingPoliciesWebhook.DestinationURL
if globalPoliciesURL == "" {
level.Info(logger).Log("msg", "empty global destination_url")
return nil
}
policies, err := filterPolicies(ctx, ds,
appConfig.WebhookSettings.FailingPoliciesWebhook.PolicyIDs,
failingPoliciesSet,
logger,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "filtering policies")
}
for _, policy := range policies {
if err := sendFailingPoliciesBatchedPOSTs(ctx, policy, failingPoliciesSet, postData{
serverURL: serverURL,
now: now,
webhookURL: globalPoliciesURL,
}, appConfig.WebhookSettings.FailingPoliciesWebhook.HostBatchSize, logger); err != nil {
return ctxerr.Wrapf(ctx, err, "sending POSTs for policy set %d", policy.ID)
globalSettings := appConfig.WebhookSettings.FailingPoliciesWebhook
var globalPolicyIDs map[uint]struct{}
var globalWebhookURL *url.URL
if globalSettings.Enable {
globalPolicyIDs = make(map[uint]struct{}, len(globalSettings.PolicyIDs))
for _, policyID := range globalSettings.PolicyIDs {
globalPolicyIDs[policyID] = struct{}{}
}
globalWebhookURL, err = url.Parse(globalSettings.DestinationURL)
if err != nil {
return ctxerr.Wrapf(ctx, err, "parse global webhook url: %s", globalSettings.DestinationURL)
}
}
return nil
}
type postData struct {
serverURL *url.URL
now time.Time
webhookURL string
// team caches
teamSettings := make(map[uint]fleet.FailingPoliciesWebhookSettings)
teamPolicyIDs := make(map[uint]map[uint]struct{})
teamWebhookURLs := make(map[uint]*url.URL)
getTeam := func(teamID uint) error {
settings, ok := teamSettings[teamID]
if ok {
return nil
}
team, err := ds.Team(ctx, teamID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "get team: %d", teamID)
}
settings = team.Config.WebhookSettings.FailingPoliciesWebhook
teamSettings[teamID] = settings
if settings.Enable {
policyIDs := make(map[uint]struct{}, len(settings.PolicyIDs))
for _, policyID := range settings.PolicyIDs {
policyIDs[policyID] = struct{}{}
}
teamPolicyIDs[teamID] = policyIDs
webhookURL, err := url.Parse(settings.DestinationURL)
if err != nil {
return ctxerr.Wrapf(ctx, err, "parse webhook url: %s", settings.DestinationURL)
}
teamWebhookURLs[teamID] = webhookURL
}
return nil
}
policySets, err := failingPoliciesSet.ListSets()
if err != nil {
return ctxerr.Wrap(ctx, err, "list policies set")
}
for _, policyID := range policySets {
policy, err := ds.Policy(ctx, policyID)
switch {
case errors.Is(err, sql.ErrNoRows):
level.Debug(logger).Log("msg", "skipping failing policy, deleted", "policyID", policyID)
if err := failingPoliciesSet.RemoveSet(policy.ID); err != nil {
level.Error(logger).Log("msg", "failed to remove policy from set", "policyID", policyID, "err", err)
}
continue
case err != nil:
return ctxerr.Wrapf(ctx, err, "get policy: %d", policyID)
default:
// Ok
}
if policy.TeamID != nil {
// team policy
err := getTeam(*policy.TeamID)
switch {
case errors.Is(err, sql.ErrNoRows):
// shouldn't happen, unless the team was deleted after the policy was retrieved above
level.Debug(logger).Log("msg", "team does not exist", "teamID", *policy.TeamID)
continue
case err != nil:
level.Error(logger).Log("msg", "failed to get team", "teamID", *policy.TeamID, "err", err)
continue
}
settings := teamSettings[*policy.TeamID]
if !settings.Enable {
continue
}
_, ok := teamPolicyIDs[*policy.TeamID][policy.ID]
if !ok {
level.Debug(logger).Log("msg", "skipping failing policy, not found in team policy IDs", "policyID", policyID)
if err := failingPoliciesSet.RemoveSet(policy.ID); err != nil {
level.Error(logger).Log("msg", "failed to remove policy from set", "policyID", policyID, "err", err)
}
continue
}
webhookURL := teamWebhookURLs[*policy.TeamID]
err = sendFailingPoliciesBatchedPOSTs(
ctx,
policy,
failingPoliciesSet,
settings.HostBatchSize,
serverURL,
webhookURL,
now,
logger,
)
if err != nil {
level.Error(logger).Log("msg", "failed to send failing policies webhook requests", "policyID", policy.ID, "err", err)
}
continue
}
// global policy
_, ok := globalPolicyIDs[policy.ID]
if !ok {
level.Debug(logger).Log("msg", "skipping failing policy, not found in global policy IDs", "policyID", policyID)
if err := failingPoliciesSet.RemoveSet(policy.ID); err != nil {
level.Error(logger).Log("msg", "failed to remove policy from set", "policyID", policyID, "err", err)
}
continue
}
err = sendFailingPoliciesBatchedPOSTs(
ctx,
policy,
failingPoliciesSet,
globalSettings.HostBatchSize,
serverURL,
globalWebhookURL,
now,
logger,
)
if err != nil {
level.Error(logger).Log("msg", "failed to send failing policies webhook requests", "policyID", policy.ID, "err", err)
}
}
return nil
}
func sendFailingPoliciesBatchedPOSTs(
ctx context.Context,
policy *fleet.Policy,
failingPoliciesSet fleet.FailingPolicySet,
postData postData,
hostBatchSize int,
serverURL *url.URL,
webhookURL *url.URL,
now time.Time,
logger kitlog.Logger,
) error {
hosts, err := failingPoliciesSet.ListHosts(policy.ID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "listing hosts for global failing policies set %d", policy.ID)
return ctxerr.Wrapf(ctx, err, "listing hosts for failing policies set %d", policy.ID)
}
if len(hosts) == 0 {
level.Debug(logger).Log("id", policy.ID, "msg", "no hosts")
level.Debug(logger).Log("msg", "no hosts", "policyID", policy.ID)
return nil
}
if hostBatchSize == 0 {
hostBatchSize = len(hosts)
}
sort.Slice(hosts, func(i, j int) bool {
return hosts[i].ID < hosts[j].ID
})
for len(hosts) > 0 {
j := hostBatchSize
if l := len(hosts); j > l {
j = l
if hostBatchSize == 0 {
hostBatchSize = len(hosts)
}
for i := 0; i < len(hosts); i += hostBatchSize {
end := i + hostBatchSize
if end > len(hosts) {
end = len(hosts)
}
batch := hosts[:j]
batch := hosts[i:end]
failingHosts := make([]FailingHost, len(batch))
for i := range batch {
failingHosts[i] = makeFailingHost(batch[i], *postData.serverURL)
for i, host := range batch {
failingHosts[i] = makeFailingHost(host, serverURL)
}
payload := FailingPoliciesPayload{
Timestamp: postData.now,
Timestamp: now,
Policy: policy,
FailingHosts: failingHosts[:j],
FailingHosts: failingHosts,
}
level.Debug(logger).Log("payload", payload, "url", postData.webhookURL, "batch", len(batch))
if err := server.PostJSONWithTimeout(ctx, postData.webhookURL, &payload); err != nil {
return ctxerr.Wrapf(ctx, err, "posting to '%s'", postData.webhookURL)
level.Debug(logger).Log("payload", payload, "url", webhookURL.String(), "batch", len(batch))
if err := server.PostJSONWithTimeout(ctx, webhookURL.String(), &payload); err != nil {
return ctxerr.Wrapf(ctx, err, "posting to %q", webhookURL)
}
if err := failingPoliciesSet.RemoveHosts(policy.ID, batch); err != nil {
return ctxerr.Wrapf(ctx, err, "removing hosts %+v from failing policies set %d", batch, policy.ID)
}
hosts = hosts[j:]
}
return nil
}
@ -128,59 +239,12 @@ type FailingHost struct {
URL string `json:"url"`
}
func makeFailingHost(host fleet.PolicySetHost, serverURL url.URL) FailingHost {
serverURL.Path = path.Join(serverURL.Path, "hosts", strconv.Itoa(int(host.ID)))
func makeFailingHost(host fleet.PolicySetHost, serverURL *url.URL) FailingHost {
u := *serverURL
u.Path = path.Join(serverURL.Path, "hosts", strconv.FormatUint(uint64(host.ID), 10))
return FailingHost{
ID: host.ID,
Hostname: host.Hostname,
URL: serverURL.String(),
URL: u.String(),
}
}
// filterPolicies fetches the policies from the policy set and filters out those
// that are not configured for webhook anymore or are deleted.
//
// The filtered out policies are removed from the set.
func filterPolicies(
ctx context.Context,
ds fleet.Datastore,
configuredPolicyIDs []uint,
failingPoliciesSet fleet.FailingPolicySet,
logger kitlog.Logger,
) ([]*fleet.Policy, error) {
configuredPolicyIDsSet := make(map[uint]struct{})
for _, policyID := range configuredPolicyIDs {
configuredPolicyIDsSet[policyID] = struct{}{}
}
policySets, err := failingPoliciesSet.ListSets()
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing global policies set")
}
var policies []*fleet.Policy
var gcSet []uint
for _, policyID := range policySets {
if _, ok := configuredPolicyIDsSet[policyID]; !ok {
level.Debug(logger).Log("msg", "skipping policy from set, not in config", "id", policyID)
gcSet = append(gcSet, policyID)
continue
}
switch policy, err := ds.Policy(ctx, policyID); {
case err == nil:
policies = append(policies, policy)
case errors.Is(err, sql.ErrNoRows):
level.Debug(logger).Log("msg", "skipping policy from set, deleted", "id", policyID)
gcSet = append(gcSet, policyID)
default:
return nil, ctxerr.Wrapf(ctx, err, "failing to load global failing policies set %d", policyID)
}
}
// Remove the policies that are present in the set but:
// - are not present in the config (user disabled automation for them), or,
// - do not exist anymore (user deleted the policy).
for _, policyID := range gcSet {
if err := failingPoliciesSet.RemoveSet(policyID); err != nil {
return nil, ctxerr.Wrapf(ctx, err, "removing global policy %d from policy set", policyID)
}
}
return policies, nil
}

View file

@ -9,7 +9,6 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"sort"
"testing"
"time"
@ -86,7 +85,7 @@ func TestTriggerFailingPoliciesWebhookBasic(t *testing.T) {
require.NoError(t, err)
mockClock := time.Now()
err = TriggerGlobalFailingPoliciesWebhook(context.Background(), ds, kitlog.NewNopLogger(), ac, failingPolicySet, mockClock)
err = TriggerFailingPoliciesWebhook(context.Background(), ds, kitlog.NewNopLogger(), ac, failingPolicySet, mockClock)
require.NoError(t, err)
timestamp, err := mockClock.MarshalJSON()
require.NoError(t, err)
@ -130,11 +129,167 @@ func TestTriggerFailingPoliciesWebhookBasic(t *testing.T) {
requestBody = ""
err = TriggerGlobalFailingPoliciesWebhook(context.Background(), ds, kitlog.NewNopLogger(), ac, failingPolicySet, mockClock)
err = TriggerFailingPoliciesWebhook(context.Background(), ds, kitlog.NewNopLogger(), ac, failingPolicySet, mockClock)
require.NoError(t, err)
assert.Empty(t, requestBody)
}
func TestTriggerFailingPoliciesWebhookTeam(t *testing.T) {
// webhook server
webhookBody := ""
webhookCalled := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
webhookCalled = true
requestBodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
webhookBody = string(requestBodyBytes)
}))
t.Cleanup(func() {
ts.Close()
})
ds := new(mock.Store)
teamID := uint(1)
policiesByID := map[uint]*fleet.Policy{
1: {
PolicyData: fleet.PolicyData{
ID: 1,
Name: "policy1",
Query: "select 1",
Description: "policy1 description",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
TeamID: &teamID,
Resolution: ptr.String("policy1 resolution"),
Platform: "darwin",
},
},
2: {
PolicyData: fleet.PolicyData{
ID: 2,
Name: "policy2",
Query: "select 2",
Description: "policy2 description",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
TeamID: &teamID,
Resolution: ptr.String("policy2 resolution"),
Platform: "darwin",
},
},
3: {
PolicyData: fleet.PolicyData{
ID: 2,
Name: "policy3",
Query: "select 3",
Description: "policy3 description",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
TeamID: nil, // global policy
Resolution: ptr.String("policy3 resolution"),
Platform: "darwin",
},
},
}
ds.PolicyFunc = func(ctx context.Context, id uint) (*fleet.Policy, error) {
policy, ok := policiesByID[id]
if !ok {
return nil, ctxerr.Wrap(ctx, sql.ErrNoRows)
}
return policy, nil
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
if tid == teamID {
return &fleet.Team{
ID: teamID,
Config: fleet.TeamConfig{
WebhookSettings: fleet.TeamWebhookSettings{
FailingPoliciesWebhook: fleet.FailingPoliciesWebhookSettings{
Enable: true,
DestinationURL: ts.URL,
PolicyIDs: []uint{1},
},
},
},
}, nil
}
return nil, ctxerr.Wrap(ctx, sql.ErrNoRows)
}
ac := &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "https://fleet.example.com",
},
}
failingPolicySet := service.NewMemFailingPolicySet()
err := failingPolicySet.AddHost(1, fleet.PolicySetHost{
ID: 1,
Hostname: "host1",
})
require.NoError(t, err)
err = failingPolicySet.AddHost(2, fleet.PolicySetHost{
ID: 2,
Hostname: "host2",
})
require.NoError(t, err)
now := time.Now()
err = TriggerFailingPoliciesWebhook(context.Background(), ds, kitlog.NewNopLogger(), ac, failingPolicySet, now)
require.NoError(t, err)
timestamp, err := now.MarshalJSON()
require.NoError(t, err)
// Request body as defined in #2756.
require.True(t, webhookCalled, "webhook was not called")
require.JSONEq(
t, fmt.Sprintf(`{
"timestamp": %s,
"policy": {
"id": 1,
"name": "policy1",
"query": "select 1",
"description": "policy1 description",
"author_id": 1,
"author_name": "Alice",
"author_email": "alice@example.com",
"team_id": 1,
"resolution": "policy1 resolution",
"platform": "darwin",
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "0001-01-01T00:00:00Z",
"passing_host_count": 0,
"failing_host_count": 0
},
"hosts": [
{
"id": 1,
"hostname": "host1",
"url": "https://fleet.example.com/hosts/1"
}
]
}`, timestamp), webhookBody)
hosts, err := failingPolicySet.ListHosts(1)
require.NoError(t, err)
assert.Empty(t, hosts)
webhookBody = ""
err = TriggerFailingPoliciesWebhook(context.Background(), ds, kitlog.NewNopLogger(), ac, failingPolicySet, now)
require.NoError(t, err)
assert.Empty(t, webhookBody)
}
func TestSendBatchedPOSTs(t *testing.T) {
allHosts := []uint{}
requestCount := 0
@ -244,13 +399,18 @@ func TestSendBatchedPOSTs(t *testing.T) {
err := failingPolicySet.AddHost(p.ID, host)
require.NoError(t, err)
}
err := sendFailingPoliciesBatchedPOSTs(context.Background(),
p, failingPolicySet, postData{
serverURL: serverURL,
now: now,
webhookURL: ts.URL,
},
webhookURL, err := url.Parse(ts.URL)
require.NoError(t, err)
err = sendFailingPoliciesBatchedPOSTs(
context.Background(),
p,
failingPolicySet,
tc.batchSize,
serverURL,
webhookURL,
now,
kitlog.NewNopLogger(),
)
require.NoError(t, err)
@ -265,84 +425,3 @@ func TestSendBatchedPOSTs(t *testing.T) {
})
}
}
func TestFilterPolicies(t *testing.T) {
ds := new(mock.Store)
for _, tc := range []struct {
name string
set []uint
cfg []uint
nonExisting []uint
expIDs []uint
}{
{
name: "one-non-configured",
set: []uint{1, 2, 6},
cfg: []uint{1, 2, 3},
nonExisting: []uint{},
expIDs: []uint{1, 2},
},
{
name: "none-configured",
set: []uint{1, 2, 6},
cfg: []uint{},
nonExisting: []uint{},
expIDs: []uint{},
},
{
name: "one-non-existing-and-one-non-configured",
set: []uint{1, 2, 6},
cfg: []uint{1, 2},
nonExisting: []uint{1},
expIDs: []uint{2},
},
{
name: "empty-set",
set: []uint{},
cfg: []uint{1, 2},
nonExisting: []uint{1},
expIDs: []uint{},
},
} {
t.Run(tc.name, func(t *testing.T) {
failingPoliciesSet := service.NewMemFailingPolicySet()
for _, policyID := range tc.set {
err := failingPoliciesSet.AddHost(policyID, fleet.PolicySetHost{ID: 1})
require.NoError(t, err)
}
ds.PolicyFunc = func(ctx context.Context, id uint) (*fleet.Policy, error) {
for _, nonID := range tc.nonExisting {
if nonID == id {
return nil, sql.ErrNoRows
}
}
return &fleet.Policy{
PolicyData: fleet.PolicyData{
ID: id,
},
}, nil
}
policies, err := filterPolicies(
context.Background(),
ds,
tc.cfg,
failingPoliciesSet,
kitlog.NewNopLogger(),
)
require.NoError(t, err)
require.Len(t, policies, len(tc.expIDs))
sets, err := failingPoliciesSet.ListSets()
sort.Slice(sets, func(i, j int) bool {
return sets[i] < sets[j]
})
sort.Slice(policies, func(i, j int) bool {
return policies[i].ID < policies[j].ID
})
require.NoError(t, err)
for i := range policies {
require.Equal(t, tc.expIDs[i], policies[i].ID)
require.Equal(t, sets[i], policies[i].ID)
}
})
}
}