mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
3757aace08
#8129 Apart from fixing the issue in #8129, this change also introduces UUIDs to Fleet errors. To be able to match a returned error from the API to a error in the Fleet logs. See https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for more context. Samples with the changes in this PR: ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d '' { "message": "Bad request", "errors": [ { "name": "base", "reason": "Expected JSON Body" } ], "uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0" } ``` ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd' { "message": "Bad request", "errors": [ { "name": "base", "reason": "json decoder error" } ], "uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d" } ``` ``` curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams" { "message": "Authentication required", "errors": [ { "name": "base", "reason": "Authentication required" } ], "uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Authorization header required", "errors": [ { "name": "base", "reason": "Authorization header required" } ], "uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Permission Denied", "uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06" } ``` - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md) - ~[ ] Documented any permissions changes~ - ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)~ - ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.~ - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [X] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
1685 lines
43 KiB
Go
1685 lines
43 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"encoding/json"
|
||
"errors"
|
||
"os"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||
"github.com/fleetdm/fleet/v4/server/mock"
|
||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||
"github.com/fleetdm/fleet/v4/server/service"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
var userRoleSpecList = []*fleet.User{
|
||
{
|
||
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
|
||
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
|
||
UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Now()},
|
||
},
|
||
ID: 42,
|
||
Name: "Test Name admin1@example.com",
|
||
Email: "admin1@example.com",
|
||
GlobalRole: ptr.String(fleet.RoleAdmin),
|
||
},
|
||
{
|
||
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
|
||
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
|
||
UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Now()},
|
||
},
|
||
ID: 23,
|
||
Name: "Test Name2 admin2@example.com",
|
||
Email: "admin2@example.com",
|
||
GlobalRole: nil,
|
||
Teams: []fleet.UserTeam{},
|
||
},
|
||
}
|
||
|
||
func TestApplyUserRoles(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
||
return userRoleSpecList, nil
|
||
}
|
||
|
||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||
if email == "admin1@example.com" {
|
||
return userRoleSpecList[0], nil
|
||
}
|
||
return userRoleSpecList[1], nil
|
||
}
|
||
|
||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||
return &fleet.Team{
|
||
ID: 1,
|
||
CreatedAt: time.Now(),
|
||
Name: "team1",
|
||
}, nil
|
||
}
|
||
|
||
ds.SaveUsersFunc = func(ctx context.Context, users []*fleet.User) error {
|
||
for _, u := range users {
|
||
switch u.Email {
|
||
case "admin1@example.com":
|
||
userRoleList[0] = u
|
||
case "admin2@example.com":
|
||
userRoleList[1] = u
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
tmpFile, err := os.CreateTemp(os.TempDir(), "*.yml")
|
||
require.NoError(t, err)
|
||
defer os.Remove(tmpFile.Name())
|
||
|
||
_, err = tmpFile.WriteString(`
|
||
---
|
||
apiVersion: v1
|
||
kind: user_roles
|
||
spec:
|
||
roles:
|
||
admin1@example.com:
|
||
global_role: admin
|
||
teams: null
|
||
admin2@example.com:
|
||
global_role: null
|
||
teams:
|
||
- role: maintainer
|
||
team: team1
|
||
`)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "[+] applied user roles\n", runAppForTest(t, []string{"apply", "-f", tmpFile.Name()}))
|
||
require.Len(t, userRoleSpecList[1].Teams, 1)
|
||
assert.Equal(t, fleet.RoleMaintainer, userRoleSpecList[1].Teams[0].Role)
|
||
}
|
||
|
||
func TestApplyTeamSpecs(t *testing.T) {
|
||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license})
|
||
|
||
teamsByName := map[string]*fleet.Team{
|
||
"team1": {
|
||
ID: 42,
|
||
Name: "team1",
|
||
Description: "team1 description",
|
||
},
|
||
}
|
||
|
||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||
team, ok := teamsByName[name]
|
||
if !ok {
|
||
return nil, sql.ErrNoRows
|
||
}
|
||
return team, nil
|
||
}
|
||
|
||
i := 1
|
||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||
team.ID = uint(i)
|
||
i++
|
||
teamsByName[team.Name] = team
|
||
return team, nil
|
||
}
|
||
|
||
agentOpts := json.RawMessage(`{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}}`)
|
||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||
return &fleet.AppConfig{AgentOptions: &agentOpts}, nil
|
||
}
|
||
|
||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||
teamsByName[team.Name] = team
|
||
return team, nil
|
||
}
|
||
|
||
enrolledSecretsCalled := make(map[uint][]*fleet.EnrollSecret)
|
||
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
||
enrolledSecretsCalled[*teamID] = secrets
|
||
return nil
|
||
}
|
||
|
||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||
return nil
|
||
}
|
||
|
||
filename := writeTmpYml(t, `
|
||
---
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team2
|
||
---
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
agent_options:
|
||
config:
|
||
views:
|
||
foo: bar
|
||
name: team1
|
||
secrets:
|
||
- secret: AAA
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: 12.3.1
|
||
deadline: 2011-03-01
|
||
`)
|
||
|
||
newAgentOpts := json.RawMessage(`{"config":{"views":{"foo":"bar"}}}`)
|
||
newMDMSettings := fleet.TeamMDM{
|
||
MacOSUpdates: fleet.MacOSUpdates{
|
||
MinimumVersion: "12.3.1",
|
||
Deadline: "2011-03-01",
|
||
},
|
||
}
|
||
require.Equal(t, "[+] applied 2 teams\n", runAppForTest(t, []string{"apply", "-f", filename}))
|
||
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)])
|
||
assert.Equal(t, fleet.TeamMDM{}, teamsByName["team2"].Config.MDM)
|
||
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
|
||
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
|
||
ds.ApplyEnrollSecretsFuncInvoked = false
|
||
|
||
filename = writeTmpYml(t, `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
`)
|
||
|
||
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", filename}))
|
||
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "AAA"}}, enrolledSecretsCalled[uint(42)])
|
||
assert.False(t, ds.ApplyEnrollSecretsFuncInvoked)
|
||
// agent options not provided, so left unchanged
|
||
assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions))
|
||
assert.Equal(t, fleet.TeamMDM{}, teamsByName["team1"].Config.MDM)
|
||
|
||
filename = writeTmpYml(t, `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
agent_options:
|
||
config:
|
||
views:
|
||
foo: qux
|
||
name: team1
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: 10.10.10
|
||
deadline: 1992-03-01
|
||
secrets:
|
||
- secret: BBB
|
||
`)
|
||
|
||
newMDMSettings = fleet.TeamMDM{
|
||
MacOSUpdates: fleet.MacOSUpdates{
|
||
MinimumVersion: "10.10.10",
|
||
Deadline: "1992-03-01",
|
||
},
|
||
}
|
||
newAgentOpts = json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)
|
||
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", filename}))
|
||
assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions))
|
||
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
|
||
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, enrolledSecretsCalled[uint(42)])
|
||
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
|
||
|
||
filename = writeTmpYml(t, `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
agent_options:
|
||
name: team1
|
||
`)
|
||
|
||
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", filename}))
|
||
// agent options provided but empty, clears the value
|
||
assert.Nil(t, teamsByName["team1"].Config.AgentOptions)
|
||
}
|
||
|
||
func writeTmpYml(t *testing.T, contents string) string {
|
||
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||
require.NoError(t, err)
|
||
_, err = tmpFile.WriteString(contents)
|
||
require.NoError(t, err)
|
||
return tmpFile.Name()
|
||
}
|
||
|
||
func TestApplyAppConfig(t *testing.T) {
|
||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license})
|
||
|
||
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
||
return userRoleSpecList, nil
|
||
}
|
||
|
||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||
return nil
|
||
}
|
||
|
||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||
if email == "admin1@example.com" {
|
||
return userRoleSpecList[0], nil
|
||
}
|
||
return userRoleSpecList[1], nil
|
||
}
|
||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||
return &fleet.Team{ID: 123}, nil
|
||
}
|
||
|
||
defaultAgentOpts := json.RawMessage(`{"config":{"foo":"bar"}}`)
|
||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||
return &fleet.AppConfig{
|
||
OrgInfo: fleet.OrgInfo{OrgName: "Fleet"},
|
||
ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
|
||
AgentOptions: &defaultAgentOpts,
|
||
}, nil
|
||
}
|
||
|
||
var savedAppConfig *fleet.AppConfig
|
||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||
savedAppConfig = config
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
features:
|
||
enable_host_users: false
|
||
enable_software_inventory: false
|
||
mdm:
|
||
apple_bm_default_team: "team1"
|
||
macos_updates:
|
||
minimum_version: 12.1.1
|
||
deadline: 2011-02-01
|
||
`)
|
||
|
||
newMDMSettings := fleet.MDM{
|
||
AppleBMDefaultTeam: "team1",
|
||
AppleBMTermsExpired: false,
|
||
MacOSUpdates: fleet.MacOSUpdates{
|
||
MinimumVersion: "12.1.1",
|
||
Deadline: "2011-02-01",
|
||
},
|
||
}
|
||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
require.NotNil(t, savedAppConfig)
|
||
assert.False(t, savedAppConfig.Features.EnableHostUsers)
|
||
assert.False(t, savedAppConfig.Features.EnableSoftwareInventory)
|
||
assert.Equal(t, newMDMSettings, savedAppConfig.MDM)
|
||
// agent options were not modified, since they were not provided
|
||
assert.Equal(t, string(defaultAgentOpts), string(*savedAppConfig.AgentOptions))
|
||
|
||
name = writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
features:
|
||
enable_host_users: true
|
||
enable_software_inventory: true
|
||
agent_options:
|
||
mdm:
|
||
macos_updates:
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
require.NotNil(t, savedAppConfig)
|
||
assert.True(t, savedAppConfig.Features.EnableHostUsers)
|
||
assert.True(t, savedAppConfig.Features.EnableSoftwareInventory)
|
||
// agent options were cleared, provided but empty
|
||
assert.Nil(t, savedAppConfig.AgentOptions)
|
||
assert.Equal(t, newMDMSettings, savedAppConfig.MDM)
|
||
}
|
||
|
||
func TestApplyAppConfigDryRunIssue(t *testing.T) {
|
||
// reproduces the bug fixed by https://github.com/fleetdm/fleet/pull/8194
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
||
return userRoleSpecList, nil
|
||
}
|
||
|
||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||
if email == "admin1@example.com" {
|
||
return userRoleSpecList[0], nil
|
||
}
|
||
return userRoleSpecList[1], nil
|
||
}
|
||
|
||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||
return nil
|
||
}
|
||
|
||
currentAppConfig := &fleet.AppConfig{
|
||
OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
|
||
}
|
||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||
return currentAppConfig, nil
|
||
}
|
||
|
||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||
currentAppConfig = config
|
||
return nil
|
||
}
|
||
|
||
// first, set the default app config's agent options as set after fleetctl setup
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
agent_options:
|
||
config:
|
||
decorators:
|
||
load:
|
||
- SELECT uuid AS host_uuid FROM system_info;
|
||
- SELECT hostname AS hostname FROM system_info;
|
||
options:
|
||
disable_distributed: false
|
||
distributed_interval: 10
|
||
distributed_plugin: tls
|
||
distributed_tls_max_attempts: 3
|
||
logger_tls_endpoint: /api/osquery/log
|
||
logger_tls_period: 10
|
||
pack_delimiter: /
|
||
overrides: {}
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
|
||
// then, dry-run a valid app config's agent options, which made the original
|
||
// app config's agent options invalid JSON (when it shouldn't have modified
|
||
// it at all - the issue was in the cached_mysql datastore, it did not clone
|
||
// the app config properly).
|
||
name = writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
agent_options:
|
||
overrides:
|
||
platforms:
|
||
darwin:
|
||
auto_table_construction:
|
||
tcc_system_entries:
|
||
query: "SELECT service, client, allowed, prompt_count, last_modified FROM access"
|
||
path: "/Library/Application Support/com.apple.TCC/TCC.db"
|
||
columns:
|
||
- "service"
|
||
- "client"
|
||
- "allowed"
|
||
- "prompt_count"
|
||
- "last_modified"
|
||
`)
|
||
|
||
assert.Equal(t, "[+] would've applied fleet config\n", runAppForTest(t, []string{"apply", "--dry-run", "-f", name}))
|
||
|
||
// the saved app config was left unchanged, still equal to the original agent
|
||
// options
|
||
got := runAppForTest(t, []string{"get", "config"})
|
||
assert.Contains(t, got, `agent_options:
|
||
config:
|
||
decorators:
|
||
load:
|
||
- SELECT uuid AS host_uuid FROM system_info;
|
||
- SELECT hostname AS hostname FROM system_info;
|
||
options:
|
||
disable_distributed: false
|
||
distributed_interval: 10
|
||
distributed_plugin: tls
|
||
distributed_tls_max_attempts: 3
|
||
logger_tls_endpoint: /api/osquery/log
|
||
logger_tls_period: 10
|
||
pack_delimiter: /
|
||
overrides: {}`)
|
||
}
|
||
|
||
func TestApplyAppConfigUnknownFields(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
||
return userRoleSpecList, nil
|
||
}
|
||
|
||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||
if email == "admin1@example.com" {
|
||
return userRoleSpecList[0], nil
|
||
}
|
||
return userRoleSpecList[1], nil
|
||
}
|
||
|
||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||
return &fleet.AppConfig{}, nil
|
||
}
|
||
|
||
var savedAppConfig *fleet.AppConfig
|
||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||
savedAppConfig = config
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
features:
|
||
enabled_software_inventory: false # typo, correct config is enable_software_inventory
|
||
`)
|
||
|
||
runAppCheckErr(t, []string{"apply", "-f", name},
|
||
"applying fleet config: PATCH /api/latest/fleet/config received status 400 Bad Request: unsupported key provided: \"enabled_software_inventory\"",
|
||
)
|
||
require.Nil(t, savedAppConfig)
|
||
}
|
||
|
||
func TestApplyAppConfigDeprecatedFields(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
||
return userRoleSpecList, nil
|
||
}
|
||
|
||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||
if email == "admin1@example.com" {
|
||
return userRoleSpecList[0], nil
|
||
}
|
||
return userRoleSpecList[1], nil
|
||
}
|
||
|
||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||
return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"}}, nil
|
||
}
|
||
|
||
var savedAppConfig *fleet.AppConfig
|
||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||
savedAppConfig = config
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
host_settings:
|
||
enable_host_users: false
|
||
enable_software_inventory: false
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
require.NotNil(t, savedAppConfig)
|
||
assert.False(t, savedAppConfig.Features.EnableHostUsers)
|
||
assert.False(t, savedAppConfig.Features.EnableSoftwareInventory)
|
||
|
||
name = writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
host_settings:
|
||
enable_host_users: true
|
||
enable_software_inventory: true
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
require.NotNil(t, savedAppConfig)
|
||
assert.True(t, savedAppConfig.Features.EnableHostUsers)
|
||
assert.True(t, savedAppConfig.Features.EnableSoftwareInventory)
|
||
}
|
||
|
||
func TestApplyPolicies(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
var appliedPolicySpecs []*fleet.PolicySpec
|
||
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
|
||
appliedPolicySpecs = specs
|
||
return nil
|
||
}
|
||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||
if name == "Team1" {
|
||
return &fleet.Team{ID: 123}, nil
|
||
}
|
||
return nil, errors.New("unexpected team name!")
|
||
}
|
||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: policy
|
||
spec:
|
||
name: Is Gatekeeper enabled on macOS devices?
|
||
query: SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;
|
||
description: Checks to make sure that the Gatekeeper feature is enabled on macOS devices. Gatekeeper tries to ensure only trusted software is run on a mac machine.
|
||
resolution: "Run the following command in the Terminal app: /usr/sbin/spctl --master-enable"
|
||
platform: darwin
|
||
team: Team1
|
||
---
|
||
apiVersion: v1
|
||
kind: policy
|
||
spec:
|
||
name: Is disk encryption enabled on Windows devices?
|
||
query: SELECT 1 FROM bitlocker_info where protection_status = 1;
|
||
description: Checks to make sure that device encryption is enabled on Windows devices.
|
||
resolution: "Option 1: Select the Start button. Select Settings > Update & Security > Device encryption. If Device encryption doesn't appear, skip to Option 2. If device encryption is turned off, select Turn on. Option 2: Select the Start button. Under Windows System, select Control Panel. Select System and Security. Under BitLocker Drive Encryption, select Manage BitLocker. Select Turn on BitLocker and then follow the instructions."
|
||
platform: windows
|
||
---
|
||
apiVersion: v1
|
||
kind: policy
|
||
spec:
|
||
name: Is Filevault enabled on macOS devices?
|
||
query: SELECT 1 FROM disk_encryption WHERE user_uuid IS NOT “” AND filevault_status = ‘on’ LIMIT 1;
|
||
description: Checks to make sure that the Filevault feature is enabled on macOS devices.
|
||
resolution: "Choose Apple menu > System Preferences, then click Security & Privacy. Click the FileVault tab. Click the Lock icon, then enter an administrator name and password. Click Turn On FileVault."
|
||
platform: darwin
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied 3 policies\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
assert.True(t, ds.ApplyPolicySpecsFuncInvoked)
|
||
assert.Len(t, appliedPolicySpecs, 3)
|
||
for _, p := range appliedPolicySpecs {
|
||
assert.NotEmpty(t, p.Platform)
|
||
}
|
||
assert.True(t, ds.TeamByNameFuncInvoked)
|
||
}
|
||
|
||
func TestApplyEnrollSecrets(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
var appliedSecrets []*fleet.EnrollSecret
|
||
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
||
appliedSecrets = secrets
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: enroll_secret
|
||
spec:
|
||
secrets:
|
||
- secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff
|
||
- secret: reallyworks
|
||
- secret: thissecretwontwork!
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied enroll secrets\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
|
||
assert.Len(t, appliedSecrets, 3)
|
||
for _, s := range appliedSecrets {
|
||
assert.NotEmpty(t, s.Secret)
|
||
}
|
||
}
|
||
|
||
func TestApplyLabels(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
var appliedLabels []*fleet.LabelSpec
|
||
ds.ApplyLabelSpecsFunc = func(ctx context.Context, specs []*fleet.LabelSpec) error {
|
||
appliedLabels = specs
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: label
|
||
spec:
|
||
name: pending_updates
|
||
query: select 1;
|
||
platforms:
|
||
- darwin
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied 1 labels\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
assert.True(t, ds.ApplyLabelSpecsFuncInvoked)
|
||
require.Len(t, appliedLabels, 1)
|
||
assert.Equal(t, "pending_updates", appliedLabels[0].Name)
|
||
assert.Equal(t, "select 1;", appliedLabels[0].Query)
|
||
}
|
||
|
||
func TestApplyPacks(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
ds.ListPacksFunc = func(ctx context.Context, opt fleet.PackListOptions) ([]*fleet.Pack, error) {
|
||
return nil, nil
|
||
}
|
||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||
return nil
|
||
}
|
||
|
||
var appliedPacks []*fleet.PackSpec
|
||
ds.ApplyPackSpecsFunc = func(ctx context.Context, specs []*fleet.PackSpec) error {
|
||
appliedPacks = specs
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: pack
|
||
spec:
|
||
name: osquery_monitoring
|
||
queries:
|
||
- query: osquery_version
|
||
name: osquery_version_snapshot
|
||
interval: 7200
|
||
snapshot: true
|
||
- query: osquery_version
|
||
name: osquery_version_differential
|
||
interval: 7200
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied 1 packs\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
assert.True(t, ds.ApplyPackSpecsFuncInvoked)
|
||
require.Len(t, appliedPacks, 1)
|
||
assert.Equal(t, "osquery_monitoring", appliedPacks[0].Name)
|
||
require.Len(t, appliedPacks[0].Queries, 2)
|
||
|
||
interval := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: pack
|
||
spec:
|
||
name: test_bad_interval
|
||
queries:
|
||
- query: good_interval
|
||
name: good_interval
|
||
interval: 7200
|
||
- query: bad_interval
|
||
name: bad_interval
|
||
interval: 604801
|
||
`)
|
||
|
||
expectedErrMsg := "applying packs: POST /api/latest/fleet/spec/packs received status 400 Bad request: pack payload verification: pack scheduled query interval must be an integer greater than 1 and less than 604800"
|
||
|
||
_, err := runAppNoChecks([]string{"apply", "-f", interval})
|
||
assert.Error(t, err)
|
||
require.Equal(t, expectedErrMsg, err.Error())
|
||
}
|
||
|
||
func TestApplyQueries(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
var appliedQueries []*fleet.Query
|
||
ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||
return nil, sql.ErrNoRows
|
||
}
|
||
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error {
|
||
appliedQueries = queries
|
||
return nil
|
||
}
|
||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: query
|
||
spec:
|
||
description: Retrieves the list of application scheme/protocol-based IPC handlers.
|
||
name: app_schemes
|
||
query: select * from app_schemes;
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied 1 queries\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
assert.True(t, ds.ApplyQueriesFuncInvoked)
|
||
require.Len(t, appliedQueries, 1)
|
||
assert.Equal(t, "app_schemes", appliedQueries[0].Name)
|
||
assert.Equal(t, "select * from app_schemes;", appliedQueries[0].Query)
|
||
}
|
||
|
||
func TestCanApplyIntervalsInNanoseconds(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
// Stubs
|
||
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
||
return userRoleSpecList, nil
|
||
}
|
||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||
if email == "admin1@example.com" {
|
||
return userRoleSpecList[0], nil
|
||
}
|
||
return userRoleSpecList[1], nil
|
||
}
|
||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||
return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"}}, nil
|
||
}
|
||
|
||
var savedAppConfig *fleet.AppConfig
|
||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||
savedAppConfig = config
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
webhook_settings:
|
||
interval: 30000000000
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
require.Equal(t, savedAppConfig.WebhookSettings.Interval.Duration, 30*time.Second)
|
||
}
|
||
|
||
func TestCanApplyIntervalsUsingDurations(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t)
|
||
|
||
// Stubs
|
||
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
||
return userRoleSpecList, nil
|
||
}
|
||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||
if email == "admin1@example.com" {
|
||
return userRoleSpecList[0], nil
|
||
}
|
||
return userRoleSpecList[1], nil
|
||
}
|
||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||
return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"}}, nil
|
||
}
|
||
|
||
var savedAppConfig *fleet.AppConfig
|
||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||
savedAppConfig = config
|
||
return nil
|
||
}
|
||
|
||
name := writeTmpYml(t, `---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
webhook_settings:
|
||
interval: 30s
|
||
`)
|
||
|
||
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||
require.Equal(t, savedAppConfig.WebhookSettings.Interval.Duration, 30*time.Second)
|
||
}
|
||
|
||
func TestApplySpecs(t *testing.T) {
|
||
setupDS := func(ds *mock.Store) {
|
||
// labels
|
||
ds.ApplyLabelSpecsFunc = func(ctx context.Context, specs []*fleet.LabelSpec) error {
|
||
return nil
|
||
}
|
||
|
||
// teams - team ID 1 already exists
|
||
teamsByName := map[string]*fleet.Team{
|
||
"team1": {
|
||
ID: 1,
|
||
Name: "team1",
|
||
Description: "team1 description",
|
||
},
|
||
}
|
||
|
||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||
team, ok := teamsByName[name]
|
||
if !ok {
|
||
return nil, sql.ErrNoRows
|
||
}
|
||
return team, nil
|
||
}
|
||
|
||
i := 1 // new teams will start at 2
|
||
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||
i++
|
||
team.ID = uint(i)
|
||
teamsByName[team.Name] = team
|
||
return team, nil
|
||
}
|
||
|
||
agentOpts := json.RawMessage(`{"config":{}}`)
|
||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||
return &fleet.AppConfig{AgentOptions: &agentOpts}, nil
|
||
}
|
||
|
||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||
teamsByName[team.Name] = team
|
||
return team, nil
|
||
}
|
||
|
||
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
||
return nil
|
||
}
|
||
|
||
// activities
|
||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||
return nil
|
||
}
|
||
|
||
// app config
|
||
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
||
return userRoleSpecList, nil
|
||
}
|
||
|
||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||
if email == "admin1@example.com" {
|
||
return userRoleSpecList[0], nil
|
||
}
|
||
return userRoleSpecList[1], nil
|
||
}
|
||
|
||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||
return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"}}, nil
|
||
}
|
||
|
||
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
cases := []struct {
|
||
desc string
|
||
flags []string
|
||
spec string
|
||
wantOutput string
|
||
wantErr string
|
||
}{
|
||
{
|
||
desc: "empty team spec",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
`,
|
||
wantOutput: "[+] applied 1 teams",
|
||
},
|
||
{
|
||
desc: "empty team name",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: ""
|
||
`,
|
||
wantErr: `422 Validation Failed: name may not be empty`,
|
||
},
|
||
{
|
||
desc: "invalid agent options for existing team",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
agent_options:
|
||
config:
|
||
blah: nope
|
||
`,
|
||
wantErr: `400 Bad Request: unsupported key provided: "blah"`,
|
||
},
|
||
{
|
||
desc: "invalid top-level key for team",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
blah: nope
|
||
`,
|
||
wantErr: `400 Bad Request: unsupported key provided: "blah"`,
|
||
},
|
||
{
|
||
desc: "invalid known key's value type for team cannot be forced",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: 123
|
||
`,
|
||
flags: []string{"--force"},
|
||
wantErr: `400 Bad Request: invalid value type at 'specs.name': expected string but got number`,
|
||
},
|
||
{
|
||
desc: "unknown key for team can be forced",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
blah: true
|
||
`,
|
||
flags: []string{"--force"},
|
||
wantOutput: `[+] applied 1 teams`,
|
||
},
|
||
{
|
||
desc: "invalid agent options for new team",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: teamNEW
|
||
agent_options:
|
||
config:
|
||
blah: nope
|
||
`,
|
||
wantErr: `400 Bad Request: unsupported key provided: "blah"`,
|
||
},
|
||
{
|
||
desc: "invalid agent options dry-run",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: teamNEW
|
||
agent_options:
|
||
config:
|
||
blah: nope
|
||
`,
|
||
flags: []string{"--dry-run"},
|
||
wantErr: `400 Bad Request: unsupported key provided: "blah"`,
|
||
},
|
||
{
|
||
desc: "invalid agent options force",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: teamNEW
|
||
agent_options:
|
||
config:
|
||
blah: nope
|
||
`,
|
||
flags: []string{"--force"},
|
||
wantOutput: `[+] applied 1 teams`,
|
||
},
|
||
{
|
||
desc: "invalid agent options field type",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: teamNEW
|
||
agent_options:
|
||
config:
|
||
options:
|
||
aws_debug: 123
|
||
`,
|
||
flags: []string{"--dry-run"},
|
||
wantErr: `400 Bad Request: invalid value type at 'options.aws_debug': expected bool but got number`,
|
||
},
|
||
{
|
||
desc: "invalid team agent options command-line flag",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: teamNEW
|
||
agent_options:
|
||
command_line_flags:
|
||
no_such_flag: 123
|
||
`,
|
||
wantErr: `400 Bad Request: unsupported key provided: "no_such_flag"`,
|
||
},
|
||
{
|
||
desc: "valid team agent options command-line flag",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: teamNEW
|
||
agent_options:
|
||
command_line_flags:
|
||
enable_tables: "abc"
|
||
`,
|
||
wantOutput: `[+] applied 1 teams`,
|
||
},
|
||
{
|
||
desc: "invalid agent options field type in overrides",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: teamNEW
|
||
agent_options:
|
||
config:
|
||
options:
|
||
aws_debug: true
|
||
overrides:
|
||
platforms:
|
||
darwin:
|
||
options:
|
||
aws_debug: 123
|
||
`,
|
||
wantErr: `400 Bad Request: invalid value type at 'options.aws_debug': expected bool but got number`,
|
||
},
|
||
{
|
||
desc: "empty config",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
`,
|
||
wantOutput: ``, // no output for empty config
|
||
},
|
||
{
|
||
desc: "config with blank required org name",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
org_info:
|
||
org_name: ""
|
||
`,
|
||
wantErr: `422 Validation Failed: organization name must be present`,
|
||
},
|
||
{
|
||
desc: "config with blank required server url",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
server_settings:
|
||
server_url: ""
|
||
`,
|
||
wantErr: `422 Validation Failed: Fleet server URL must be present`,
|
||
},
|
||
{
|
||
desc: "config with unknown key",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
server_settings:
|
||
foo: bar
|
||
`,
|
||
wantErr: `400 Bad Request: unsupported key provided: "foo"`,
|
||
},
|
||
{
|
||
desc: "config with invalid key type",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
server_settings:
|
||
server_url: 123
|
||
`,
|
||
wantErr: `400 Bad request: failed to decode app config`,
|
||
},
|
||
{
|
||
desc: "config with invalid agent options in dry-run",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
agent_options:
|
||
foo: bar
|
||
`,
|
||
flags: []string{"--dry-run"},
|
||
wantErr: `400 Bad Request: unsupported key provided: "foo"`,
|
||
},
|
||
{
|
||
desc: "config with invalid agent options data type in dry-run",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
agent_options:
|
||
config:
|
||
options:
|
||
aws_debug: 123
|
||
`,
|
||
flags: []string{"--dry-run"},
|
||
wantErr: `400 Bad Request: invalid value type at 'options.aws_debug': expected bool but got number`,
|
||
},
|
||
{
|
||
desc: "config with invalid agent options data type with force",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
agent_options:
|
||
config:
|
||
options:
|
||
aws_debug: 123
|
||
`,
|
||
flags: []string{"--force"},
|
||
wantOutput: `[+] applied fleet config`,
|
||
},
|
||
{
|
||
desc: "config with invalid agent options command-line flags",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
agent_options:
|
||
command_line_flags:
|
||
enable_tables: "foo"
|
||
no_such_flag: false
|
||
`,
|
||
wantErr: `400 Bad Request: unsupported key provided: "no_such_flag"`,
|
||
},
|
||
{
|
||
desc: "config with invalid value for agent options command-line flags",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
agent_options:
|
||
command_line_flags:
|
||
enable_tables: 123
|
||
`,
|
||
wantErr: `400 Bad Request: invalid value type at 'enable_tables': expected string but got number`,
|
||
},
|
||
{
|
||
desc: "config with valid agent options command-line flags",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
agent_options:
|
||
command_line_flags:
|
||
enable_tables: "abc"
|
||
`,
|
||
wantOutput: `[+] applied fleet config`,
|
||
},
|
||
{
|
||
desc: "dry-run set with unsupported spec",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: label
|
||
spec:
|
||
name: label1
|
||
query: SELECT 1
|
||
`,
|
||
flags: []string{"--dry-run"},
|
||
wantOutput: `[!] ignoring labels, dry run mode only supported for 'config' and 'team' specs`,
|
||
},
|
||
{
|
||
desc: "dry-run set with various specs, appconfig warning for legacy",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: teamNEW
|
||
---
|
||
apiVersion: v1
|
||
kind: label
|
||
spec:
|
||
name: label1
|
||
query: SELECT 1
|
||
---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
host_settings:
|
||
enable_software_inventory: true
|
||
`,
|
||
flags: []string{"--dry-run"},
|
||
wantErr: `400 Bad request: warning: deprecated settings were used in the configuration: [host_settings]`,
|
||
wantOutput: `[!] ignoring labels, dry run mode only supported for 'config' and 'team' spec`,
|
||
},
|
||
{
|
||
desc: "dry-run set with various specs, no errors",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: teamNEW
|
||
---
|
||
apiVersion: v1
|
||
kind: label
|
||
spec:
|
||
name: label1
|
||
query: SELECT 1
|
||
---
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
features:
|
||
enable_software_inventory: true
|
||
`,
|
||
flags: []string{"--dry-run"},
|
||
wantOutput: `[!] ignoring labels, dry run mode only supported for 'config' and 'team' specs
|
||
[+] would've applied fleet config
|
||
[+] would've applied 1 teams`,
|
||
},
|
||
{
|
||
desc: "macos_updates deadline set but minimum_version empty",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_updates:
|
||
deadline: 2022-01-04
|
||
`,
|
||
wantErr: `422 Validation Failed: minimum_version is required when deadline is provided`,
|
||
},
|
||
{
|
||
desc: "macos_updates minimum_version set but deadline empty",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2"
|
||
`,
|
||
wantErr: `422 Validation Failed: deadline is required when minimum_version is provided`,
|
||
},
|
||
{
|
||
desc: "macos_updates.minimum_version with build version",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2 (ABCD)"
|
||
deadline: 1892-01-01
|
||
`,
|
||
wantErr: `422 Validation Failed: minimum_version accepts version numbers only. (E.g., "13.0.1.") NOT "Ventura 13" or "13.0.1 (22A400)"`,
|
||
},
|
||
{
|
||
desc: "macos_updates.deadline with timestamp",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2"
|
||
deadline: "1892-01-01T00:00:00Z"
|
||
`,
|
||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
||
},
|
||
{
|
||
desc: "macos_updates.deadline with invalid date",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2"
|
||
deadline: "18-01-01"
|
||
`,
|
||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
||
},
|
||
{
|
||
desc: "macos_updates.deadline with incomplete date",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2"
|
||
deadline: "2022-01"
|
||
`,
|
||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
||
},
|
||
{
|
||
desc: "missing required sso entity_id",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
sso_settings:
|
||
enable_sso: true
|
||
entity_id: ""
|
||
issuer_uri: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php"
|
||
idp_name: "SimpleSAML"
|
||
metadata_url: "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
|
||
`,
|
||
wantErr: `422 Validation Failed: required`,
|
||
},
|
||
{
|
||
desc: "missing required sso idp_name",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
sso_settings:
|
||
enable_sso: true
|
||
entity_id: "https://localhost:8080"
|
||
issuer_uri: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php"
|
||
idp_name: ""
|
||
metadata_url: "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
|
||
`,
|
||
wantErr: `422 Validation Failed: required`,
|
||
},
|
||
{
|
||
desc: "missing required failing policies destination_url",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
webhook_settings:
|
||
failing_policies_webhook:
|
||
enable_failing_policies_webhook: true
|
||
destination_url: ""
|
||
policy_ids:
|
||
- 1
|
||
host_batch_size: 1000
|
||
interval: 1h
|
||
`,
|
||
wantErr: `422 Validation Failed: destination_url is required to enable the failing policies webhook`,
|
||
},
|
||
{
|
||
desc: "missing required vulnerabilities destination_url",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
webhook_settings:
|
||
vulnerabilities_webhook:
|
||
enable_vulnerabilities_webhook: true
|
||
destination_url: ""
|
||
host_batch_size: 1000
|
||
interval: 1h
|
||
`,
|
||
wantErr: `422 Validation Failed: destination_url is required to enable the vulnerabilities webhook`,
|
||
},
|
||
{
|
||
desc: "missing required host status destination_url",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
webhook_settings:
|
||
host_status_webhook:
|
||
enable_host_status_webhook: true
|
||
destination_url: ""
|
||
days_count: 10
|
||
host_percentage: 10
|
||
interval: 1h
|
||
`,
|
||
wantErr: `422 Validation Failed: destination_url is required to enable the host status webhook`,
|
||
},
|
||
{
|
||
desc: "missing required host status days_count",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
webhook_settings:
|
||
host_status_webhook:
|
||
enable_host_status_webhook: true
|
||
destination_url: "http://some/url"
|
||
days_count: 0
|
||
host_percentage: 10
|
||
interval: 1h
|
||
`,
|
||
wantErr: `422 Validation Failed: days_count must be > 0 to enable the host status webhook`,
|
||
},
|
||
{
|
||
desc: "missing required host status host_percentage",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
webhook_settings:
|
||
host_status_webhook:
|
||
enable_host_status_webhook: true
|
||
destination_url: "http://some/url"
|
||
days_count: 10
|
||
host_percentage: -1
|
||
interval: 1h
|
||
`,
|
||
wantErr: `422 Validation Failed: host_percentage must be > 0 to enable the host status webhook`,
|
||
},
|
||
{
|
||
desc: "config with FIM values for agent options (#8699)",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
agent_options:
|
||
config:
|
||
file_paths:
|
||
ssh:
|
||
- /home/%/.ssh/authorized_keys
|
||
exclude_paths:
|
||
ssh:
|
||
- /home/ubuntu/.ssh/authorized_keys
|
||
`,
|
||
wantOutput: `[+] applied fleet config`,
|
||
},
|
||
{
|
||
desc: "app config macos_updates deadline set but minimum_version empty",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_updates:
|
||
deadline: 2022-01-04
|
||
`,
|
||
wantErr: `422 Validation Failed: minimum_version is required when deadline is provided`,
|
||
},
|
||
{
|
||
desc: "app config macos_updates minimum_version set but deadline empty",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2"
|
||
`,
|
||
wantErr: `422 Validation Failed: deadline is required when minimum_version is provided`,
|
||
},
|
||
{
|
||
desc: "app config macos_updates.minimum_version with build version",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2 (ABCD)"
|
||
deadline: 1892-01-01
|
||
`,
|
||
wantErr: `422 Validation Failed: minimum_version accepts version numbers only. (E.g., "13.0.1.") NOT "Ventura 13" or "13.0.1 (22A400)"`,
|
||
},
|
||
{
|
||
desc: "app config macos_updates.deadline with timestamp",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2"
|
||
deadline: "1892-01-01T00:00:00Z"
|
||
`,
|
||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
||
},
|
||
{
|
||
desc: "app config macos_updates.deadline with invalid date",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2"
|
||
deadline: "18-01-01"
|
||
`,
|
||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
||
},
|
||
{
|
||
desc: "app config macos_updates.deadline with incomplete date",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_updates:
|
||
minimum_version: "12.2"
|
||
deadline: "2022-01"
|
||
`,
|
||
wantErr: `422 Validation Failed: deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`,
|
||
},
|
||
{
|
||
desc: "app config macos_settings.enable_disk_encryption without a value",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_settings:
|
||
enable_disk_encryption:
|
||
`,
|
||
wantOutput: `[+] applied fleet config`,
|
||
},
|
||
{
|
||
desc: "app config macos_settings.enable_disk_encryption with invalid value type",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_settings:
|
||
enable_disk_encryption: 123
|
||
`,
|
||
wantErr: `400 Bad request: failed to decode app config`,
|
||
},
|
||
{
|
||
desc: "app config macos_settings.enable_disk_encryption true",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_settings:
|
||
enable_disk_encryption: true
|
||
`,
|
||
wantErr: `Couldn't update macos_settings because MDM features aren't turned on in Fleet.`,
|
||
},
|
||
{
|
||
desc: "app config macos_settings.enable_disk_encryption false",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: config
|
||
spec:
|
||
mdm:
|
||
macos_settings:
|
||
enable_disk_encryption: false
|
||
`,
|
||
wantOutput: `[+] applied fleet config`,
|
||
},
|
||
{
|
||
desc: "team config macos_settings.enable_disk_encryption without a value",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_settings:
|
||
enable_disk_encryption:
|
||
`,
|
||
wantErr: `400 Bad Request: invalid value type at 'macos_settings.enable_disk_encryption': expected bool but got <nil>`,
|
||
},
|
||
{
|
||
desc: "team config macos_settings.enable_disk_encryption with invalid value type",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_settings:
|
||
enable_disk_encryption: 123
|
||
`,
|
||
wantErr: `400 Bad Request: invalid value type at 'macos_settings.enable_disk_encryption': expected bool but got float64`,
|
||
},
|
||
{
|
||
desc: "team config macos_settings.enable_disk_encryption true",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_settings:
|
||
enable_disk_encryption: true
|
||
`,
|
||
wantErr: `Couldn't update macos_settings because MDM features aren't turned on in Fleet.`,
|
||
},
|
||
{
|
||
desc: "team config macos_settings.enable_disk_encryption false",
|
||
spec: `
|
||
apiVersion: v1
|
||
kind: team
|
||
spec:
|
||
team:
|
||
name: team1
|
||
mdm:
|
||
macos_settings:
|
||
enable_disk_encryption: false
|
||
`,
|
||
wantOutput: `[+] applied 1 teams`,
|
||
},
|
||
}
|
||
// NOTE: Integrations required fields are not tested (Jira/Zendesk) because
|
||
// they require a complex setup to mock the client that would communicate
|
||
// with the external API. However, we make a test API call when enabling an
|
||
// integration, ensuring that any missing configuration field results in an
|
||
// error. Same for smtp_settings (a test email is sent when enabling).
|
||
|
||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||
for _, c := range cases {
|
||
t.Run(c.desc, func(t *testing.T) {
|
||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license})
|
||
setupDS(ds)
|
||
filename := writeTmpYml(t, c.spec)
|
||
|
||
var got string
|
||
if c.wantErr == "" {
|
||
got = runAppForTest(t, append([]string{"apply", "-f", filename}, c.flags...))
|
||
} else {
|
||
buf, err := runAppNoChecks(append([]string{"apply", "-f", filename}, c.flags...))
|
||
require.ErrorContains(t, err, c.wantErr)
|
||
got = buf.String()
|
||
}
|
||
if c.wantOutput == "" {
|
||
require.Empty(t, got)
|
||
} else {
|
||
require.Contains(t, got, c.wantOutput)
|
||
}
|
||
})
|
||
}
|
||
}
|