mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Add team failing policies webhook (#4633)
* add config to teams * update api docs * update tests
This commit is contained in:
parent
ecdfd627b6
commit
7b671ac2a3
22 changed files with 844 additions and 266 deletions
1
changes/issue-3267-add-team-webhook
Normal file
1
changes/issue-3267-add-team-webhook
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Add ability to configure team failing policies webhook
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue