mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 07:58:31 +00:00
Maps are reference types in Go, so the called function can modify the map even when it is passed by value (because the pointer is passed by value, pointing to the same underlying map). This simplifies the code. # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Added/updated automated tests - [ ] Manual QA for all new/changed functionality
4413 lines
144 KiB
Go
4413 lines
144 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/WatchBeam/clock"
|
|
"github.com/fleetdm/fleet/v4/server/authz"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
|
|
fleetLogging "github.com/fleetdm/fleet/v4/server/contexts/logging"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysqlredis"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
mockresult "github.com/fleetdm/fleet/v4/server/mock/mockresult"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/pubsub"
|
|
"github.com/fleetdm/fleet/v4/server/service/async"
|
|
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
|
|
"github.com/fleetdm/fleet/v4/server/service/osquery_utils"
|
|
"github.com/fleetdm/fleet/v4/server/service/redis_policy_set"
|
|
"github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetClientConfig(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
|
|
ds.TeamAgentOptionsFunc = func(ctx context.Context, teamID uint) (*json.RawMessage, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) {
|
|
return []*fleet.Pack{}, nil
|
|
}
|
|
ds.ListScheduledQueriesInPackFunc = func(ctx context.Context, pid uint) (fleet.ScheduledQueryList, error) {
|
|
tru := true
|
|
fals := false
|
|
fortytwo := uint(42)
|
|
switch pid {
|
|
case 1:
|
|
return []*fleet.ScheduledQuery{
|
|
{Name: "time", Query: "select * from time", Interval: 30, Removed: &fals},
|
|
}, nil
|
|
case 4:
|
|
return []*fleet.ScheduledQuery{
|
|
{Name: "foobar", Query: "select 3", Interval: 20, Shard: &fortytwo},
|
|
{Name: "froobing", Query: "select 'guacamole'", Interval: 60, Snapshot: &tru},
|
|
}, nil
|
|
default:
|
|
return []*fleet.ScheduledQuery{}, nil
|
|
}
|
|
}
|
|
ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint, hostID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) {
|
|
if teamID == nil {
|
|
return nil, nil
|
|
}
|
|
return []*fleet.Query{
|
|
{
|
|
Query: "SELECT 1 FROM table_1",
|
|
Name: "Some strings carry more weight than others",
|
|
Interval: 10,
|
|
Platform: "linux",
|
|
MinOsqueryVersion: "5.12.2",
|
|
Logging: "snapshot",
|
|
TeamID: ptr.Uint(1),
|
|
},
|
|
{
|
|
Query: "SELECT 1 FROM table_2",
|
|
Name: "You shall not pass",
|
|
Interval: 20,
|
|
Platform: "macos",
|
|
Logging: "differential",
|
|
TeamID: ptr.Uint(1),
|
|
},
|
|
}, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{AgentOptions: ptr.RawMessage(json.RawMessage(`{"config":{"options":{"baz":"bar"}}}`))}, nil
|
|
}
|
|
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
|
return nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
if id != 1 && id != 2 {
|
|
return nil, errors.New("not found")
|
|
}
|
|
return &fleet.Host{ID: id}, nil
|
|
}
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
ctx1 := hostctx.NewContext(ctx, &fleet.Host{ID: 1})
|
|
ctx2 := hostctx.NewContext(ctx, &fleet.Host{ID: 2})
|
|
ctx3 := hostctx.NewContext(ctx, &fleet.Host{ID: 1, TeamID: ptr.Uint(1)})
|
|
|
|
expectedOptions := map[string]interface{}{
|
|
"baz": "bar",
|
|
}
|
|
|
|
expectedConfig := map[string]interface{}{
|
|
"options": expectedOptions,
|
|
}
|
|
|
|
// No packs loaded yet
|
|
conf, err := svc.GetClientConfig(ctx1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedConfig, conf)
|
|
|
|
conf, err = svc.GetClientConfig(ctx2)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedConfig, conf)
|
|
|
|
// Now add packs
|
|
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) {
|
|
switch hid {
|
|
case 1:
|
|
return []*fleet.Pack{
|
|
{ID: 1, Name: "pack_by_label"},
|
|
{ID: 4, Name: "pack_by_other_label"},
|
|
}, nil
|
|
|
|
case 2:
|
|
return []*fleet.Pack{
|
|
{ID: 1, Name: "pack_by_label"},
|
|
}, nil
|
|
}
|
|
return []*fleet.Pack{}, nil
|
|
}
|
|
|
|
conf, err = svc.GetClientConfig(ctx1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedOptions, conf["options"])
|
|
assert.JSONEq(t, `{
|
|
"pack_by_other_label": {
|
|
"queries": {
|
|
"foobar":{"query":"select 3","interval":20,"shard":42},
|
|
"froobing":{"query":"select 'guacamole'","interval":60,"snapshot":true}
|
|
}
|
|
},
|
|
"pack_by_label": {
|
|
"queries":{
|
|
"time":{"query":"select * from time","interval":30,"removed":false}
|
|
}
|
|
}
|
|
}`,
|
|
string(conf["packs"].(json.RawMessage)),
|
|
)
|
|
|
|
conf, err = svc.GetClientConfig(ctx2)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedOptions, conf["options"])
|
|
assert.JSONEq(t, `{
|
|
"pack_by_label": {
|
|
"queries":{
|
|
"time":{"query":"select * from time","interval":30,"removed":false}
|
|
}
|
|
}
|
|
}`,
|
|
string(conf["packs"].(json.RawMessage)),
|
|
)
|
|
|
|
// Check scheduled queries are loaded properly
|
|
conf, err = svc.GetClientConfig(ctx3)
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `{
|
|
"pack_by_label": {
|
|
"queries":{
|
|
"time":{"query":"select * from time","interval":30,"removed":false}
|
|
}
|
|
},
|
|
"pack_by_other_label": {
|
|
"queries": {
|
|
"foobar":{"query":"select 3","interval":20,"shard":42},
|
|
"froobing":{"query":"select 'guacamole'","interval":60,"snapshot":true}
|
|
}
|
|
},
|
|
"team-1": {
|
|
"queries": {
|
|
"Some strings carry more weight than others": {
|
|
"query": "SELECT 1 FROM table_1",
|
|
"interval": 10,
|
|
"platform": "linux",
|
|
"version": "5.12.2",
|
|
"snapshot": true
|
|
},
|
|
"You shall not pass": {
|
|
"query": "SELECT 1 FROM table_2",
|
|
"interval": 20,
|
|
"platform": "macos",
|
|
"removed": true,
|
|
"version": ""
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
string(conf["packs"].(json.RawMessage)),
|
|
)
|
|
}
|
|
|
|
func TestAgentOptionsForHost(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
teamID := uint(1)
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{
|
|
AgentOptions: ptr.RawMessage(json.RawMessage(`{"config":{"baz":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override2"}}}}`)),
|
|
}, nil
|
|
}
|
|
ds.TeamAgentOptionsFunc = func(ctx context.Context, id uint) (*json.RawMessage, error) {
|
|
return ptr.RawMessage(json.RawMessage(`{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}}`)), nil
|
|
}
|
|
|
|
host := &fleet.Host{
|
|
TeamID: &teamID,
|
|
Platform: "darwin",
|
|
}
|
|
|
|
opt, err := svc.AgentOptionsForHost(ctx, host.TeamID, host.Platform)
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `{"foo":"override"}`, string(opt))
|
|
|
|
host.Platform = "windows"
|
|
opt, err = svc.AgentOptionsForHost(ctx, host.TeamID, host.Platform)
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `{"foo":"bar"}`, string(opt))
|
|
|
|
// Should take gobal option with no team
|
|
host.TeamID = nil
|
|
opt, err = svc.AgentOptionsForHost(ctx, host.TeamID, host.Platform)
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `{"baz":"bar"}`, string(opt))
|
|
|
|
host.Platform = "darwin"
|
|
opt, err = svc.AgentOptionsForHost(ctx, host.TeamID, host.Platform)
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `{"foo":"override2"}`, string(opt))
|
|
}
|
|
|
|
var allDetailQueries = osquery_utils.GetDetailQueries(
|
|
context.Background(),
|
|
config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}},
|
|
nil,
|
|
&fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
},
|
|
osquery_utils.Integrations{},
|
|
)
|
|
|
|
func expectedDetailQueriesForPlatform(platform string) map[string]osquery_utils.DetailQuery {
|
|
queries := make(map[string]osquery_utils.DetailQuery)
|
|
for k, v := range allDetailQueries {
|
|
if v.RunsForPlatform(platform) {
|
|
queries[k] = v
|
|
}
|
|
}
|
|
return queries
|
|
}
|
|
|
|
func TestEnrollAgent(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ds.VerifyEnrollSecretFunc = func(ctx context.Context, secret string) (*fleet.EnrollSecret, error) {
|
|
switch secret {
|
|
case "valid_secret":
|
|
return &fleet.EnrollSecret{Secret: "valid_secret", TeamID: ptr.Uint(3)}, nil
|
|
default:
|
|
return nil, errors.New("not found")
|
|
}
|
|
}
|
|
ds.EnrollHostFunc = func(ctx context.Context, isMDMEnabled bool, osqueryHostId, hUUID, hSerial, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error) {
|
|
assert.Equal(t, ptr.Uint(3), teamID)
|
|
return &fleet.Host{
|
|
OsqueryHostID: &osqueryHostId, NodeKey: &nodeKey,
|
|
}, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
nodeKey, err := svc.EnrollAgent(ctx, "valid_secret", "host123", nil)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, nodeKey)
|
|
}
|
|
|
|
func TestEnrollAgentEnforceLimit(t *testing.T) {
|
|
runTest := func(t *testing.T, pool fleet.RedisPool) {
|
|
const maxHosts = 2
|
|
|
|
var hostIDSeq uint
|
|
ds := new(mock.Store)
|
|
ds.VerifyEnrollSecretFunc = func(ctx context.Context, secret string) (*fleet.EnrollSecret, error) {
|
|
switch secret {
|
|
case "valid_secret":
|
|
return &fleet.EnrollSecret{Secret: "valid_secret"}, nil
|
|
default:
|
|
return nil, errors.New("not found")
|
|
}
|
|
}
|
|
ds.EnrollHostFunc = func(ctx context.Context, isMDMEnabled bool, osqueryHostId, hUUID, hSerial, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error) {
|
|
hostIDSeq++
|
|
return &fleet.Host{
|
|
ID: hostIDSeq, OsqueryHostID: &osqueryHostId, NodeKey: &nodeKey,
|
|
}, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
return &fleet.Host{ID: id}, nil
|
|
}
|
|
ds.DeleteHostFunc = func(ctx context.Context, id uint) error {
|
|
return nil
|
|
}
|
|
|
|
redisWrapDS := mysqlredis.New(ds, pool, mysqlredis.WithEnforcedHostLimit(maxHosts))
|
|
svc, ctx := newTestService(t, redisWrapDS, nil, nil, &TestServerOpts{
|
|
EnrollHostLimiter: redisWrapDS,
|
|
License: &fleet.LicenseInfo{DeviceCount: maxHosts},
|
|
})
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{
|
|
User: &fleet.User{
|
|
ID: 0,
|
|
GlobalRole: ptr.String(fleet.RoleAdmin),
|
|
},
|
|
})
|
|
|
|
nodeKey, err := svc.EnrollAgent(ctx, "valid_secret", "host001", nil)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, nodeKey)
|
|
|
|
nodeKey, err = svc.EnrollAgent(ctx, "valid_secret", "host002", nil)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, nodeKey)
|
|
|
|
_, err = svc.EnrollAgent(ctx, "valid_secret", "host003", nil)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), fmt.Sprintf("maximum number of hosts reached: %d", maxHosts))
|
|
|
|
// delete a host with id 1
|
|
err = svc.DeleteHost(ctx, 1)
|
|
require.NoError(t, err)
|
|
|
|
// now host 003 can be enrolled
|
|
nodeKey, err = svc.EnrollAgent(ctx, "valid_secret", "host003", nil)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, nodeKey)
|
|
}
|
|
|
|
t.Run("standalone", func(t *testing.T) {
|
|
pool := redistest.SetupRedis(t, "enrolled_hosts:*", false, false, false)
|
|
runTest(t, pool)
|
|
})
|
|
|
|
t.Run("cluster", func(t *testing.T) {
|
|
pool := redistest.SetupRedis(t, "enrolled_hosts:*", true, true, false)
|
|
runTest(t, pool)
|
|
})
|
|
}
|
|
|
|
func TestEnrollAgentIncorrectEnrollSecret(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ds.VerifyEnrollSecretFunc = func(ctx context.Context, secret string) (*fleet.EnrollSecret, error) {
|
|
switch secret {
|
|
case "valid_secret":
|
|
return &fleet.EnrollSecret{Secret: "valid_secret", TeamID: ptr.Uint(3)}, nil
|
|
default:
|
|
return nil, errors.New("not found")
|
|
}
|
|
}
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
nodeKey, err := svc.EnrollAgent(ctx, "not_correct", "host123", nil)
|
|
assert.NotNil(t, err)
|
|
assert.Empty(t, nodeKey)
|
|
}
|
|
|
|
func TestEnrollAgentDetails(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ds.VerifyEnrollSecretFunc = func(ctx context.Context, secret string) (*fleet.EnrollSecret, error) {
|
|
return &fleet.EnrollSecret{}, nil
|
|
}
|
|
ds.EnrollHostFunc = func(ctx context.Context, isMDMEnabled bool, osqueryHostId, hUUID, hSerial, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error) {
|
|
return &fleet.Host{
|
|
OsqueryHostID: &osqueryHostId, NodeKey: &nodeKey,
|
|
}, nil
|
|
}
|
|
var gotHost *fleet.Host
|
|
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
|
gotHost = host
|
|
return nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
details := map[string](map[string]string){
|
|
"osquery_info": {"version": "2.12.0"},
|
|
"system_info": {"hostname": "zwass.local", "uuid": "froobling_uuid"},
|
|
"os_version": {
|
|
"name": "Mac OS X",
|
|
"major": "10",
|
|
"minor": "14",
|
|
"patch": "5",
|
|
"platform": "darwin",
|
|
},
|
|
"foo": {"foo": "bar"},
|
|
}
|
|
nodeKey, err := svc.EnrollAgent(ctx, "", "host123", details)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, nodeKey)
|
|
|
|
assert.Equal(t, "Mac OS X 10.14.5", gotHost.OSVersion)
|
|
assert.Equal(t, "darwin", gotHost.Platform)
|
|
assert.Equal(t, "2.12.0", gotHost.OsqueryVersion)
|
|
assert.Equal(t, "zwass.local", gotHost.Hostname)
|
|
assert.Equal(t, "froobling_uuid", gotHost.UUID)
|
|
}
|
|
|
|
func TestAuthenticateHost(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
task := async.NewTask(ds, nil, clock.C, config.OsqueryConfig{})
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{Task: task})
|
|
|
|
var gotKey string
|
|
host := fleet.Host{ID: 1, Hostname: "foobar"}
|
|
ds.LoadHostByNodeKeyFunc = func(ctx context.Context, nodeKey string) (*fleet.Host, error) {
|
|
gotKey = nodeKey
|
|
return &host, nil
|
|
}
|
|
var gotHostIDs []uint
|
|
ds.MarkHostsSeenFunc = func(ctx context.Context, hostIDs []uint, t time.Time) error {
|
|
gotHostIDs = hostIDs
|
|
return nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
_, _, err := svc.AuthenticateHost(ctx, "test")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test", gotKey)
|
|
assert.False(t, ds.MarkHostsSeenFuncInvoked)
|
|
|
|
host = fleet.Host{ID: 7, Hostname: "foobar"}
|
|
_, _, err = svc.AuthenticateHost(ctx, "floobar")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "floobar", gotKey)
|
|
assert.False(t, ds.MarkHostsSeenFuncInvoked)
|
|
// Host checks in twice
|
|
host = fleet.Host{ID: 7, Hostname: "foobar"}
|
|
_, _, err = svc.AuthenticateHost(ctx, "floobar")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "floobar", gotKey)
|
|
assert.False(t, ds.MarkHostsSeenFuncInvoked)
|
|
|
|
err = task.FlushHostsLastSeen(ctx, time.Now())
|
|
require.NoError(t, err)
|
|
assert.True(t, ds.MarkHostsSeenFuncInvoked)
|
|
ds.MarkHostsSeenFuncInvoked = false
|
|
assert.ElementsMatch(t, []uint{1, 7}, gotHostIDs)
|
|
|
|
err = task.FlushHostsLastSeen(ctx, time.Now())
|
|
require.NoError(t, err)
|
|
assert.True(t, ds.MarkHostsSeenFuncInvoked)
|
|
require.Len(t, gotHostIDs, 0)
|
|
}
|
|
|
|
func TestAuthenticateHostFailure(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
ds.LoadHostByNodeKeyFunc = func(ctx context.Context, nodeKey string) (*fleet.Host, error) {
|
|
return nil, errors.New("not found")
|
|
}
|
|
|
|
_, _, err := svc.AuthenticateHost(ctx, "test")
|
|
require.NotNil(t, err)
|
|
}
|
|
|
|
type testJSONLogger struct {
|
|
logs []json.RawMessage
|
|
}
|
|
|
|
func (n *testJSONLogger) Write(ctx context.Context, logs []json.RawMessage) error {
|
|
n.logs = logs
|
|
return nil
|
|
}
|
|
|
|
func TestSubmitStatusLogs(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
// Hack to get at the service internals and modify the writer
|
|
serv := ((svc.(validationMiddleware)).Service).(*Service)
|
|
|
|
testLogger := &testJSONLogger{}
|
|
serv.osqueryLogWriter = &OsqueryLogger{Status: testLogger}
|
|
|
|
logs := []string{
|
|
`{"severity":"0","filename":"tls.cpp","line":"216","message":"some message","version":"1.8.2","decorations":{"host_uuid":"uuid_foobar","username":"zwass"}}`,
|
|
`{"severity":"1","filename":"buffered.cpp","line":"122","message":"warning!","version":"1.8.2","decorations":{"host_uuid":"uuid_foobar","username":"zwass"}}`,
|
|
}
|
|
logJSON := fmt.Sprintf("[%s]", strings.Join(logs, ","))
|
|
|
|
var status []json.RawMessage
|
|
err := json.Unmarshal([]byte(logJSON), &status)
|
|
require.NoError(t, err)
|
|
|
|
host := fleet.Host{}
|
|
ctx = hostctx.NewContext(ctx, &host)
|
|
err = serv.SubmitStatusLogs(ctx, status)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, status, testLogger.logs)
|
|
}
|
|
|
|
func TestSubmitResultLogsToLogDestination(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{Logger: log.NewJSONLogger(os.Stdout)})
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
|
|
switch {
|
|
case teamID != nil && *teamID == 1:
|
|
return &fleet.Query{
|
|
ID: 4242,
|
|
Name: name,
|
|
AutomationsEnabled: true,
|
|
TeamID: ptr.Uint(1),
|
|
Logging: fleet.LoggingSnapshot,
|
|
}, nil
|
|
case teamID != nil && *teamID == 2:
|
|
return &fleet.Query{
|
|
ID: 4343,
|
|
Name: name,
|
|
AutomationsEnabled: true,
|
|
TeamID: ptr.Uint(2),
|
|
Logging: fleet.LoggingSnapshot,
|
|
}, nil
|
|
case teamID == nil && (name == "time" || name == "system_info" || name == "encrypted" || name == "hosts"):
|
|
return &fleet.Query{
|
|
ID: uint(name[0]),
|
|
Name: name,
|
|
AutomationsEnabled: true,
|
|
}, nil
|
|
case teamID != nil && *teamID == 1 && name == "hosts":
|
|
return &fleet.Query{
|
|
Name: name,
|
|
AutomationsEnabled: true,
|
|
TeamID: teamID,
|
|
}, nil
|
|
case teamID == nil && name == "query_not_automated":
|
|
return &fleet.Query{
|
|
Name: name,
|
|
AutomationsEnabled: false,
|
|
}, nil
|
|
case teamID == nil && name == "query_should_be_saved_and_submitted":
|
|
return &fleet.Query{
|
|
ID: 123,
|
|
Name: name,
|
|
AutomationsEnabled: true,
|
|
Logging: fleet.LoggingSnapshot,
|
|
}, nil
|
|
case teamID == nil && name == "All USB devices":
|
|
return &fleet.Query{
|
|
ID: 777,
|
|
Name: name,
|
|
AutomationsEnabled: true,
|
|
Logging: fleet.LoggingSnapshot,
|
|
}, nil
|
|
case teamID == nil && name == "query_should_be_saved_and_submitted_with_custom_pack_delimiter":
|
|
return &fleet.Query{
|
|
ID: 1234,
|
|
Name: name,
|
|
AutomationsEnabled: true,
|
|
Logging: fleet.LoggingSnapshot,
|
|
}, nil
|
|
case teamID == nil && name == "query_should_be_saved_but_not_submitted":
|
|
return &fleet.Query{
|
|
ID: 444,
|
|
Name: name,
|
|
AutomationsEnabled: false,
|
|
Logging: fleet.LoggingSnapshot,
|
|
}, nil
|
|
case teamID == nil && name == "query_no_rows":
|
|
return &fleet.Query{
|
|
ID: 555,
|
|
Name: name,
|
|
AutomationsEnabled: true,
|
|
Logging: fleet.LoggingSnapshot,
|
|
}, nil
|
|
default:
|
|
return nil, newNotFoundError()
|
|
}
|
|
}
|
|
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
|
|
return 0, nil
|
|
}
|
|
teamQueryResultsStored := false
|
|
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
|
if len(rows) == 0 {
|
|
return nil
|
|
}
|
|
if rows[0].QueryID == 777 {
|
|
require.Len(t, rows, 3)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
require.NotZero(t, rows[i].LastFetched)
|
|
require.Equal(t, uint(999), rows[i].HostID)
|
|
require.Equal(t, uint(777), rows[i].QueryID)
|
|
}
|
|
|
|
require.JSONEq(t, `{"class":"9","model":"AppleUSBVHCIBCE Root Hub Simulation","model_id":"8007","protocol":"","removable":"0","serial":"0","subclass":"255","usb_address":"","usb_port":"","vendor":"Apple Inc.","vendor_id":"05ac","version":"0.0"}`, string(*rows[0].Data))
|
|
require.JSONEq(t, `{"class":"9","model":"AppleUSBXHCI Root Hub Simulation","model_id":"8007","protocol":"","removable":"0","serial":"0","subclass":"255","usb_address":"","usb_port":"","vendor":"Apple Inc.","vendor_id":"05ac","version":"0.0"}`, string(*rows[1].Data))
|
|
require.JSONEq(t, `{"class":"9","model":"AppleUSBXHCI Root Hub Simulation","model_id":"8008","protocol":"","removable":"0","serial":"1","subclass":"255","usb_address":"","usb_port":"","vendor":"Apple Inc.","vendor_id":"05ac","version":"0.0"}`, string(*rows[2].Data))
|
|
}
|
|
switch {
|
|
case rows[0].QueryID == 4242:
|
|
t.Fatal("should not happen, as query 4242 is a team query and host is global")
|
|
case rows[0].QueryID == 4343:
|
|
teamQueryResultsStored = true
|
|
case rows[0].QueryID == 123:
|
|
require.Len(t, rows, 1)
|
|
require.Equal(t, uint(999), rows[0].HostID)
|
|
require.NotZero(t, rows[0].LastFetched)
|
|
require.JSONEq(t, `{"hour":"20","minutes":"8"}`, string(*rows[0].Data))
|
|
case rows[0].QueryID == 444:
|
|
require.Len(t, rows, 2)
|
|
require.Equal(t, uint(999), rows[0].HostID)
|
|
require.NotZero(t, rows[0].LastFetched)
|
|
require.JSONEq(t, `{"hour":"20","minutes":"8"}`, string(*rows[0].Data))
|
|
require.Equal(t, uint(999), rows[1].HostID)
|
|
require.Equal(t, uint(444), rows[1].QueryID)
|
|
require.NotZero(t, rows[1].LastFetched)
|
|
require.JSONEq(t, `{"hour":"21","minutes":"9"}`, string(*rows[1].Data))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Hack to get at the service internals and modify the writer
|
|
serv := ((svc.(validationMiddleware)).Service).(*Service)
|
|
|
|
testLogger := &testJSONLogger{}
|
|
serv.osqueryLogWriter = &OsqueryLogger{Result: testLogger}
|
|
|
|
validLogResults := []string{
|
|
`{"action":"added","calendarTime":"Fri Sep 30 17:55:15 2016 UTC","columns":{"cpu_brand":"Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz","hostname":"hostimus","physical_memory":"17179869184"},"decorations":{"host_uuid":"some_uuid","username":"zwass"},"hostIdentifier":"some_uuid","name":"pack/Global/system_info","query_id":115,"unixTime":1475258115}`,
|
|
|
|
`{"name":"pack/SomePack/encrypted","hostIdentifier":"some_uuid","calendarTime":"Fri Sep 30 21:19:15 2016 UTC","unixTime":1475270355,"decorations":{"host_uuid":"4740D59F-699E-5B29-960B-979AAF9BBEEB","username":"zwass"},"columns":{"encrypted":"1","name":"\/dev\/disk1","type":"AES-XTS","uid":"","user_uuid":"","uuid":"some_uuid"},"action":"added"}`,
|
|
`{"name":"pack/SomePack/encrypted","hostIdentifier":"some_uuid","calendarTime":"Fri Sep 30 21:19:14 2016 UTC","unixTime":1475270354,"decorations":{"host_uuid":"4740D59F-699E-5B29-960B-979AAF9BBEEB","username":"zwass"},"columns":{"encrypted":"1","name":"\/dev\/disk1","type":"AES-XTS","uid":"","user_uuid":"","uuid":"some_uuid"},"action":"added"}`,
|
|
|
|
// These results belong to the same query but have 1 second difference.
|
|
`{"action":"snapshot","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"},"hostIdentifier":"1379f59d98f4","name":"pack/Global/time","query_id":116,"snapshot":[{"hour":"20","minutes":"8"}],"unixTime":1484078931}`,
|
|
`{"action":"snapshot","calendarTime":"Tue Jan 10 20:08:50 2017 UTC","decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"},"hostIdentifier":"1379f59d98f4","name":"pack/Global/time","query_id":116,"snapshot":[{"hour":"20","minutes":"8"}],"unixTime":1484078930}`,
|
|
`{"action":"snapshot","calendarTime":"Tue Jan 10 20:08:52 2017 UTC","decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"},"hostIdentifier":"1379f59d98f4","name":"pack/Global/time","query_id":116,"snapshot":[{"hour":"20","minutes":"8"}],"unixTime":1484078932}`,
|
|
|
|
`{"calendarTime":"Sun Nov 19 00:02:08 2017 UTC","counter":"10","decorations":{"host_uuid":"FA01680E-98CA-5557-8F59-7716ECFEE964","hostname":"kl.groob.io"},"diffResults":{"removed":[{"address":"127.0.0.1","hostnames":"kl.groob.io"}],"added":""},"epoch":"0","hostIdentifier":"FA01680E-98CA-5557-8F59-7716ECFEE964","name":"pack\/team-1/hosts","query_id":4242,"unixTime":1511049728}`,
|
|
|
|
`{"action":"snapshot","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"},"hostIdentifier":"1379f59d98f4","name":"pack/Global/query_should_be_saved_and_submitted","query_id":123,"snapshot":[{"hour":"20","minutes":"8"}],"unixTime":1484078931}`,
|
|
`{"action":"snapshot","calendarTime":"Tue Jan 10 20:08:52 2017 UTC","decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"},"hostIdentifier":"1379f59d98f4","name":"pack_Global_query_should_be_saved_and_submitted_with_custom_pack_delimiter","query_id":1234,"snapshot":[{"hour":"20","minutes":"8"}],"unixTime":1484078932}`,
|
|
|
|
// Fleet doesn't know of this query, so this result should be streamed as is (This is to support streaming results for osquery nodes that are configured outside of Fleet, e.g. `--config_plugin=filesystem`).
|
|
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/Global/doesntexist","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
|
|
|
|
// If a global query belongs to a 2017/legacy pack, it should be automated even if the global query has automations turned off.
|
|
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/Some Pack Name/query_not_automated","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
|
|
|
|
// The "name" field has invalid format, so this result will be streamed as is (This is to support streaming results for osquery nodes that are configured outside of Fleet, e.g. `--config_plugin=filesystem`).
|
|
`{"name":"com.foo.bar","hostIdentifier":"52eb420a-2085-438a-abf0-5670e97588e2","calendarTime":"Thu Dec 7 15:15:20 2023 UTC","unixTime":1701962120,"epoch":0,"counter":0,"numerics":false,"columns":{"foo": "bar"},"action":"snapshot"}`,
|
|
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"some_name","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
|
|
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/team-foo/bar","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
|
|
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/team-","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
|
|
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/PackName","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
|
|
|
|
// Query results of a query that belongs to a different team than the host's team (can happen when host is transferred from one team to another or no team).
|
|
`{"action":"snapshot","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"},"hostIdentifier":"1379f59d98f4","name":"pack/team-1/Foobar","query_id":4242,"snapshot":[{"hour":"20","minutes":"8"}],"unixTime":1484078931}`,
|
|
|
|
// Query results in event format (this is received for scheduled query results when the host is configured with `--logger_snapshot_event_type=true`).
|
|
// The "event format" contains one result for each row returned by the query and the columns in the "columns" key.
|
|
`{"action":"snapshot","calendarTime":"Wed Jan 29 12:32:54 2025 UTC","columns":{"class":"9","model":"AppleUSBVHCIBCE Root Hub Simulation","model_id":"8007","protocol":"","removable":"0","serial":"0","subclass":"255","usb_address":"","usb_port":"","vendor":"Apple Inc.","vendor_id":"05ac","version":"0.0"},"counter":0,"decorations":{"host_uuid":"634B6F24-97FB-44C9-8342-5CF20A08619F","hostname":"foobar.local"},"epoch":0,"hostIdentifier":"634B6F24-97FB-44C9-8342-5CF20A08619F","name":"pack/Global/All USB devices","numerics":false,"query_id":777,"unixTime":1738153974}`,
|
|
`{"action":"snapshot","calendarTime":"Wed Jan 29 12:32:54 2025 UTC","columns":{"class":"9","model":"AppleUSBXHCI Root Hub Simulation","model_id":"8007","protocol":"","removable":"0","serial":"0","subclass":"255","usb_address":"","usb_port":"","vendor":"Apple Inc.","vendor_id":"05ac","version":"0.0"},"counter":0,"decorations":{"host_uuid":"634B6F24-97FB-44C9-8342-5CF20A08619F","hostname":"foobar.local"},"epoch":0,"hostIdentifier":"634B6F24-97FB-44C9-8342-5CF20A08619F","name":"pack/Global/All USB devices","numerics":false,"query_id":777,"unixTime":1738153974}`,
|
|
`{"action":"snapshot","calendarTime":"Wed Jan 29 12:32:54 2025 UTC","columns":{"class":"9","model":"AppleUSBXHCI Root Hub Simulation","model_id":"8008","protocol":"","removable":"0","serial":"1","subclass":"255","usb_address":"","usb_port":"","vendor":"Apple Inc.","vendor_id":"05ac","version":"0.0"},"counter":0,"decorations":{"host_uuid":"634B6F24-97FB-44C9-8342-5CF20A08619F","hostname":"foobar.local"},"epoch":0,"hostIdentifier":"634B6F24-97FB-44C9-8342-5CF20A08619F","name":"pack/Global/All USB devices","numerics":false,"query_id":777,"unixTime":1738153974}`,
|
|
}
|
|
logJSON := fmt.Sprintf("[%s]", strings.Join(validLogResults, ","))
|
|
|
|
resultWithInvalidJSON := []byte("foobar:\n\t123")
|
|
resultWithInvalidJSONLong := []byte("foobar:\n\t1233333333333333333333333333333333333333333333333333333333")
|
|
// The "name" field will be empty, so this result will be ignored.
|
|
resultWithoutName := []byte(`{"unknown":{"foo": [] }}`)
|
|
// The query was configured with automations disabled, so this result will be ignored.
|
|
resultWithQueryNotAutomated := []byte(`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/Global/query_not_automated","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`)
|
|
// The query is supposed to be saved but with automations disabled (and has two columns).
|
|
resultWithQuerySavedNotAutomated := []byte(`{"snapshot":[{"hour":"20","minutes":"8"},{"hour":"21","minutes":"9"}],"action":"snapshot","name":"pack/Global/query_should_be_saved_but_not_submitted","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`)
|
|
|
|
var validResults []json.RawMessage
|
|
err := json.Unmarshal([]byte(logJSON), &validResults)
|
|
require.NoError(t, err)
|
|
|
|
host := fleet.Host{
|
|
ID: 999,
|
|
TeamID: nil, // Global host.
|
|
}
|
|
ctx = hostctx.NewContext(ctx, &host)
|
|
|
|
// Submit valid, invalid and to-be-ignored log results mixed.
|
|
validAndInvalidResults := make([]json.RawMessage, 0, len(validResults)+5)
|
|
for i, result := range validResults {
|
|
validAndInvalidResults = append(validAndInvalidResults, result)
|
|
if i == 2 {
|
|
validAndInvalidResults = append(validAndInvalidResults,
|
|
resultWithInvalidJSON, resultWithInvalidJSONLong,
|
|
resultWithoutName, resultWithQueryNotAutomated,
|
|
resultWithQuerySavedNotAutomated,
|
|
)
|
|
}
|
|
}
|
|
err = serv.SubmitResultLogs(ctx, validAndInvalidResults)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, validResults, testLogger.logs)
|
|
|
|
//
|
|
// Run a similar test but now with a team host.
|
|
//
|
|
host = fleet.Host{
|
|
ID: 999,
|
|
TeamID: ptr.Uint(2),
|
|
}
|
|
ctx = hostctx.NewContext(ctx, &host)
|
|
results := []json.RawMessage{
|
|
// This query should be ignored.
|
|
json.RawMessage(`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/team-1/Foobar","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`),
|
|
// This query should be stored.
|
|
json.RawMessage(`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/team-2/Zoobar","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`),
|
|
}
|
|
err = serv.SubmitResultLogs(ctx, results)
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, teamQueryResultsStored)
|
|
}
|
|
|
|
func TestSaveResultLogsToQueryReports(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
// Hack to get at the private methods
|
|
serv := ((svc.(validationMiddleware)).Service).(*Service)
|
|
|
|
host := fleet.Host{}
|
|
ctx = hostctx.NewContext(ctx, &host)
|
|
|
|
results := []*fleet.ScheduledQueryResult{
|
|
{
|
|
QueryName: "pack/Global/Uptime",
|
|
OsqueryHostID: "1379f59d98f4",
|
|
Snapshot: []*json.RawMessage{
|
|
ptr.RawMessage(json.RawMessage(`{"hour":"20","minutes":"8"}`)),
|
|
},
|
|
UnixTime: 1484078931,
|
|
},
|
|
}
|
|
|
|
// Results not saved if DiscardData is true in Query
|
|
discardDataFalse := map[string]*fleet.Query{
|
|
"pack/Global/Uptime": {
|
|
ID: 1,
|
|
DiscardData: true,
|
|
Logging: fleet.LoggingSnapshot,
|
|
},
|
|
}
|
|
serv.saveResultLogsToQueryReports(ctx, results, discardDataFalse, fleet.DefaultMaxQueryReportRows)
|
|
assert.False(t, ds.OverwriteQueryResultRowsFuncInvoked)
|
|
|
|
// Happy Path: Results saved
|
|
discardDataTrue := map[string]*fleet.Query{
|
|
"pack/Global/Uptime": {
|
|
ID: 1,
|
|
DiscardData: false,
|
|
Logging: fleet.LoggingSnapshot,
|
|
},
|
|
}
|
|
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
|
return nil
|
|
}
|
|
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
|
|
return 0, nil
|
|
}
|
|
serv.saveResultLogsToQueryReports(ctx, results, discardDataTrue, fleet.DefaultMaxQueryReportRows)
|
|
require.True(t, ds.OverwriteQueryResultRowsFuncInvoked)
|
|
}
|
|
|
|
func TestSubmitResultLogsToQueryResultsWithEmptySnapShot(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
host := fleet.Host{
|
|
ID: 999,
|
|
}
|
|
ctx = hostctx.NewContext(ctx, &host)
|
|
|
|
logs := []string{
|
|
`{"snapshot":[],"action":"snapshot","name":"pack/Global/query_no_rows","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
|
|
}
|
|
|
|
logJSON := fmt.Sprintf("[%s]", strings.Join(logs, ","))
|
|
var results []json.RawMessage
|
|
err := json.Unmarshal([]byte(logJSON), &results)
|
|
require.NoError(t, err)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{
|
|
ServerSettings: fleet.ServerSettings{
|
|
QueryReportsDisabled: false,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
|
|
return &fleet.Query{
|
|
ID: 1,
|
|
DiscardData: false,
|
|
Logging: fleet.LoggingSnapshot,
|
|
}, nil
|
|
}
|
|
|
|
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
|
require.Len(t, rows, 1)
|
|
require.Equal(t, uint(999), rows[0].HostID)
|
|
require.NotZero(t, rows[0].LastFetched)
|
|
require.Nil(t, rows[0].Data)
|
|
return nil
|
|
}
|
|
|
|
err = svc.SubmitResultLogs(ctx, results)
|
|
require.NoError(t, err)
|
|
assert.True(t, ds.OverwriteQueryResultRowsFuncInvoked)
|
|
}
|
|
|
|
func TestSubmitResultLogsToQueryResultsDoesNotCountNullDataRows(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
host := fleet.Host{
|
|
ID: 999,
|
|
}
|
|
ctx = hostctx.NewContext(ctx, &host)
|
|
|
|
logs := []string{
|
|
`{"snapshot":[],"action":"snapshot","name":"pack/Global/query_no_rows","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
|
|
}
|
|
|
|
logJSON := fmt.Sprintf("[%s]", strings.Join(logs, ","))
|
|
var results []json.RawMessage
|
|
err := json.Unmarshal([]byte(logJSON), &results)
|
|
require.NoError(t, err)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{
|
|
ServerSettings: fleet.ServerSettings{
|
|
QueryReportsDisabled: false,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
|
|
return &fleet.Query{
|
|
ID: 1,
|
|
DiscardData: false,
|
|
Logging: fleet.LoggingSnapshot,
|
|
}, nil
|
|
}
|
|
|
|
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
|
require.Len(t, rows, 1)
|
|
require.Equal(t, uint(999), rows[0].HostID)
|
|
require.NotZero(t, rows[0].LastFetched)
|
|
require.Nil(t, rows[0].Data)
|
|
return nil
|
|
}
|
|
|
|
err = svc.SubmitResultLogs(ctx, results)
|
|
require.NoError(t, err)
|
|
assert.True(t, ds.OverwriteQueryResultRowsFuncInvoked)
|
|
}
|
|
|
|
type failingLogger struct{}
|
|
|
|
func (n *failingLogger) Write(context.Context, []json.RawMessage) error {
|
|
return errors.New("some error")
|
|
}
|
|
|
|
func TestSubmitResultLogsFail(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
host := fleet.Host{
|
|
ID: 999,
|
|
}
|
|
ctx = hostctx.NewContext(ctx, &host)
|
|
|
|
// Hack to get at the service internals and modify the writer
|
|
serv := ((svc.(validationMiddleware)).Service).(*Service)
|
|
|
|
testLogger := &failingLogger{}
|
|
serv.osqueryLogWriter = &OsqueryLogger{Result: testLogger}
|
|
|
|
logs := []string{
|
|
`{"name":"pack/Global/system_info","hostIdentifier":"some_uuid","calendarTime":"Fri Sep 30 17:55:15 2016 UTC","unixTime":1475258115,"decorations":{"host_uuid":"some_uuid","username":"zwass"},"columns":{"cpu_brand":"Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz","hostname":"hostimus","physical_memory":"17179869184"},"action":"added"}`,
|
|
}
|
|
|
|
logJSON := fmt.Sprintf("[%s]", strings.Join(logs, ","))
|
|
var results []json.RawMessage
|
|
err := json.Unmarshal([]byte(logJSON), &results)
|
|
require.NoError(t, err)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
|
|
return &fleet.Query{
|
|
ID: 1,
|
|
DiscardData: false,
|
|
AutomationsEnabled: true,
|
|
Name: name,
|
|
}, nil
|
|
}
|
|
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
|
|
return 0, nil
|
|
}
|
|
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
|
return nil
|
|
}
|
|
|
|
// Expect an error when unable to write to logging destination.
|
|
err = svc.SubmitResultLogs(ctx, results)
|
|
require.Error(t, err)
|
|
assert.Equal(t, http.StatusRequestEntityTooLarge, err.(*endpoint_utils.OsqueryError).Status())
|
|
}
|
|
|
|
func TestGetQueryNameAndTeamIDFromResult(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedID *uint
|
|
expectedName string
|
|
hasErr bool
|
|
}{
|
|
{"pack/Global/Query Name", nil, "Query Name", false}, // valid global query
|
|
{"pack/team-1/Query Name", ptr.Uint(1), "Query Name", false}, // valid team query
|
|
{"pack/team-12345/Another Query", ptr.Uint(12345), "Another Query", false}, // valid team query
|
|
{"pack/team-foo/Query", nil, "", true}, // missing team ID
|
|
{"pack/Global/QueryWith/Slash", nil, "QueryWith/Slash", false}, // query name contains forward slash
|
|
{"packGlobalGlobalGlobalGlobal", nil, "Global", false}, // pack_delimiter=Global
|
|
{"packXGlobalGlobalXGlobalQueryWith/Slash", nil, "QueryWith/Slash", false}, // pack_delimiter=XGlobal
|
|
{"pack//Global//QueryWith/Slash", nil, "QueryWith/Slash", false}, // pack_delimiter=//
|
|
{"pack/team-1/QueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false},
|
|
{"pack_team-1_QueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false},
|
|
{"packFOOBARteam-1FOOBARQueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false}, // pack_delimiter=FOOBAR
|
|
{"pack123😁123team-1123😁123QueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false}, // pack_delimiter=123😁123
|
|
{"pack(foo)team-1(foo)fo(o)bar", ptr.Uint(1), "fo(o)bar", false}, // pack_delimiter=(foo)
|
|
{"packteam-1team-1team-1team-1", ptr.Uint(1), "team-1", false}, // pack_delimiter=team-1
|
|
{"pack/Global/GlobalInQueryName", nil, "GlobalInQueryName", false}, // query name contains Global
|
|
{"pack/team-1/team-1InQueryName", ptr.Uint(1), "team-1InQueryName", false}, // query name contains team-1
|
|
|
|
{"InvalidString", nil, "", true},
|
|
{"Invalid/Query", nil, "", true},
|
|
{"pac", nil, "", true},
|
|
{"pack", nil, "", true},
|
|
{"pack/", nil, "", true},
|
|
{"pack/Global", nil, "", true},
|
|
{"pack/Global/", nil, "", true},
|
|
{"pack/team/foo", nil, "", true},
|
|
{"pack/team-123", nil, "", true},
|
|
{"pack/team-/foo", nil, "", true},
|
|
{"pack/team-123/", nil, "", true},
|
|
|
|
// Legacy 2017 packs should fail the parsing as they are separate
|
|
// from global or team queries.
|
|
{"pack/PackName/Query", nil, "", true},
|
|
{"pack/PackName/QueryWith/Slash", nil, "", true},
|
|
{"packFOOBARPackNameFOOBARQueryWith/Slash", nil, "", true}, // pack_delimiter=FOOBAR
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
id, str, err := getQueryNameAndTeamIDFromResult(tt.input)
|
|
assert.Equal(t, tt.expectedID, id)
|
|
assert.Equal(t, tt.expectedName, str)
|
|
if tt.hasErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetMostRecentResults(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []*fleet.ScheduledQueryResult
|
|
expected []*fleet.ScheduledQueryResult
|
|
}{
|
|
{
|
|
name: "basic test",
|
|
input: []*fleet.ScheduledQueryResult{
|
|
{QueryName: "test1", UnixTime: 1},
|
|
{QueryName: "test1", UnixTime: 2},
|
|
{QueryName: "test1", UnixTime: 3},
|
|
{QueryName: "test2", UnixTime: 1},
|
|
{QueryName: "test2", UnixTime: 2},
|
|
{QueryName: "test2", UnixTime: 3},
|
|
},
|
|
expected: []*fleet.ScheduledQueryResult{
|
|
{QueryName: "test1", UnixTime: 3},
|
|
{QueryName: "test2", UnixTime: 3},
|
|
},
|
|
},
|
|
{
|
|
name: "out of order test",
|
|
input: []*fleet.ScheduledQueryResult{
|
|
{QueryName: "test1", UnixTime: 2},
|
|
{QueryName: "test1", UnixTime: 3},
|
|
{QueryName: "test1", UnixTime: 1},
|
|
{QueryName: "test2", UnixTime: 3},
|
|
{QueryName: "test2", UnixTime: 2},
|
|
{QueryName: "test2", UnixTime: 1},
|
|
},
|
|
expected: []*fleet.ScheduledQueryResult{
|
|
{QueryName: "test1", UnixTime: 3},
|
|
{QueryName: "test2", UnixTime: 3},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
results := getMostRecentResults(tt.input)
|
|
assert.ElementsMatch(t, tt.expected, results)
|
|
})
|
|
}
|
|
}
|
|
|
|
func verifyDiscovery(t *testing.T, queries, discovery map[string]string) {
|
|
t.Helper()
|
|
assert.Equal(t, len(queries), len(discovery))
|
|
// discoveryUsed holds the queries where we know use the distributed discovery feature.
|
|
discoveryUsed := map[string]struct{}{
|
|
hostDetailQueryPrefix + "google_chrome_profiles": {},
|
|
hostDetailQueryPrefix + "mdm": {},
|
|
hostDetailQueryPrefix + "munki_info": {},
|
|
hostDetailQueryPrefix + "windows_update_history": {},
|
|
hostDetailQueryPrefix + "kubequery_info": {},
|
|
hostDetailQueryPrefix + "orbit_info": {},
|
|
hostDetailQueryPrefix + "software_vscode_extensions": {},
|
|
hostDetailQueryPrefix + "software_python_packages": {},
|
|
hostDetailQueryPrefix + "software_python_packages_with_users_dir": {},
|
|
hostDetailQueryPrefix + "software_macos_firefox": {},
|
|
hostDetailQueryPrefix + "battery": {},
|
|
hostDetailQueryPrefix + "software_macos_codesign": {},
|
|
}
|
|
for name := range queries {
|
|
require.NotEmpty(t, discovery[name])
|
|
if _, ok := discoveryUsed[name]; ok {
|
|
require.NotEqual(t, alwaysTrueQuery, discovery[name])
|
|
} else {
|
|
require.Equal(t, alwaysTrueQuery, discovery[name])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHostDetailQueries(t *testing.T) {
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
additional := json.RawMessage(`{"foobar": "select foo", "bim": "bam"}`)
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: fleet.Features{
|
|
AdditionalQueries: &additional,
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
}}, nil
|
|
}
|
|
|
|
mockClock := clock.NewMockClock()
|
|
host := fleet.Host{
|
|
ID: 1,
|
|
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
|
|
UpdateTimestamp: fleet.UpdateTimestamp{
|
|
UpdatedAt: mockClock.Now(),
|
|
},
|
|
CreateTimestamp: fleet.CreateTimestamp{
|
|
CreatedAt: mockClock.Now(),
|
|
},
|
|
},
|
|
|
|
Platform: "darwin",
|
|
DetailUpdatedAt: mockClock.Now(),
|
|
NodeKey: ptr.String("test_key"),
|
|
Hostname: "test_hostname",
|
|
UUID: "test_uuid",
|
|
}
|
|
|
|
svc := &Service{
|
|
clock: mockClock,
|
|
logger: log.NewNopLogger(),
|
|
config: config.TestConfig(),
|
|
ds: ds,
|
|
jitterMu: new(sync.Mutex),
|
|
jitterH: make(map[time.Duration]*jitterHashTable),
|
|
}
|
|
|
|
// detail_updated_at is now, so nothing gets returned by default
|
|
queries, discovery, err := svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, queries)
|
|
verifyDiscovery(t, queries, discovery)
|
|
|
|
// With refetch requested detail queries should be returned
|
|
host.RefetchRequested = true
|
|
queries, discovery, err = svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
// +2: additional queries: bim, foobar
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+2, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
host.RefetchRequested = false
|
|
|
|
// Advance the time
|
|
mockClock.AddTime(1*time.Hour + 1*time.Minute)
|
|
|
|
// all queries returned now that detail udpated at is in the past
|
|
queries, discovery, err = svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
// +2: additional queries: bim, foobar
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+2, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
for name := range queries {
|
|
assert.True(t,
|
|
strings.HasPrefix(name, hostDetailQueryPrefix) || strings.HasPrefix(name, hostAdditionalQueryPrefix),
|
|
)
|
|
}
|
|
assert.Equal(t, "bam", queries[hostAdditionalQueryPrefix+"bim"])
|
|
assert.Equal(t, "select foo", queries[hostAdditionalQueryPrefix+"foobar"])
|
|
|
|
host.DetailUpdatedAt = mockClock.Now()
|
|
|
|
// detail_updated_at is now, so nothing gets returned
|
|
queries, discovery, err = svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, queries)
|
|
verifyDiscovery(t, queries, discovery)
|
|
|
|
// setting refetch_critical_queries_until in the past still returns nothing
|
|
host.RefetchCriticalQueriesUntil = ptr.Time(mockClock.Now().Add(-1 * time.Minute))
|
|
queries, discovery, err = svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, queries)
|
|
verifyDiscovery(t, queries, discovery)
|
|
|
|
// setting refetch_critical_queries_until in the future returns only the critical queries
|
|
host.RefetchCriticalQueriesUntil = ptr.Time(mockClock.Now().Add(1 * time.Minute))
|
|
queries, discovery, err = svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
// host is darwin so it gets only the darwin critical query
|
|
require.Equal(t, 1, len(queries), distQueriesMapKeys(queries))
|
|
for name := range criticalDetailQueries {
|
|
if strings.HasSuffix(name, "_windows") {
|
|
continue
|
|
}
|
|
assert.Contains(t, queries, hostDetailQueryPrefix+name)
|
|
}
|
|
verifyDiscovery(t, queries, discovery)
|
|
}
|
|
|
|
func TestQueriesAndHostFeatures(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
team1 := fleet.Team{
|
|
ID: 1,
|
|
Config: fleet.TeamConfig{
|
|
Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
team2 := fleet.Team{
|
|
ID: 2,
|
|
Config: fleet.TeamConfig{
|
|
Features: fleet.Features{
|
|
EnableHostUsers: false,
|
|
EnableSoftwareInventory: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
host := fleet.Host{
|
|
ID: 1,
|
|
Platform: "darwin",
|
|
NodeKey: ptr.String("test_key"),
|
|
Hostname: "test_hostname",
|
|
UUID: "test_uuid",
|
|
TeamID: nil,
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{
|
|
Features: fleet.Features{
|
|
EnableHostUsers: false,
|
|
EnableSoftwareInventory: false,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
ds.TeamFeaturesFunc = func(ctx context.Context, id uint) (*fleet.Features, error) {
|
|
switch id {
|
|
case uint(1):
|
|
return &team1.Config.Features, nil
|
|
case uint(2):
|
|
return &team2.Config.Features, nil
|
|
default:
|
|
return nil, errors.New("team not found")
|
|
}
|
|
}
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostuuid string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
lq := live_query_mock.New(t)
|
|
lq.On("QueriesForHost", uint(1)).Return(map[string]string{}, nil)
|
|
lq.On("QueriesForHost", uint(2)).Return(map[string]string{}, nil)
|
|
lq.On("QueriesForHost", nil).Return(map[string]string{}, nil)
|
|
|
|
t.Run("free license", func(t *testing.T) {
|
|
license := &fleet.LicenseInfo{Tier: fleet.TierFree}
|
|
svc, ctx := newTestService(t, ds, nil, lq, &TestServerOpts{License: license})
|
|
|
|
ctx = hostctx.NewContext(ctx, &host)
|
|
queries, _, _, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.NotContains(t, queries, "fleet_detail_query_users")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_macos")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_linux")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_windows")
|
|
|
|
// assign team 1 to host
|
|
host.TeamID = &team1.ID
|
|
queries, _, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.NotContains(t, queries, "fleet_detail_query_users")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_macos")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_linux")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_windows")
|
|
|
|
// assign team 2 to host
|
|
host.TeamID = &team2.ID
|
|
queries, _, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.NotContains(t, queries, "fleet_detail_query_users")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_macos")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_linux")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_windows")
|
|
})
|
|
|
|
t.Run("premium license", func(t *testing.T) {
|
|
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
|
svc, ctx := newTestService(t, ds, nil, lq, &TestServerOpts{License: license})
|
|
|
|
host.TeamID = nil
|
|
ctx = hostctx.NewContext(ctx, &host)
|
|
queries, _, _, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.NotContains(t, queries, "fleet_detail_query_users")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_macos")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_linux")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_windows")
|
|
|
|
// assign team 1 to host
|
|
host.TeamID = &team1.ID
|
|
queries, _, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Contains(t, queries, "fleet_detail_query_users")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_macos")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_linux")
|
|
require.NotContains(t, queries, "fleet_detail_query_software_windows")
|
|
|
|
// assign team 2 to host
|
|
host.TeamID = &team2.ID
|
|
queries, _, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.NotContains(t, queries, "fleet_detail_query_users")
|
|
require.Contains(t, queries, "fleet_detail_query_software_macos")
|
|
})
|
|
}
|
|
|
|
func TestGetDistributedQueriesMissingHost(t *testing.T) {
|
|
svc, ctx := newTestService(t, &mock.Store{}, nil, nil)
|
|
|
|
_, _, _, err := svc.GetDistributedQueries(ctx)
|
|
require.NotNil(t, err)
|
|
assert.Contains(t, err.Error(), "missing host")
|
|
}
|
|
|
|
func TestGetDistributedQueriesEmptyQuery(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
lq := live_query_mock.New(t)
|
|
svc, ctx := newTestServiceWithClock(t, ds, nil, lq, mockClock)
|
|
|
|
host := &fleet.Host{
|
|
Platform: "darwin",
|
|
}
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{"empty_label_query": ""}, nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
return host, nil
|
|
}
|
|
ds.UpdateHostFunc = func(ctx context.Context, gotHost *fleet.Host) error {
|
|
return nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, nil
|
|
}
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{"empty_policy_query": ""}, nil
|
|
}
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostuuid string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
lq.On("QueriesForHost", uint(0)).Return(map[string]string{"empty_live_query": ""}, nil)
|
|
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, _, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, queries)
|
|
for n, q := range queries {
|
|
require.NotEmpty(t, q, n)
|
|
}
|
|
for n, q := range discovery {
|
|
require.NotEmpty(t, q, n)
|
|
}
|
|
}
|
|
|
|
func TestLabelQueries(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
lq := live_query_mock.New(t)
|
|
svc, ctx := newTestServiceWithClock(t, ds, nil, lq, mockClock)
|
|
|
|
host := &fleet.Host{
|
|
Platform: "darwin",
|
|
}
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
return host, nil
|
|
}
|
|
ds.UpdateHostFunc = func(ctx context.Context, gotHost *fleet.Host) error {
|
|
host = gotHost
|
|
return nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
}}, nil
|
|
}
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostuuid string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
|
|
lq.On("QueriesForHost", uint(0)).Return(map[string]string{}, nil)
|
|
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
// With a new host, we should get the detail queries (and accelerate
|
|
// should be turned on so that we can quickly fill labels)
|
|
queries, discovery, acc, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +1 for the fleet_no_policies_wildcard query.
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+1, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.NotZero(t, acc)
|
|
|
|
// Simulate the detail queries being added.
|
|
host.DetailUpdatedAt = mockClock.Now().Add(-1 * time.Minute)
|
|
host.Hostname = "zwass.local"
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, queries, 1) // fleet_no_policies_wildcard query
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.Zero(t, acc)
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{
|
|
"label1": "query1",
|
|
"label2": "query2",
|
|
"label3": "query3",
|
|
}, nil
|
|
}
|
|
|
|
// Now we should get the label queries
|
|
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +1 for the fleet_no_policies_wildcard query.
|
|
require.Len(t, queries, 3+1)
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.Zero(t, acc)
|
|
|
|
var gotHost *fleet.Host
|
|
var gotResults map[uint]*bool
|
|
var gotTime time.Time
|
|
ds.RecordLabelQueryExecutionsFunc = func(ctx context.Context, host *fleet.Host, results map[uint]*bool, t time.Time, deferred bool) error {
|
|
gotHost = host
|
|
gotResults = results
|
|
gotTime = t
|
|
return nil
|
|
}
|
|
|
|
// Record a query execution
|
|
err = svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostLabelQueryPrefix + "1": {{"col1": "val1"}},
|
|
},
|
|
map[string]fleet.OsqueryStatus{},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
host.LabelUpdatedAt = mockClock.Now()
|
|
assert.Equal(t, host, gotHost)
|
|
assert.Equal(t, mockClock.Now(), gotTime)
|
|
require.Len(t, gotResults, 1)
|
|
assert.Equal(t, true, *gotResults[1])
|
|
|
|
mockClock.AddTime(1 * time.Second)
|
|
|
|
// Record a query execution
|
|
err = svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostLabelQueryPrefix + "2": {{"col1": "val1"}},
|
|
hostLabelQueryPrefix + "3": {},
|
|
},
|
|
map[string]fleet.OsqueryStatus{},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
host.LabelUpdatedAt = mockClock.Now()
|
|
assert.Equal(t, host, gotHost)
|
|
assert.Equal(t, mockClock.Now(), gotTime)
|
|
require.Len(t, gotResults, 2)
|
|
assert.Equal(t, true, *gotResults[2])
|
|
assert.Equal(t, false, *gotResults[3])
|
|
|
|
// We should get no labels now.
|
|
host.LabelUpdatedAt = mockClock.Now()
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, queries, 1) // fleet_no_policies_wildcard query
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.Zero(t, acc)
|
|
|
|
// With refetch requested details+label queries should be returned.
|
|
host.RefetchRequested = true
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +3 for label queries, +1 for the fleet_no_policies_wildcard query.
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+3+1, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.Zero(t, acc)
|
|
|
|
// Record a query execution
|
|
err = svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostLabelQueryPrefix + "2": {{"col1": "val1"}},
|
|
hostLabelQueryPrefix + "3": {},
|
|
},
|
|
map[string]fleet.OsqueryStatus{},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
host.LabelUpdatedAt = mockClock.Now()
|
|
assert.Equal(t, host, gotHost)
|
|
assert.Equal(t, mockClock.Now(), gotTime)
|
|
require.Len(t, gotResults, 2)
|
|
assert.Equal(t, true, *gotResults[2])
|
|
assert.Equal(t, false, *gotResults[3])
|
|
|
|
// SubmitDistributedQueryResults will set RefetchRequested to false.
|
|
require.False(t, host.RefetchRequested)
|
|
|
|
// There shouldn't be any labels now.
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, queries, 1) // fleet_no_policies_wildcard query
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.Zero(t, acc)
|
|
}
|
|
|
|
func TestDetailQueriesWithEmptyStrings(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
mockClock := clock.NewMockClock()
|
|
lq := live_query_mock.New(t)
|
|
svc, ctx := newTestServiceWithClock(t, ds, nil, lq, mockClock)
|
|
|
|
host := &fleet.Host{
|
|
ID: 1,
|
|
Platform: "windows",
|
|
}
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
}}, nil
|
|
}
|
|
ds.LabelQueriesForHostFunc = func(context.Context, *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostuuid string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
if id != 1 {
|
|
return nil, errors.New("not found")
|
|
}
|
|
return host, nil
|
|
}
|
|
|
|
lq.On("QueriesForHost", host.ID).Return(map[string]string{}, nil)
|
|
|
|
// With a new host, we should get the detail queries (and accelerated
|
|
// queries)
|
|
queries, discovery, acc, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +1 due to 'windows_update_history', +1 due to fleet_no_policies_wildcard query.
|
|
if expected := expectedDetailQueriesForPlatform(host.Platform); !assert.Equal(t, len(expected)+1+1, len(queries)) {
|
|
// this is just to print the diff between the expected and actual query
|
|
// keys when the count assertion fails, to help debugging - they are not
|
|
// expected to match.
|
|
require.ElementsMatch(t, osqueryMapKeys(expected), distQueriesMapKeys(queries))
|
|
}
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.NotZero(t, acc)
|
|
|
|
resultJSON := `
|
|
{
|
|
"fleet_detail_query_network_interface_windows": [
|
|
{
|
|
"address": "192.168.0.1",
|
|
"mac": "5f:3d:4b:10:25:82"
|
|
}
|
|
],
|
|
"fleet_detail_query_os_version": [
|
|
{
|
|
"platform": "darwin",
|
|
"build": "15G1004",
|
|
"major": "10",
|
|
"minor": "10",
|
|
"name": "Mac OS X",
|
|
"patch": "6"
|
|
}
|
|
],
|
|
"fleet_detail_query_osquery_info": [
|
|
{
|
|
"build_distro": "10.10",
|
|
"build_platform": "darwin",
|
|
"config_hash": "3c6e4537c4d0eb71a7c6dda19d",
|
|
"config_valid": "1",
|
|
"extensions": "active",
|
|
"pid": "38113",
|
|
"start_time": "1475603155",
|
|
"version": "1.8.2",
|
|
"watcher": "38112"
|
|
}
|
|
],
|
|
"fleet_detail_query_system_info": [
|
|
{
|
|
"computer_name": "computer",
|
|
"cpu_brand": "Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz",
|
|
"cpu_logical_cores": "8",
|
|
"cpu_physical_cores": "4",
|
|
"cpu_subtype": "Intel x86-64h Haswell",
|
|
"cpu_type": "x86_64h",
|
|
"hardware_model": "MacBookPro11,4",
|
|
"hardware_serial": "NEW_HW_SRL",
|
|
"hardware_vendor": "Apple Inc.",
|
|
"hardware_version": "1.0",
|
|
"hostname": "computer.local",
|
|
"physical_memory": "17179869184",
|
|
"uuid": "uuid"
|
|
}
|
|
],
|
|
"fleet_detail_query_uptime": [
|
|
{
|
|
"days": "20",
|
|
"hours": "0",
|
|
"minutes": "48",
|
|
"seconds": "13",
|
|
"total_seconds": "1730893"
|
|
}
|
|
],
|
|
"fleet_detail_query_osquery_flags": [
|
|
{
|
|
"name": "config_tls_refresh",
|
|
"value": ""
|
|
},
|
|
{
|
|
"name": "distributed_interval",
|
|
"value": ""
|
|
},
|
|
{
|
|
"name": "logger_tls_period",
|
|
"value": ""
|
|
}
|
|
],
|
|
"fleet_detail_query_orbit_info": [
|
|
{
|
|
"name": "version",
|
|
"value": "42"
|
|
},
|
|
{
|
|
"name": "device_auth_token",
|
|
"value": "foo"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
var results fleet.OsqueryDistributedQueryResults
|
|
err = json.Unmarshal([]byte(resultJSON), &results)
|
|
require.NoError(t, err)
|
|
|
|
var gotHost *fleet.Host
|
|
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
|
gotHost = host
|
|
return nil
|
|
}
|
|
|
|
// Verify that results are ingested properly
|
|
require.NoError(
|
|
t, svc.SubmitDistributedQueryResults(ctx, results, map[string]fleet.OsqueryStatus{}, map[string]string{}, map[string]*fleet.Stats{}),
|
|
)
|
|
|
|
// osquery_info
|
|
assert.Equal(t, "darwin", gotHost.Platform)
|
|
assert.Equal(t, "1.8.2", gotHost.OsqueryVersion)
|
|
|
|
// system_info
|
|
assert.Equal(t, int64(17179869184), gotHost.Memory)
|
|
assert.Equal(t, "computer.local", gotHost.Hostname)
|
|
assert.Equal(t, "uuid", gotHost.UUID)
|
|
assert.Equal(t, "NEW_HW_SRL", gotHost.HardwareSerial)
|
|
|
|
// os_version
|
|
assert.Equal(t, "Mac OS X 10.10.6", gotHost.OSVersion)
|
|
|
|
// uptime
|
|
assert.Equal(t, 1730893*time.Second, gotHost.Uptime)
|
|
|
|
// osquery_flags
|
|
assert.Equal(t, uint(0), gotHost.ConfigTLSRefresh)
|
|
assert.Equal(t, uint(0), gotHost.DistributedInterval)
|
|
assert.Equal(t, uint(0), gotHost.LoggerTLSPeriod)
|
|
|
|
host.Hostname = "computer.local"
|
|
host.DetailUpdatedAt = mockClock.Now()
|
|
mockClock.AddTime(1 * time.Minute)
|
|
|
|
// Now no detail queries should be required
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, queries, 1) // fleet_no_policies_wildcard query
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.Zero(t, acc)
|
|
|
|
// Advance clock and queries should exist again
|
|
mockClock.AddTime(1*time.Hour + 1*time.Minute)
|
|
|
|
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// somehow confusingly, the query response above changed the host's platform
|
|
// from windows to darwin
|
|
// +1 due to fleet_no_policies_wildcard query.
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(gotHost.Platform))+1, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.Zero(t, acc)
|
|
}
|
|
|
|
func TestDetailQueries(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
mockClock := clock.NewMockClock()
|
|
lq := live_query_mock.New(t)
|
|
svc, ctx := newTestServiceWithClock(t, ds, nil, lq, mockClock)
|
|
|
|
host := &fleet.Host{
|
|
ID: 1,
|
|
Platform: "linux",
|
|
HardwareSerial: "HW_SRL",
|
|
}
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
lq.On("QueriesForHost", host.ID).Return(map[string]string{}, nil)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
}}, nil
|
|
}
|
|
ds.LabelQueriesForHostFunc = func(context.Context, *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.SetOrUpdateMDMDataFunc = func(ctx context.Context, hostID uint, isServer, enrolled bool, serverURL string, installedFromDep bool, name string, fleetEnrollmentRef string) error {
|
|
require.True(t, enrolled)
|
|
require.False(t, installedFromDep)
|
|
require.Equal(t, "hi.com", serverURL)
|
|
require.Empty(t, fleetEnrollmentRef)
|
|
return nil
|
|
}
|
|
ds.SetOrUpdateMunkiInfoFunc = func(ctx context.Context, hostID uint, version string, errs, warns []string) error {
|
|
require.Equal(t, "3.4.5", version)
|
|
return nil
|
|
}
|
|
ds.SetOrUpdateHostOrbitInfoFunc = func(
|
|
ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool,
|
|
) error {
|
|
require.Equal(t, "42", version)
|
|
require.Equal(t, sql.NullString{String: "1.2.3", Valid: true}, desktopVersion)
|
|
require.Equal(t, sql.NullBool{Bool: true, Valid: true}, scriptsEnabled)
|
|
return nil
|
|
}
|
|
ds.SetOrUpdateDeviceAuthTokenFunc = func(ctx context.Context, hostID uint, authToken string) error {
|
|
require.Equal(t, uint(1), hostID)
|
|
require.Equal(t, "foo", authToken)
|
|
return nil
|
|
}
|
|
ds.SetOrUpdateHostDisksSpaceFunc = func(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error {
|
|
require.Equal(t, 277.0, gigsAvailable)
|
|
require.Equal(t, 56.0, percentAvailable)
|
|
require.Equal(t, 500.1, gigsTotal)
|
|
return nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
if id != 1 {
|
|
return nil, errors.New("not found")
|
|
}
|
|
return host, nil
|
|
}
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostuuid string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
// With a new host, we should get the detail queries (and accelerated
|
|
// queries)
|
|
queries, discovery, acc, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +1 for fleet_no_policies_wildcard
|
|
if expected := expectedDetailQueriesForPlatform(host.Platform); !assert.Equal(t, len(expected)+1, len(queries)) {
|
|
// this is just to print the diff between the expected and actual query
|
|
// keys when the count assertion fails, to help debugging - they are not
|
|
// expected to match.
|
|
require.ElementsMatch(t, osqueryMapKeys(expected), distQueriesMapKeys(queries))
|
|
}
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.NotZero(t, acc)
|
|
|
|
resultJSON := `
|
|
{
|
|
"fleet_detail_query_network_interface": [
|
|
{
|
|
"address": "192.168.0.1",
|
|
"broadcast": "192.168.0.255",
|
|
"ibytes": "1601207629",
|
|
"ierrors": "314179",
|
|
"interface": "en0",
|
|
"ipackets": "25698094",
|
|
"last_change": "1474233476",
|
|
"mac": "5f:3d:4b:10:25:82",
|
|
"mask": "255.255.255.0",
|
|
"metric": "1",
|
|
"mtu": "1453",
|
|
"obytes": "2607283152",
|
|
"oerrors": "101010",
|
|
"opackets": "12264603",
|
|
"point_to_point": "",
|
|
"type": "6"
|
|
}
|
|
],
|
|
"fleet_detail_query_os_version": [
|
|
{
|
|
"platform": "darwin",
|
|
"build": "15G1004",
|
|
"major": "10",
|
|
"minor": "10",
|
|
"name": "Mac OS X",
|
|
"patch": "6"
|
|
}
|
|
],
|
|
"fleet_detail_query_osquery_info": [
|
|
{
|
|
"build_distro": "10.10",
|
|
"build_platform": "darwin",
|
|
"config_hash": "3c6e4537c4d0eb71a7c6dda19d",
|
|
"config_valid": "1",
|
|
"extensions": "active",
|
|
"pid": "38113",
|
|
"start_time": "1475603155",
|
|
"version": "1.8.2",
|
|
"watcher": "38112"
|
|
}
|
|
],
|
|
"fleet_detail_query_system_info": [
|
|
{
|
|
"computer_name": "computer",
|
|
"cpu_brand": "Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz",
|
|
"cpu_logical_cores": "8",
|
|
"cpu_physical_cores": "4",
|
|
"cpu_subtype": "Intel x86-64h Haswell",
|
|
"cpu_type": "x86_64h",
|
|
"hardware_model": "MacBookPro11,4",
|
|
"hardware_serial": "-1",
|
|
"hardware_vendor": "Apple Inc.",
|
|
"hardware_version": "1.0",
|
|
"hostname": "computer.local",
|
|
"physical_memory": "17179869184",
|
|
"uuid": "uuid"
|
|
}
|
|
],
|
|
"fleet_detail_query_uptime": [
|
|
{
|
|
"days": "20",
|
|
"hours": "0",
|
|
"minutes": "48",
|
|
"seconds": "13",
|
|
"total_seconds": "1730893"
|
|
}
|
|
],
|
|
"fleet_detail_query_osquery_flags": [
|
|
{
|
|
"name":"config_tls_refresh",
|
|
"value":"10"
|
|
},
|
|
{
|
|
"name":"config_refresh",
|
|
"value":"9"
|
|
},
|
|
{
|
|
"name":"distributed_interval",
|
|
"value":"5"
|
|
},
|
|
{
|
|
"name":"logger_tls_period",
|
|
"value":"60"
|
|
}
|
|
],
|
|
"fleet_detail_query_users": [
|
|
{
|
|
"uid": "1234",
|
|
"username": "user1",
|
|
"type": "sometype",
|
|
"groupname": "somegroup",
|
|
"shell": "someloginshell"
|
|
},
|
|
{
|
|
"uid": "5678",
|
|
"username": "user2",
|
|
"type": "sometype",
|
|
"groupname": "somegroup"
|
|
}
|
|
],
|
|
"fleet_detail_query_software_macos": [
|
|
{
|
|
"name": "app1",
|
|
"version": "1.0.0",
|
|
"source": "source1"
|
|
},
|
|
{
|
|
"name": "app2",
|
|
"version": "1.0.0",
|
|
"source": "source2",
|
|
"bundle_identifier": "somebundle"
|
|
}
|
|
],
|
|
"fleet_detail_query_disk_space_unix": [
|
|
{
|
|
"percent_disk_space_available": "56",
|
|
"gigs_disk_space_available": "277.0",
|
|
"gigs_total_disk_space": "500.1"
|
|
}
|
|
],
|
|
"fleet_detail_query_mdm": [
|
|
{
|
|
"enrolled": "true",
|
|
"server_url": "hi.com",
|
|
"installed_from_dep": "false"
|
|
}
|
|
],
|
|
"fleet_detail_query_munki_info": [
|
|
{
|
|
"version": "3.4.5"
|
|
}
|
|
],
|
|
"fleet_detail_query_orbit_info": [
|
|
{
|
|
"version": "42",
|
|
"desktop_version": "1.2.3",
|
|
"scripts_enabled": "1"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
var results fleet.OsqueryDistributedQueryResults
|
|
err = json.Unmarshal([]byte(resultJSON), &results)
|
|
require.NoError(t, err)
|
|
|
|
var gotHost *fleet.Host
|
|
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
|
gotHost = host
|
|
return nil
|
|
}
|
|
var gotUsers []fleet.HostUser
|
|
ds.SaveHostUsersFunc = func(ctx context.Context, hostID uint, users []fleet.HostUser) error {
|
|
if hostID != 1 {
|
|
return errors.New("not found")
|
|
}
|
|
gotUsers = users
|
|
return nil
|
|
}
|
|
var gotSoftware []fleet.Software
|
|
ds.UpdateHostSoftwareFunc = func(ctx context.Context, hostID uint, software []fleet.Software) (*fleet.UpdateHostSoftwareDBResult, error) {
|
|
if hostID != 1 {
|
|
return nil, errors.New("not found")
|
|
}
|
|
gotSoftware = software
|
|
return nil, nil
|
|
}
|
|
|
|
ds.UpdateHostSoftwareInstalledPathsFunc = func(ctx context.Context, hostID uint, paths map[string]struct{}, result *fleet.UpdateHostSoftwareDBResult) error {
|
|
return nil
|
|
}
|
|
|
|
// Verify that results are ingested properly
|
|
require.NoError(
|
|
t, svc.SubmitDistributedQueryResults(ctx, results, map[string]fleet.OsqueryStatus{}, map[string]string{}, map[string]*fleet.Stats{}),
|
|
)
|
|
require.NotNil(t, gotHost)
|
|
|
|
require.True(t, ds.SetOrUpdateMDMDataFuncInvoked)
|
|
require.True(t, ds.SetOrUpdateMunkiInfoFuncInvoked)
|
|
require.True(t, ds.SetOrUpdateHostDisksSpaceFuncInvoked)
|
|
|
|
// osquery_info
|
|
assert.Equal(t, "darwin", gotHost.Platform)
|
|
assert.Equal(t, "1.8.2", gotHost.OsqueryVersion)
|
|
|
|
// system_info
|
|
assert.Equal(t, int64(17179869184), gotHost.Memory)
|
|
assert.Equal(t, "computer.local", gotHost.Hostname)
|
|
assert.Equal(t, "uuid", gotHost.UUID)
|
|
// The hardware serial should not have updated because return value was -1. See: https://github.com/fleetdm/fleet/issues/19789
|
|
assert.Equal(t, "HW_SRL", gotHost.HardwareSerial)
|
|
|
|
// os_version
|
|
assert.Equal(t, "Mac OS X 10.10.6", gotHost.OSVersion)
|
|
|
|
// uptime
|
|
assert.Equal(t, 1730893*time.Second, gotHost.Uptime)
|
|
|
|
// osquery_flags
|
|
assert.Equal(t, uint(10), gotHost.ConfigTLSRefresh)
|
|
assert.Equal(t, uint(5), gotHost.DistributedInterval)
|
|
assert.Equal(t, uint(60), gotHost.LoggerTLSPeriod)
|
|
|
|
// users
|
|
require.Len(t, gotUsers, 2)
|
|
assert.Equal(t, fleet.HostUser{
|
|
Uid: 1234,
|
|
Username: "user1",
|
|
Type: "sometype",
|
|
GroupName: "somegroup",
|
|
Shell: "someloginshell",
|
|
}, gotUsers[0])
|
|
assert.Equal(t, fleet.HostUser{
|
|
Uid: 5678,
|
|
Username: "user2",
|
|
Type: "sometype",
|
|
GroupName: "somegroup",
|
|
Shell: "",
|
|
}, gotUsers[1])
|
|
|
|
// software
|
|
require.Len(t, gotSoftware, 2)
|
|
assert.Equal(t, []fleet.Software{
|
|
{
|
|
Name: "app1",
|
|
Version: "1.0.0",
|
|
Source: "source1",
|
|
},
|
|
{
|
|
Name: "app2",
|
|
Version: "1.0.0",
|
|
BundleIdentifier: "somebundle",
|
|
Source: "source2",
|
|
},
|
|
}, gotSoftware)
|
|
|
|
host.Hostname = "computer.local"
|
|
host.Platform = "darwin"
|
|
host.DetailUpdatedAt = mockClock.Now()
|
|
mockClock.AddTime(1 * time.Minute)
|
|
|
|
// Now no detail queries should be required
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, queries, 1) // fleet_no_policies_wildcard query
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.Zero(t, acc)
|
|
|
|
// Advance clock and queries should exist again
|
|
mockClock.AddTime(1*time.Hour + 1*time.Minute)
|
|
|
|
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +1 fleet_no_policies_wildcard query
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+1, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
assert.Zero(t, acc)
|
|
}
|
|
|
|
func TestMDMQueries(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc := &Service{
|
|
clock: clock.NewMockClock(),
|
|
logger: log.NewNopLogger(),
|
|
config: config.TestConfig(),
|
|
ds: ds,
|
|
jitterMu: new(sync.Mutex),
|
|
jitterH: make(map[time.Duration]*jitterHashTable),
|
|
}
|
|
|
|
expectedMDMQueries := []struct {
|
|
name string
|
|
discoveryTable string
|
|
}{
|
|
{"fleet_detail_query_mdm_config_profiles_darwin", "macos_profiles"},
|
|
{"fleet_detail_query_mdm_disk_encryption_key_file_darwin", "filevault_prk"},
|
|
{"fleet_detail_query_mdm_disk_encryption_key_file_lines_darwin", "file_lines"},
|
|
}
|
|
|
|
mdmEnabled := true
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: mdmEnabled}}, nil
|
|
}
|
|
ds.GetNanoMDMUserEnrollmentUsernameFunc = func(ctx context.Context, deviceID string) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
host := fleet.Host{
|
|
ID: 1,
|
|
Platform: "darwin",
|
|
NodeKey: ptr.String("test_key"),
|
|
Hostname: "test_hostname",
|
|
UUID: "test_uuid",
|
|
TeamID: nil,
|
|
}
|
|
ctx := hostctx.NewContext(context.Background(), &host)
|
|
|
|
// MDM enabled, darwin
|
|
queries, discovery, err := svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
for _, q := range expectedMDMQueries {
|
|
require.Contains(t, queries, q.name)
|
|
d, ok := discovery[q.name]
|
|
require.True(t, ok)
|
|
require.Contains(t, d, fmt.Sprintf("name = '%s'", q.discoveryTable))
|
|
}
|
|
|
|
// MDM disabled, darwin
|
|
mdmEnabled = false
|
|
queries, discovery, err = svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
for _, q := range expectedMDMQueries {
|
|
require.NotContains(t, queries, q.name)
|
|
require.NotContains(t, discovery, q.name)
|
|
}
|
|
|
|
// MDM enabled, not darwin
|
|
mdmEnabled = true
|
|
host.Platform = "windows"
|
|
ctx = hostctx.NewContext(context.Background(), &host)
|
|
queries, discovery, err = svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
for _, q := range expectedMDMQueries {
|
|
require.NotContains(t, queries, q.name)
|
|
require.NotContains(t, discovery, q.name)
|
|
}
|
|
|
|
// MDM disabled, not darwin
|
|
mdmEnabled = false
|
|
queries, discovery, err = svc.detailQueriesForHost(ctx, &host)
|
|
require.NoError(t, err)
|
|
for _, q := range expectedMDMQueries {
|
|
require.NotContains(t, queries, q.name)
|
|
require.NotContains(t, discovery, q.name)
|
|
}
|
|
}
|
|
|
|
func TestNewDistributedQueryCampaign(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
rs := &mockresult.QueryResultStore{
|
|
HealthCheckFunc: func() error {
|
|
return nil
|
|
},
|
|
}
|
|
lq := live_query_mock.New(t)
|
|
mockClock := clock.NewMockClock()
|
|
svc, ctx := newTestServiceWithClock(t, ds, rs, lq, mockClock)
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
var gotQuery *fleet.Query
|
|
ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
|
gotQuery = query
|
|
query.ID = 42
|
|
return query, nil
|
|
}
|
|
var gotCampaign *fleet.DistributedQueryCampaign
|
|
ds.NewDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) {
|
|
gotCampaign = camp
|
|
camp.ID = 21
|
|
return camp, nil
|
|
}
|
|
var gotTargets []*fleet.DistributedQueryCampaignTarget
|
|
ds.NewDistributedQueryCampaignTargetFunc = func(ctx context.Context, target *fleet.DistributedQueryCampaignTarget) (*fleet.DistributedQueryCampaignTarget, error) {
|
|
gotTargets = append(gotTargets, target)
|
|
return target, nil
|
|
}
|
|
|
|
ds.CountHostsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) {
|
|
return fleet.TargetMetrics{}, nil
|
|
}
|
|
ds.HostIDsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) {
|
|
return []uint{1, 3, 5}, nil
|
|
}
|
|
lq.On("RunQuery", "21", "select year, month, day, hour, minutes, seconds from time", []uint{1, 3, 5}).Return(nil)
|
|
viewerCtx := viewer.NewContext(ctx, viewer.Viewer{
|
|
User: &fleet.User{
|
|
ID: 0,
|
|
GlobalRole: ptr.String(fleet.RoleAdmin),
|
|
},
|
|
})
|
|
q := "select year, month, day, hour, minutes, seconds from time"
|
|
campaign, err := svc.NewDistributedQueryCampaign(viewerCtx, q, nil, fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, gotQuery.ID, gotCampaign.QueryID)
|
|
assert.Equal(t, []*fleet.DistributedQueryCampaignTarget{
|
|
{
|
|
Type: fleet.TargetHost,
|
|
DistributedQueryCampaignID: campaign.ID,
|
|
TargetID: 2,
|
|
},
|
|
{
|
|
Type: fleet.TargetLabel,
|
|
DistributedQueryCampaignID: campaign.ID,
|
|
TargetID: 1,
|
|
},
|
|
}, gotTargets,
|
|
)
|
|
}
|
|
|
|
func TestDistributedQueryResults(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
rs := pubsub.NewInmemQueryResults()
|
|
lq := live_query_mock.New(t)
|
|
svc, ctx := newTestServiceWithClock(t, ds, rs, lq, mockClock)
|
|
|
|
campaign := &fleet.DistributedQueryCampaign{ID: 42}
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
host := &fleet.Host{
|
|
ID: 1,
|
|
Platform: "windows",
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
if id != 1 {
|
|
return nil, errors.New("not found")
|
|
}
|
|
return host, nil
|
|
}
|
|
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
|
if host.ID != 1 {
|
|
return errors.New("not found")
|
|
}
|
|
return nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
}}, nil
|
|
}
|
|
|
|
hostCtx := hostctx.NewContext(ctx, host)
|
|
|
|
lq.On("QueriesForHost", uint(1)).Return(
|
|
map[string]string{
|
|
fmt.Sprint(campaign.ID): "select * from time",
|
|
},
|
|
nil,
|
|
)
|
|
lq.On("QueryCompletedByHost", fmt.Sprint(campaign.ID), host.ID).Return(nil)
|
|
|
|
// Now we should get the active distributed query
|
|
queries, discovery, acc, err := svc.GetDistributedQueries(hostCtx)
|
|
require.NoError(t, err)
|
|
// +1 for the distributed query for campaign ID 42, +1 for windows update history, +1 for the fleet_no_policies_wildcard query.
|
|
if expected := expectedDetailQueriesForPlatform(host.Platform); !assert.Equal(t, len(expected)+3, len(queries)) {
|
|
// this is just to print the diff between the expected and actual query
|
|
// keys when the count assertion fails, to help debugging - they are not
|
|
// expected to match.
|
|
require.ElementsMatch(t, osqueryMapKeys(expected), distQueriesMapKeys(queries))
|
|
}
|
|
verifyDiscovery(t, queries, discovery)
|
|
queryKey := fmt.Sprintf("%s%d", hostDistributedQueryPrefix, campaign.ID)
|
|
assert.Equal(t, "select * from time", queries[queryKey])
|
|
assert.NotZero(t, acc)
|
|
|
|
expectedRows := []map[string]string{
|
|
{
|
|
"year": "2016",
|
|
"month": "11",
|
|
"day": "11",
|
|
"hour": "6",
|
|
"minutes": "12",
|
|
"seconds": "10",
|
|
},
|
|
}
|
|
results := map[string][]map[string]string{
|
|
queryKey: expectedRows,
|
|
}
|
|
expectedStats := fleet.Stats{
|
|
UserTime: uint64(1),
|
|
}
|
|
stats := map[string]*fleet.Stats{
|
|
queryKey: &expectedStats,
|
|
}
|
|
|
|
// TODO use service method
|
|
readChan, err := rs.ReadChannel(context.Background(), *campaign)
|
|
require.NoError(t, err)
|
|
|
|
// We need to listen for the result in a separate thread to prevent the
|
|
// write to the result channel from failing
|
|
var waitSetup, waitComplete sync.WaitGroup
|
|
waitSetup.Add(1)
|
|
waitComplete.Add(1)
|
|
go func() {
|
|
waitSetup.Done()
|
|
select {
|
|
case val := <-readChan:
|
|
if res, ok := val.(fleet.DistributedQueryResult); ok {
|
|
assert.Equal(t, campaign.ID, res.DistributedQueryCampaignID)
|
|
assert.Equal(t, expectedRows, res.Rows)
|
|
assert.Equal(t, host.ID, res.Host.ID)
|
|
assert.Equal(t, host.Hostname, res.Host.Hostname)
|
|
assert.Equal(t, host.DisplayName(), res.Host.DisplayName)
|
|
assert.Equal(t, &expectedStats, res.Stats)
|
|
} else {
|
|
t.Error("Wrong result type")
|
|
}
|
|
assert.NotNil(t, val)
|
|
|
|
case <-time.After(1 * time.Second):
|
|
t.Error("No result received")
|
|
}
|
|
waitComplete.Done()
|
|
}()
|
|
|
|
waitSetup.Wait()
|
|
// Sleep a short time to ensure that the above goroutine is blocking on
|
|
// the channel read (the waitSetup.Wait() is not necessarily sufficient
|
|
// if there is a context switch immediately after waitSetup.Done() is
|
|
// called). This should be a small price to pay to prevent flakiness in
|
|
// this test.
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
err = svc.SubmitDistributedQueryResults(
|
|
hostCtx, results, map[string]fleet.OsqueryStatus{}, map[string]string{}, stats,
|
|
)
|
|
require.NoError(t, err)
|
|
// Sleep to ensure checks in the goroutine are actually done.
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
func TestIngestDistributedQueryParseIdError(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
rs := pubsub.NewInmemQueryResults()
|
|
lq := live_query_mock.New(t)
|
|
svc := &Service{
|
|
ds: ds,
|
|
resultStore: rs,
|
|
liveQueryStore: lq,
|
|
logger: log.NewNopLogger(),
|
|
clock: mockClock,
|
|
}
|
|
|
|
host := fleet.Host{ID: 1}
|
|
err := svc.ingestDistributedQuery(context.Background(), host, "bad_name", []map[string]string{}, "", nil)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "unable to parse campaign")
|
|
}
|
|
|
|
func TestIngestDistributedQueryOrphanedCampaignLoadError(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
rs := pubsub.NewInmemQueryResults()
|
|
lq := live_query_mock.New(t)
|
|
svc := &Service{
|
|
ds: ds,
|
|
resultStore: rs,
|
|
liveQueryStore: lq,
|
|
logger: log.NewNopLogger(),
|
|
clock: mockClock,
|
|
}
|
|
|
|
ds.DistributedQueryCampaignFunc = func(ctx context.Context, id uint) (*fleet.DistributedQueryCampaign, error) {
|
|
return nil, errors.New("missing campaign")
|
|
}
|
|
|
|
lq.On("StopQuery", "42").Return(nil)
|
|
|
|
host := fleet.Host{ID: 1}
|
|
|
|
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "", nil)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "loading orphaned campaign")
|
|
}
|
|
|
|
func TestIngestDistributedQueryOrphanedCampaignWaitListener(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
rs := pubsub.NewInmemQueryResults()
|
|
lq := live_query_mock.New(t)
|
|
svc := &Service{
|
|
ds: ds,
|
|
resultStore: rs,
|
|
liveQueryStore: lq,
|
|
logger: log.NewNopLogger(),
|
|
clock: mockClock,
|
|
}
|
|
|
|
campaign := &fleet.DistributedQueryCampaign{
|
|
ID: 42,
|
|
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
|
|
CreateTimestamp: fleet.CreateTimestamp{
|
|
CreatedAt: mockClock.Now().Add(-1 * time.Second),
|
|
},
|
|
},
|
|
}
|
|
|
|
ds.DistributedQueryCampaignFunc = func(ctx context.Context, id uint) (*fleet.DistributedQueryCampaign, error) {
|
|
return campaign, nil
|
|
}
|
|
|
|
host := fleet.Host{ID: 1}
|
|
|
|
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "", nil)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "campaignID=42 waiting for listener")
|
|
}
|
|
|
|
func TestIngestDistributedQueryOrphanedCloseError(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
rs := pubsub.NewInmemQueryResults()
|
|
lq := live_query_mock.New(t)
|
|
svc := &Service{
|
|
ds: ds,
|
|
resultStore: rs,
|
|
liveQueryStore: lq,
|
|
logger: log.NewNopLogger(),
|
|
clock: mockClock,
|
|
}
|
|
|
|
campaign := &fleet.DistributedQueryCampaign{
|
|
ID: 42,
|
|
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
|
|
CreateTimestamp: fleet.CreateTimestamp{
|
|
CreatedAt: mockClock.Now().Add(-2 * time.Minute),
|
|
},
|
|
},
|
|
}
|
|
|
|
ds.DistributedQueryCampaignFunc = func(ctx context.Context, id uint) (*fleet.DistributedQueryCampaign, error) {
|
|
return campaign, nil
|
|
}
|
|
ds.SaveDistributedQueryCampaignFunc = func(ctx context.Context, campaign *fleet.DistributedQueryCampaign) error {
|
|
return errors.New("failed save")
|
|
}
|
|
|
|
host := fleet.Host{ID: 1}
|
|
|
|
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "", nil)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "closing orphaned campaign")
|
|
}
|
|
|
|
func TestIngestDistributedQueryOrphanedStopError(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
rs := pubsub.NewInmemQueryResults()
|
|
lq := live_query_mock.New(t)
|
|
svc := &Service{
|
|
ds: ds,
|
|
resultStore: rs,
|
|
liveQueryStore: lq,
|
|
logger: log.NewNopLogger(),
|
|
clock: mockClock,
|
|
}
|
|
|
|
campaign := &fleet.DistributedQueryCampaign{
|
|
ID: 42,
|
|
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
|
|
CreateTimestamp: fleet.CreateTimestamp{
|
|
CreatedAt: mockClock.Now().Add(-2 * time.Minute),
|
|
},
|
|
},
|
|
}
|
|
|
|
ds.DistributedQueryCampaignFunc = func(ctx context.Context, id uint) (*fleet.DistributedQueryCampaign, error) {
|
|
return campaign, nil
|
|
}
|
|
ds.SaveDistributedQueryCampaignFunc = func(ctx context.Context, campaign *fleet.DistributedQueryCampaign) error {
|
|
return nil
|
|
}
|
|
lq.On("StopQuery", fmt.Sprint(campaign.ID)).Return(errors.New("failed"))
|
|
|
|
host := fleet.Host{ID: 1}
|
|
|
|
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "", nil)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "stopping orphaned campaign")
|
|
}
|
|
|
|
func TestIngestDistributedQueryOrphanedStop(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
rs := pubsub.NewInmemQueryResults()
|
|
lq := live_query_mock.New(t)
|
|
svc := &Service{
|
|
ds: ds,
|
|
resultStore: rs,
|
|
liveQueryStore: lq,
|
|
logger: log.NewNopLogger(),
|
|
clock: mockClock,
|
|
}
|
|
|
|
campaign := &fleet.DistributedQueryCampaign{
|
|
ID: 42,
|
|
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
|
|
CreateTimestamp: fleet.CreateTimestamp{
|
|
CreatedAt: mockClock.Now().Add(-2 * time.Minute),
|
|
},
|
|
},
|
|
}
|
|
|
|
ds.DistributedQueryCampaignFunc = func(ctx context.Context, id uint) (*fleet.DistributedQueryCampaign, error) {
|
|
return campaign, nil
|
|
}
|
|
ds.SaveDistributedQueryCampaignFunc = func(ctx context.Context, campaign *fleet.DistributedQueryCampaign) error {
|
|
return nil
|
|
}
|
|
lq.On("StopQuery", fmt.Sprint(campaign.ID)).Return(nil)
|
|
|
|
host := fleet.Host{ID: 1}
|
|
|
|
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "", nil)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "campaignID=42 stopped")
|
|
lq.AssertExpectations(t)
|
|
}
|
|
|
|
func TestIngestDistributedQueryRecordCompletionError(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
rs := pubsub.NewInmemQueryResults()
|
|
lq := live_query_mock.New(t)
|
|
svc := &Service{
|
|
ds: ds,
|
|
resultStore: rs,
|
|
liveQueryStore: lq,
|
|
logger: log.NewNopLogger(),
|
|
clock: mockClock,
|
|
}
|
|
|
|
campaign := &fleet.DistributedQueryCampaign{ID: 42}
|
|
host := fleet.Host{ID: 1}
|
|
|
|
lq.On("QueryCompletedByHost", fmt.Sprint(campaign.ID), host.ID).Return(errors.New("fail"))
|
|
|
|
go func() {
|
|
ch, err := rs.ReadChannel(context.Background(), *campaign)
|
|
require.NoError(t, err)
|
|
<-ch
|
|
}()
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "", nil)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "record query completion")
|
|
lq.AssertExpectations(t)
|
|
}
|
|
|
|
func TestIngestDistributedQuery(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
rs := pubsub.NewInmemQueryResults()
|
|
lq := live_query_mock.New(t)
|
|
svc := &Service{
|
|
ds: ds,
|
|
resultStore: rs,
|
|
liveQueryStore: lq,
|
|
logger: log.NewNopLogger(),
|
|
clock: mockClock,
|
|
}
|
|
|
|
campaign := &fleet.DistributedQueryCampaign{ID: 42}
|
|
host := fleet.Host{ID: 1}
|
|
|
|
lq.On("QueryCompletedByHost", fmt.Sprint(campaign.ID), host.ID).Return(nil)
|
|
|
|
go func() {
|
|
ch, err := rs.ReadChannel(context.Background(), *campaign)
|
|
require.NoError(t, err)
|
|
<-ch
|
|
}()
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
err := svc.ingestDistributedQuery(context.Background(), host, "fleet_distributed_query_42", []map[string]string{}, "", nil)
|
|
require.NoError(t, err)
|
|
lq.AssertExpectations(t)
|
|
}
|
|
|
|
func TestUpdateHostIntervals(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint, hostID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) {
|
|
return []*fleet.Pack{}, nil
|
|
}
|
|
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
|
|
return nil, 0, nil, nil
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
initIntervals fleet.HostOsqueryIntervals
|
|
finalIntervals fleet.HostOsqueryIntervals
|
|
configOptions json.RawMessage
|
|
updateIntervalsCalled bool
|
|
}{
|
|
{
|
|
"Both updated",
|
|
fleet.HostOsqueryIntervals{
|
|
ConfigTLSRefresh: 60,
|
|
},
|
|
fleet.HostOsqueryIntervals{
|
|
DistributedInterval: 11,
|
|
LoggerTLSPeriod: 33,
|
|
ConfigTLSRefresh: 60,
|
|
},
|
|
json.RawMessage(`{"options": {
|
|
"distributed_interval": 11,
|
|
"logger_tls_period": 33,
|
|
"logger_plugin": "tls"
|
|
}}`),
|
|
true,
|
|
},
|
|
{
|
|
"Only logger_tls_period updated",
|
|
fleet.HostOsqueryIntervals{
|
|
DistributedInterval: 11,
|
|
ConfigTLSRefresh: 60,
|
|
},
|
|
fleet.HostOsqueryIntervals{
|
|
DistributedInterval: 11,
|
|
LoggerTLSPeriod: 33,
|
|
ConfigTLSRefresh: 60,
|
|
},
|
|
json.RawMessage(`{"options": {
|
|
"distributed_interval": 11,
|
|
"logger_tls_period": 33
|
|
}}`),
|
|
true,
|
|
},
|
|
{
|
|
"Only distributed_interval updated",
|
|
fleet.HostOsqueryIntervals{
|
|
ConfigTLSRefresh: 60,
|
|
LoggerTLSPeriod: 33,
|
|
},
|
|
fleet.HostOsqueryIntervals{
|
|
DistributedInterval: 11,
|
|
LoggerTLSPeriod: 33,
|
|
ConfigTLSRefresh: 60,
|
|
},
|
|
json.RawMessage(`{"options": {
|
|
"distributed_interval": 11,
|
|
"logger_tls_period": 33
|
|
}}`),
|
|
true,
|
|
},
|
|
{
|
|
"Fleet not managing distributed_interval",
|
|
fleet.HostOsqueryIntervals{
|
|
ConfigTLSRefresh: 60,
|
|
DistributedInterval: 11,
|
|
},
|
|
fleet.HostOsqueryIntervals{
|
|
DistributedInterval: 11,
|
|
LoggerTLSPeriod: 33,
|
|
ConfigTLSRefresh: 60,
|
|
},
|
|
json.RawMessage(`{"options":{
|
|
"logger_tls_period": 33
|
|
}}`),
|
|
true,
|
|
},
|
|
{
|
|
"config_refresh should also cause an update",
|
|
fleet.HostOsqueryIntervals{
|
|
DistributedInterval: 11,
|
|
LoggerTLSPeriod: 33,
|
|
ConfigTLSRefresh: 60,
|
|
},
|
|
fleet.HostOsqueryIntervals{
|
|
DistributedInterval: 11,
|
|
LoggerTLSPeriod: 33,
|
|
ConfigTLSRefresh: 42,
|
|
},
|
|
json.RawMessage(`{"options":{
|
|
"distributed_interval": 11,
|
|
"logger_tls_period": 33,
|
|
"config_refresh": 42
|
|
}}`),
|
|
true,
|
|
},
|
|
{
|
|
"update intervals should not be called with no changes",
|
|
fleet.HostOsqueryIntervals{
|
|
DistributedInterval: 11,
|
|
LoggerTLSPeriod: 33,
|
|
ConfigTLSRefresh: 60,
|
|
},
|
|
fleet.HostOsqueryIntervals{
|
|
DistributedInterval: 11,
|
|
LoggerTLSPeriod: 33,
|
|
ConfigTLSRefresh: 60,
|
|
},
|
|
json.RawMessage(`{"options":{
|
|
"distributed_interval": 11,
|
|
"logger_tls_period": 33
|
|
}}`),
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := hostctx.NewContext(ctx, &fleet.Host{
|
|
ID: 1,
|
|
NodeKey: ptr.String("123456"),
|
|
DistributedInterval: tt.initIntervals.DistributedInterval,
|
|
ConfigTLSRefresh: tt.initIntervals.ConfigTLSRefresh,
|
|
LoggerTLSPeriod: tt.initIntervals.LoggerTLSPeriod,
|
|
})
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{AgentOptions: ptr.RawMessage(json.RawMessage(`{"config":` + string(tt.configOptions) + `}`))}, nil
|
|
}
|
|
|
|
updateIntervalsCalled := false
|
|
ds.UpdateHostOsqueryIntervalsFunc = func(ctx context.Context, hostID uint, intervals fleet.HostOsqueryIntervals) error {
|
|
if hostID != 1 {
|
|
return errors.New("not found")
|
|
}
|
|
updateIntervalsCalled = true
|
|
assert.Equal(t, tt.finalIntervals, intervals)
|
|
return nil
|
|
}
|
|
|
|
_, err := svc.GetClientConfig(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.updateIntervalsCalled, updateIntervalsCalled)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthenticationErrors(t *testing.T) {
|
|
ms := new(mock.Store)
|
|
ms.LoadHostByNodeKeyFunc = func(ctx context.Context, nodeKey string) (*fleet.Host, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
svc, ctx := newTestService(t, ms, nil, nil)
|
|
|
|
_, _, err := svc.AuthenticateHost(ctx, "")
|
|
require.Error(t, err)
|
|
require.True(t, err.(*endpoint_utils.OsqueryError).NodeInvalid())
|
|
|
|
ms.LoadHostByNodeKeyFunc = func(ctx context.Context, nodeKey string) (*fleet.Host, error) {
|
|
return &fleet.Host{ID: 1}, nil
|
|
}
|
|
ms.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
_, _, err = svc.AuthenticateHost(ctx, "foo")
|
|
require.NoError(t, err)
|
|
|
|
// return not found error
|
|
ms.LoadHostByNodeKeyFunc = func(ctx context.Context, nodeKey string) (*fleet.Host, error) {
|
|
return nil, newNotFoundError()
|
|
}
|
|
|
|
_, _, err = svc.AuthenticateHost(ctx, "foo")
|
|
require.Error(t, err)
|
|
require.True(t, err.(*endpoint_utils.OsqueryError).NodeInvalid())
|
|
|
|
// return other error
|
|
ms.LoadHostByNodeKeyFunc = func(ctx context.Context, nodeKey string) (*fleet.Host, error) {
|
|
return nil, errors.New("foo")
|
|
}
|
|
|
|
_, _, err = svc.AuthenticateHost(ctx, "foo")
|
|
require.NotNil(t, err)
|
|
require.False(t, err.(*endpoint_utils.OsqueryError).NodeInvalid())
|
|
}
|
|
|
|
func TestGetHostIdentifier(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
details := map[string](map[string]string){
|
|
"osquery_info": map[string]string{
|
|
"uuid": "foouuid",
|
|
"instance_id": "fooinstance",
|
|
},
|
|
"system_info": map[string]string{
|
|
"hostname": "foohost",
|
|
},
|
|
}
|
|
|
|
emptyDetails := map[string](map[string]string){
|
|
"osquery_info": map[string]string{
|
|
"uuid": "",
|
|
"instance_id": "",
|
|
},
|
|
"system_info": map[string]string{
|
|
"hostname": "",
|
|
},
|
|
}
|
|
|
|
testCases := []struct {
|
|
identifierOption string
|
|
providedIdentifier string
|
|
details map[string](map[string]string)
|
|
expected string
|
|
shouldPanic bool
|
|
}{
|
|
// Panix
|
|
{identifierOption: "bad", shouldPanic: true},
|
|
{identifierOption: "", shouldPanic: true},
|
|
|
|
// Missing details
|
|
{identifierOption: "instance", providedIdentifier: "foobar", expected: "foobar"},
|
|
{identifierOption: "uuid", providedIdentifier: "foobar", expected: "foobar"},
|
|
{identifierOption: "hostname", providedIdentifier: "foobar", expected: "foobar"},
|
|
{identifierOption: "provided", providedIdentifier: "foobar", expected: "foobar"},
|
|
|
|
// Empty details
|
|
{identifierOption: "instance", providedIdentifier: "foobar", details: emptyDetails, expected: "foobar"},
|
|
{identifierOption: "uuid", providedIdentifier: "foobar", details: emptyDetails, expected: "foobar"},
|
|
{identifierOption: "hostname", providedIdentifier: "foobar", details: emptyDetails, expected: "foobar"},
|
|
{identifierOption: "provided", providedIdentifier: "foobar", details: emptyDetails, expected: "foobar"},
|
|
|
|
// Successes
|
|
{identifierOption: "instance", providedIdentifier: "foobar", details: details, expected: "fooinstance"},
|
|
{identifierOption: "uuid", providedIdentifier: "foobar", details: details, expected: "foouuid"},
|
|
{identifierOption: "hostname", providedIdentifier: "foobar", details: details, expected: "foohost"},
|
|
{identifierOption: "provided", providedIdentifier: "foobar", details: details, expected: "foobar"},
|
|
}
|
|
logger := log.NewNopLogger()
|
|
|
|
for _, tt := range testCases {
|
|
t.Run("", func(t *testing.T) {
|
|
if tt.shouldPanic {
|
|
assert.Panics(
|
|
t,
|
|
func() { getHostIdentifier(logger, tt.identifierOption, tt.providedIdentifier, tt.details) },
|
|
)
|
|
return
|
|
}
|
|
|
|
assert.Equal(
|
|
t,
|
|
tt.expected,
|
|
getHostIdentifier(logger, tt.identifierOption, tt.providedIdentifier, tt.details),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDistributedQueriesLogsManyErrors(t *testing.T) {
|
|
buf := new(bytes.Buffer)
|
|
logger := log.NewJSONLogger(buf)
|
|
logger = level.NewFilter(logger, level.AllowDebug())
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
host := &fleet.Host{
|
|
ID: 1,
|
|
Platform: "darwin",
|
|
}
|
|
|
|
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
|
return authz.CheckMissingWithResponse(nil)
|
|
}
|
|
ds.RecordLabelQueryExecutionsFunc = func(ctx context.Context, host *fleet.Host, results map[uint]*bool, t time.Time, deferred bool) error {
|
|
return errors.New("something went wrong")
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.SaveHostAdditionalFunc = func(ctx context.Context, hostID uint, additional *json.RawMessage) error {
|
|
return errors.New("something went wrong")
|
|
}
|
|
|
|
lCtx := &fleetLogging.LoggingContext{}
|
|
ctx = fleetLogging.NewContext(ctx, lCtx)
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
err := svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostDetailQueryPrefix + "network_interface_unix": {{"col1": "val1"}}, // we need one detail query that updates hosts.
|
|
hostLabelQueryPrefix + "1": {{"col1": "val1"}},
|
|
hostAdditionalQueryPrefix + "1": {{"col1": "val1"}},
|
|
},
|
|
map[string]fleet.OsqueryStatus{},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
lCtx.Log(ctx, logger)
|
|
|
|
logs := buf.String()
|
|
parts := strings.Split(strings.TrimSpace(logs), "\n")
|
|
require.Len(t, parts, 1)
|
|
|
|
var logData map[string]interface{}
|
|
err = json.Unmarshal([]byte(parts[0]), &logData)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "something went wrong || something went wrong", logData["err"])
|
|
assert.Equal(t, "Missing authorization check", logData["internal"])
|
|
}
|
|
|
|
func TestDistributedQueriesReloadsHostIfDetailsAreIn(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
host := &fleet.Host{
|
|
ID: 42,
|
|
Platform: "darwin",
|
|
}
|
|
|
|
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
|
return nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
err := svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostDetailQueryPrefix + "network_interface_unix": {{"col1": "val1"}},
|
|
},
|
|
map[string]fleet.OsqueryStatus{},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.True(t, ds.UpdateHostFuncInvoked)
|
|
}
|
|
|
|
func TestObserversCanOnlyRunDistributedCampaigns(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
rs := &mockresult.QueryResultStore{
|
|
HealthCheckFunc: func() error {
|
|
return nil
|
|
},
|
|
}
|
|
lq := live_query_mock.New(t)
|
|
mockClock := clock.NewMockClock()
|
|
svc, ctx := newTestServiceWithClock(t, ds, rs, lq, mockClock)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
ds.NewDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) {
|
|
return camp, nil
|
|
}
|
|
ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
|
|
return &fleet.Query{
|
|
ID: 42,
|
|
Name: "query",
|
|
Query: "select 1;",
|
|
ObserverCanRun: false,
|
|
}, nil
|
|
}
|
|
viewerCtx := viewer.NewContext(ctx, viewer.Viewer{
|
|
User: &fleet.User{ID: 0, GlobalRole: ptr.String(fleet.RoleObserver)},
|
|
})
|
|
|
|
q := "select year, month, day, hour, minutes, seconds from time"
|
|
ds.NewActivityFunc = func(
|
|
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
|
) error {
|
|
return nil
|
|
}
|
|
_, err := svc.NewDistributedQueryCampaign(viewerCtx, q, nil, fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}})
|
|
require.Error(t, err)
|
|
|
|
_, err = svc.NewDistributedQueryCampaign(viewerCtx, "", ptr.Uint(42), fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}})
|
|
require.Error(t, err)
|
|
|
|
ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
|
|
return &fleet.Query{
|
|
ID: 42,
|
|
Name: "query",
|
|
Query: "select 1;",
|
|
ObserverCanRun: true,
|
|
}, nil
|
|
}
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.NewDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) {
|
|
camp.ID = 21
|
|
return camp, nil
|
|
}
|
|
ds.NewDistributedQueryCampaignTargetFunc = func(ctx context.Context, target *fleet.DistributedQueryCampaignTarget) (*fleet.DistributedQueryCampaignTarget, error) {
|
|
return target, nil
|
|
}
|
|
ds.CountHostsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) {
|
|
return fleet.TargetMetrics{}, nil
|
|
}
|
|
ds.HostIDsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) {
|
|
return []uint{1, 3, 5}, nil
|
|
}
|
|
ds.NewActivityFunc = func(
|
|
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
|
) error {
|
|
return nil
|
|
}
|
|
lq.On("RunQuery", "21", "select 1;", []uint{1, 3, 5}).Return(nil)
|
|
_, err = svc.NewDistributedQueryCampaign(viewerCtx, "", ptr.Uint(42), fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTeamMaintainerCanRunNewDistributedCampaigns(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
rs := &mockresult.QueryResultStore{
|
|
HealthCheckFunc: func() error {
|
|
return nil
|
|
},
|
|
}
|
|
lq := live_query_mock.New(t)
|
|
mockClock := clock.NewMockClock()
|
|
svc, ctx := newTestServiceWithClock(t, ds, rs, lq, mockClock)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
ds.NewDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) {
|
|
return camp, nil
|
|
}
|
|
ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
|
|
return &fleet.Query{
|
|
ID: 42,
|
|
AuthorID: ptr.Uint(99),
|
|
Name: "query",
|
|
Query: "select 1;",
|
|
ObserverCanRun: false,
|
|
}, nil
|
|
}
|
|
viewerCtx := viewer.NewContext(ctx, viewer.Viewer{
|
|
User: &fleet.User{ID: 99, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 123}, Role: fleet.RoleMaintainer}}},
|
|
})
|
|
|
|
q := "select year, month, day, hour, minutes, seconds from time"
|
|
ds.NewActivityFunc = func(
|
|
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
|
) error {
|
|
return nil
|
|
}
|
|
// var gotQuery *fleet.Query
|
|
ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
|
// gotQuery = query
|
|
query.ID = 42
|
|
return query, nil
|
|
}
|
|
ds.NewDistributedQueryCampaignTargetFunc = func(ctx context.Context, target *fleet.DistributedQueryCampaignTarget) (*fleet.DistributedQueryCampaignTarget, error) {
|
|
return target, nil
|
|
}
|
|
ds.CountHostsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) {
|
|
return fleet.TargetMetrics{}, nil
|
|
}
|
|
ds.HostIDsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) {
|
|
return []uint{1, 3, 5}, nil
|
|
}
|
|
ds.NewActivityFunc = func(
|
|
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
|
) error {
|
|
return nil
|
|
}
|
|
lq.On("RunQuery", "0", "select year, month, day, hour, minutes, seconds from time", []uint{1, 3, 5}).Return(nil)
|
|
_, err := svc.NewDistributedQueryCampaign(viewerCtx, q, nil, fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}, TeamIDs: []uint{123}})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPolicyQueries(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
lq := live_query_mock.New(t)
|
|
svc, ctx := newTestServiceWithClock(t, ds, nil, lq, mockClock)
|
|
|
|
host := &fleet.Host{
|
|
Platform: "darwin",
|
|
}
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
return host, nil
|
|
}
|
|
ds.UpdateHostFunc = func(ctx context.Context, gotHost *fleet.Host) error {
|
|
host = gotHost
|
|
return nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
}}, nil
|
|
}
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostuuid string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
lq.On("QueriesForHost", uint(0)).Return(map[string]string{}, nil)
|
|
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{"1": "select 1", "2": "select 42;"}, nil
|
|
}
|
|
recordedResults := make(map[uint]*bool)
|
|
ds.RecordPolicyQueryExecutionsFunc = func(ctx context.Context, gotHost *fleet.Host, results map[uint]*bool, updated time.Time, deferred bool) error {
|
|
recordedResults = results
|
|
host = gotHost
|
|
return nil
|
|
}
|
|
ds.FlippingPoliciesForHostFunc = func(ctx context.Context, hostID uint, incomingResults map[uint]*bool) (newFailing []uint, newPassing []uint, err error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
queries, discovery, _, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +2 policy queries
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+2, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
|
|
checkPolicyResults := func(queries map[string]string) {
|
|
hasPolicy1, hasPolicy2 := false, false
|
|
for name := range queries {
|
|
if strings.HasPrefix(name, hostPolicyQueryPrefix) {
|
|
if name[len(hostPolicyQueryPrefix):] == "1" {
|
|
hasPolicy1 = true
|
|
}
|
|
if name[len(hostPolicyQueryPrefix):] == "2" {
|
|
hasPolicy2 = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, hasPolicy1)
|
|
assert.True(t, hasPolicy2)
|
|
}
|
|
|
|
checkPolicyResults(queries)
|
|
|
|
// Record a query execution.
|
|
err = svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostPolicyQueryPrefix + "1": {{"col1": "val1"}},
|
|
hostPolicyQueryPrefix + "2": {},
|
|
},
|
|
map[string]fleet.OsqueryStatus{
|
|
hostPolicyQueryPrefix + "2": 1,
|
|
},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, recordedResults, 2)
|
|
require.NotNil(t, recordedResults[1])
|
|
require.True(t, *recordedResults[1])
|
|
result, ok := recordedResults[2]
|
|
require.True(t, ok)
|
|
require.Nil(t, result)
|
|
|
|
noPolicyResults := func(queries map[string]string) {
|
|
hasAnyPolicy := false
|
|
for name := range queries {
|
|
if strings.HasPrefix(name, hostPolicyQueryPrefix) {
|
|
hasAnyPolicy = true
|
|
break
|
|
}
|
|
}
|
|
assert.False(t, hasAnyPolicy)
|
|
}
|
|
|
|
// After the first time we get policies and update the host, then there shouldn't be any policies.
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform)), len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
noPolicyResults(queries)
|
|
|
|
// Let's move time forward, there should be policies now.
|
|
mockClock.AddTime(2 * time.Hour)
|
|
|
|
queries, discovery, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +2 policy queries
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+2, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
checkPolicyResults(queries)
|
|
|
|
// Record another query execution.
|
|
err = svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostPolicyQueryPrefix + "1": {{"col1": "val1"}},
|
|
hostPolicyQueryPrefix + "2": {},
|
|
},
|
|
map[string]fleet.OsqueryStatus{
|
|
hostPolicyQueryPrefix + "2": 1,
|
|
},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, recordedResults, 2)
|
|
require.NotNil(t, recordedResults[1])
|
|
require.True(t, *recordedResults[1])
|
|
result, ok = recordedResults[2]
|
|
require.True(t, ok)
|
|
require.Nil(t, result)
|
|
|
|
// There shouldn't be any policies now.
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform)), len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
noPolicyResults(queries)
|
|
|
|
// With refetch requested policy queries should be returned.
|
|
host.RefetchRequested = true
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +2 policy queries
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+2, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
checkPolicyResults(queries)
|
|
|
|
// Record another query execution.
|
|
err = svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostPolicyQueryPrefix + "1": {{"col1": "val1"}},
|
|
hostPolicyQueryPrefix + "2": {},
|
|
},
|
|
map[string]fleet.OsqueryStatus{
|
|
hostPolicyQueryPrefix + "2": 1,
|
|
},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, recordedResults[1])
|
|
require.True(t, *recordedResults[1])
|
|
result, ok = recordedResults[2]
|
|
require.True(t, ok)
|
|
require.Nil(t, result)
|
|
|
|
// SubmitDistributedQueryResults will set RefetchRequested to false.
|
|
require.False(t, host.RefetchRequested)
|
|
|
|
// There shouldn't be any policies now.
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform)), len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
noPolicyResults(queries)
|
|
}
|
|
|
|
func TestPolicyQueriesDuringSetupExperience(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
lq := live_query_mock.New(t)
|
|
svc, ctx := newTestService(t, ds, nil, lq)
|
|
|
|
host := &fleet.Host{
|
|
Platform: "darwin",
|
|
}
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
return host, nil
|
|
}
|
|
ds.UpdateHostFunc = func(ctx context.Context, gotHost *fleet.Host) error {
|
|
host = gotHost
|
|
return nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
}}, nil
|
|
}
|
|
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostUUID string) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
lq.On("QueriesForHost", uint(0)).Return(map[string]string{}, nil)
|
|
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{"1": "select 1", "2": "select 42;"}, nil
|
|
}
|
|
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
queries, discovery, _, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Should not return the 2 policy queries because we're in setup experience
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform)), len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
|
|
checkPolicyResults := func(queries map[string]string, shouldHavePolicies bool) {
|
|
hasPolicy1, hasPolicy2 := false, false
|
|
for name := range queries {
|
|
if strings.HasPrefix(name, hostPolicyQueryPrefix) {
|
|
if name[len(hostPolicyQueryPrefix):] == "1" {
|
|
hasPolicy1 = true
|
|
}
|
|
if name[len(hostPolicyQueryPrefix):] == "2" {
|
|
hasPolicy2 = true
|
|
}
|
|
}
|
|
}
|
|
assert.Equal(t, hasPolicy1, shouldHavePolicies)
|
|
assert.Equal(t, hasPolicy2, shouldHavePolicies)
|
|
}
|
|
|
|
// Make it appear the host is out of setup experience and ask again for policies
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostUUID string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
queries, discovery, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// Should now return the 2 additional policy queries because we're out of setup experience
|
|
assert.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+2, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
|
|
checkPolicyResults(queries, true)
|
|
}
|
|
|
|
func TestPolicyWebhooks(t *testing.T) {
|
|
mockClock := clock.NewMockClock()
|
|
ds := new(mock.Store)
|
|
lq := live_query_mock.New(t)
|
|
pool := redistest.SetupRedis(t, t.Name(), false, false, false)
|
|
failingPolicySet := redis_policy_set.NewFailingTest(t, pool)
|
|
testConfig := config.TestConfig()
|
|
svc, ctx := newTestServiceWithConfig(t, ds, testConfig, nil, lq, &TestServerOpts{
|
|
FailingPolicySet: failingPolicySet,
|
|
Clock: mockClock,
|
|
})
|
|
|
|
host := &fleet.Host{
|
|
ID: 5,
|
|
Platform: "darwin",
|
|
Hostname: "test.hostname",
|
|
}
|
|
|
|
lq.On("QueriesForHost", uint(5)).Return(map[string]string{}, nil)
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
return host, nil
|
|
}
|
|
ds.UpdateHostFunc = func(ctx context.Context, gotHost *fleet.Host) error {
|
|
host = gotHost
|
|
return nil
|
|
}
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostuuid string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{
|
|
Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
},
|
|
WebhookSettings: fleet.WebhookSettings{
|
|
FailingPoliciesWebhook: fleet.FailingPoliciesWebhookSettings{
|
|
Enable: true,
|
|
PolicyIDs: []uint{1, 2, 3},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{
|
|
"1": "select 1;", // passing policy
|
|
"2": "select * from unexistent_table;", // policy that fails to execute (e.g. missing table)
|
|
"3": "select 1 where 1 = 0;", // failing policy
|
|
}, nil
|
|
}
|
|
recordedResults := make(map[uint]*bool)
|
|
ds.RecordPolicyQueryExecutionsFunc = func(ctx context.Context, gotHost *fleet.Host, results map[uint]*bool, updated time.Time, deferred bool) error {
|
|
recordedResults = results
|
|
host = gotHost
|
|
return nil
|
|
}
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
queries, discovery, _, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +3 for policies
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+3, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
|
|
checkPolicyResults := func(queries map[string]string) {
|
|
hasPolicy1, hasPolicy2, hasPolicy3 := false, false, false
|
|
for name := range queries {
|
|
if strings.HasPrefix(name, hostPolicyQueryPrefix) {
|
|
switch name[len(hostPolicyQueryPrefix):] {
|
|
case "1":
|
|
hasPolicy1 = true
|
|
case "2":
|
|
hasPolicy2 = true
|
|
case "3":
|
|
hasPolicy3 = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, hasPolicy1)
|
|
assert.True(t, hasPolicy2)
|
|
assert.True(t, hasPolicy3)
|
|
}
|
|
|
|
checkPolicyResults(queries)
|
|
|
|
ds.FlippingPoliciesForHostFunc = func(ctx context.Context, hostID uint, incomingResults map[uint]*bool) (newFailing []uint, newPassing []uint, err error) {
|
|
return []uint{3}, nil, nil
|
|
}
|
|
|
|
// Record a query execution.
|
|
err = svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostPolicyQueryPrefix + "1": {{"col1": "val1"}}, // succeeds
|
|
hostPolicyQueryPrefix + "2": {}, // didn't execute
|
|
hostPolicyQueryPrefix + "3": {}, // fails
|
|
},
|
|
map[string]fleet.OsqueryStatus{
|
|
hostPolicyQueryPrefix + "2": 1, // didn't execute
|
|
},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, recordedResults, 3)
|
|
require.NotNil(t, recordedResults[1])
|
|
require.True(t, *recordedResults[1])
|
|
result, ok := recordedResults[2]
|
|
require.True(t, ok)
|
|
require.Nil(t, result)
|
|
require.NotNil(t, recordedResults[3])
|
|
require.False(t, *recordedResults[3])
|
|
|
|
cmpSets := func(expSets map[uint][]fleet.PolicySetHost) error {
|
|
actualSets, err := failingPolicySet.ListSets()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var expSets_ []uint
|
|
for expSet := range expSets {
|
|
expSets_ = append(expSets_, expSet)
|
|
}
|
|
sort.Slice(expSets_, func(i, j int) bool {
|
|
return expSets_[i] < expSets_[j]
|
|
})
|
|
sort.Slice(actualSets, func(i, j int) bool {
|
|
return actualSets[i] < actualSets[j]
|
|
})
|
|
if !reflect.DeepEqual(actualSets, expSets_) {
|
|
return fmt.Errorf("sets mismatch: %+v vs %+v", actualSets, expSets_)
|
|
}
|
|
for expID, expHosts := range expSets {
|
|
actualHosts, err := failingPolicySet.ListHosts(expID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sort.Slice(actualHosts, func(i, j int) bool {
|
|
return actualHosts[i].ID < actualHosts[j].ID
|
|
})
|
|
sort.Slice(expHosts, func(i, j int) bool {
|
|
return expHosts[i].ID < expHosts[j].ID
|
|
})
|
|
if !reflect.DeepEqual(actualHosts, expHosts) {
|
|
return fmt.Errorf("hosts mismatch %d: %+v vs %+v", expID, actualHosts, expHosts)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
assert.Eventually(t, func() bool {
|
|
err = cmpSets(map[uint][]fleet.PolicySetHost{
|
|
3: {{
|
|
ID: host.ID,
|
|
Hostname: host.Hostname,
|
|
}},
|
|
})
|
|
return err == nil
|
|
}, 1*time.Minute, 250*time.Millisecond)
|
|
require.NoError(t, err)
|
|
|
|
noPolicyResults := func(queries map[string]string) {
|
|
hasAnyPolicy := false
|
|
for name := range queries {
|
|
if strings.HasPrefix(name, hostPolicyQueryPrefix) {
|
|
hasAnyPolicy = true
|
|
break
|
|
}
|
|
}
|
|
assert.False(t, hasAnyPolicy)
|
|
}
|
|
|
|
// After the first time we get policies and update the host, then there shouldn't be any policies.
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
queries, discovery, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform)), len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
noPolicyResults(queries)
|
|
|
|
// Let's move time forward, there should be policies now.
|
|
mockClock.AddTime(2 * time.Hour)
|
|
|
|
queries, discovery, _, err = svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +3 for policies
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+3, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
checkPolicyResults(queries)
|
|
|
|
ds.FlippingPoliciesForHostFunc = func(ctx context.Context, hostID uint, incomingResults map[uint]*bool) (newFailing []uint, newPassing []uint, err error) {
|
|
return []uint{1}, []uint{3}, nil
|
|
}
|
|
|
|
// Record another query execution.
|
|
err = svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostPolicyQueryPrefix + "1": {}, // 1 now fails
|
|
hostPolicyQueryPrefix + "2": {}, // didn't execute
|
|
hostPolicyQueryPrefix + "3": {{"col1": "val1"}}, // 1 now succeeds
|
|
},
|
|
map[string]fleet.OsqueryStatus{
|
|
hostPolicyQueryPrefix + "2": 1, // didn't execute
|
|
},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, recordedResults, 3)
|
|
require.NotNil(t, recordedResults[1])
|
|
require.False(t, *recordedResults[1])
|
|
result, ok = recordedResults[2]
|
|
require.True(t, ok)
|
|
require.Nil(t, result)
|
|
require.NotNil(t, recordedResults[3])
|
|
require.True(t, *recordedResults[3])
|
|
|
|
assert.Eventually(t, func() bool {
|
|
err = cmpSets(map[uint][]fleet.PolicySetHost{
|
|
1: {{
|
|
ID: host.ID,
|
|
Hostname: host.Hostname,
|
|
}},
|
|
3: {},
|
|
})
|
|
return err == nil
|
|
}, 1*time.Minute, 250*time.Millisecond)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate webhook trigger by removing the hosts.
|
|
err = failingPolicySet.RemoveHosts(1, []fleet.PolicySetHost{{
|
|
ID: host.ID,
|
|
Hostname: host.Hostname,
|
|
}})
|
|
require.NoError(t, err)
|
|
|
|
ds.FlippingPoliciesForHostFunc = func(ctx context.Context, hostID uint, incomingResults map[uint]*bool) (newFailing []uint, newPassing []uint, err error) {
|
|
return []uint{}, []uint{2}, nil
|
|
}
|
|
|
|
// Record another query execution.
|
|
err = svc.SubmitDistributedQueryResults(
|
|
ctx,
|
|
map[string][]map[string]string{
|
|
hostPolicyQueryPrefix + "1": {}, // continues to fail
|
|
hostPolicyQueryPrefix + "2": {{"col1": "val1"}}, // now passes
|
|
hostPolicyQueryPrefix + "3": {{"col1": "val1"}}, // continues to succeed
|
|
},
|
|
map[string]fleet.OsqueryStatus{},
|
|
map[string]string{},
|
|
map[string]*fleet.Stats{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, recordedResults, 3)
|
|
require.NotNil(t, recordedResults[1])
|
|
require.False(t, *recordedResults[1])
|
|
require.NotNil(t, recordedResults[2])
|
|
require.True(t, *recordedResults[2])
|
|
require.NotNil(t, recordedResults[3])
|
|
require.True(t, *recordedResults[3])
|
|
|
|
assert.Eventually(t, func() bool {
|
|
err = cmpSets(map[uint][]fleet.PolicySetHost{
|
|
1: {},
|
|
3: {},
|
|
})
|
|
return err == nil
|
|
}, 1*time.Minute, 250*time.Millisecond)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// If the live query store (Redis) is down we still (see #3503)
|
|
// want hosts to get queries and continue to check in.
|
|
func TestLiveQueriesFailing(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
lq := live_query_mock.New(t)
|
|
cfg := config.TestConfig()
|
|
buf := new(bytes.Buffer)
|
|
logger := log.NewLogfmtLogger(buf)
|
|
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, lq, &TestServerOpts{
|
|
Logger: logger,
|
|
})
|
|
|
|
hostID := uint(1)
|
|
host := &fleet.Host{
|
|
ID: hostID,
|
|
Platform: "darwin",
|
|
}
|
|
lq.On("QueriesForHost", hostID).Return(
|
|
map[string]string{},
|
|
errors.New("failed to get queries for host"),
|
|
)
|
|
|
|
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
|
return host, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{Features: fleet.Features{
|
|
EnableHostUsers: true,
|
|
EnableSoftwareInventory: true,
|
|
}}, nil
|
|
}
|
|
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostuuid string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
ctx = hostctx.NewContext(ctx, host)
|
|
|
|
queries, discovery, _, err := svc.GetDistributedQueries(ctx)
|
|
require.NoError(t, err)
|
|
// +1 to account for the fleet_no_policies_wildcard query.
|
|
require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+1, len(queries), distQueriesMapKeys(queries))
|
|
verifyDiscovery(t, queries, discovery)
|
|
|
|
logs, err := io.ReadAll(buf)
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(logs), "level=error")
|
|
require.Contains(t, string(logs), "failed to get queries for host")
|
|
}
|
|
|
|
func distQueriesMapKeys(m map[string]string) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, strings.TrimPrefix(k, "fleet_detail_query_"))
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func osqueryMapKeys(m map[string]osquery_utils.DetailQuery) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func TestPreProcessSoftwareResults(t *testing.T) {
|
|
foobarApp := map[string]string{
|
|
"name": "Foobar.app",
|
|
"version": "1.2.3",
|
|
"type": "Application (macOS)",
|
|
"bundle_identifier": "com.zoobar.foobar",
|
|
"extension_id": "",
|
|
"browser": "",
|
|
"source": "apps",
|
|
"vendor": "",
|
|
"last_opened_at": "0",
|
|
"installed_path": "/some/path",
|
|
}
|
|
zoobarApp := map[string]string{
|
|
"name": "Zoobar.app",
|
|
"version": "3.2.1",
|
|
"type": "Application (macOS)",
|
|
"bundle_identifier": "com.acme.zoobar",
|
|
"extension_id": "",
|
|
"browser": "",
|
|
"source": "apps",
|
|
"vendor": "",
|
|
"last_opened_at": "0",
|
|
"installed_path": "/some/other/path",
|
|
}
|
|
foobarVSCodeExtension := map[string]string{
|
|
"name": "vendor-x.foobar",
|
|
"version": "2024.2.1",
|
|
"type": "IDE extension (VS Code)",
|
|
"bundle_identifier": "",
|
|
"extension_id": "",
|
|
"browser": "",
|
|
"source": "vscode_extensions",
|
|
"vendor": "VendorX",
|
|
"last_opened_at": "",
|
|
"installed_path": "/some/foobar/path",
|
|
}
|
|
zoobarVSCodeExtension := map[string]string{
|
|
"name": "vendor-x.zoobar",
|
|
"version": "2023.2.1",
|
|
"type": "IDE extension (VS Code)",
|
|
"bundle_identifier": "",
|
|
"extension_id": "",
|
|
"browser": "",
|
|
"source": "vscode_extensions",
|
|
"vendor": "VendorX",
|
|
"last_opened_at": "",
|
|
"installed_path": "/some/zoobar/path",
|
|
}
|
|
someRow := map[string]string{
|
|
"1": "1",
|
|
}
|
|
appToOverride := map[string]string{
|
|
"name": "OverrideMe.app",
|
|
"version": "1.2.3",
|
|
"type": "Application (macOS)",
|
|
"bundle_identifier": "com.zoobar.overrideme",
|
|
"extension_id": "",
|
|
"browser": "",
|
|
"source": "apps",
|
|
"vendor": "OverrideMe",
|
|
"last_opened_at": "0",
|
|
"installed_path": "/some/override/path",
|
|
}
|
|
|
|
appThatOverrides := map[string]string{
|
|
"name": "OverrideMeSuccess.app",
|
|
"version": "1.2.3",
|
|
"type": "Application (macOS)",
|
|
"bundle_identifier": "com.zoobar.overrideme",
|
|
"extension_id": "",
|
|
"browser": "",
|
|
"source": "apps",
|
|
"vendor": "OverrideMe",
|
|
"last_opened_at": "0",
|
|
"installed_path": "/some/override/path",
|
|
}
|
|
|
|
pythonPackageOne := map[string]string{
|
|
"name": "cryptography",
|
|
"version": "41.0.7",
|
|
"extension_id": "",
|
|
"browser": "",
|
|
"source": "python_packages",
|
|
"vendor": "",
|
|
"installed_path": "/usr/lib/python3/dist-packages",
|
|
}
|
|
pythonPackageTwo := map[string]string{
|
|
"name": "pip",
|
|
"version": "25.0.1",
|
|
"extension_id": "",
|
|
"browser": "",
|
|
"source": "python_packages",
|
|
"vendor": "",
|
|
"installed_path": "/Users/fleetdm/.pyenv/versions/3.13.1/lib/python3.13/site-packages",
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
host *fleet.Host
|
|
resultsIn fleet.OsqueryDistributedQueryResults
|
|
statusesIn map[string]fleet.OsqueryStatus
|
|
messagesIn map[string]string
|
|
overrides map[string]osquery_utils.DetailQuery
|
|
|
|
resultsOut fleet.OsqueryDistributedQueryResults
|
|
}{
|
|
{
|
|
name: "python packages using original query in extras adds results",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_macos": fleet.StatusOK,
|
|
hostDetailQueryPrefix + "software_python_packages": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
},
|
|
hostDetailQueryPrefix + "software_python_packages": []map[string]string{
|
|
pythonPackageOne,
|
|
},
|
|
},
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
pythonPackageOne,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "python packages using user query in extras adds results",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_macos": fleet.StatusOK,
|
|
hostDetailQueryPrefix + "software_python_packages_with_users_dir": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
},
|
|
hostDetailQueryPrefix + "software_python_packages_with_users_dir": []map[string]string{
|
|
pythonPackageTwo,
|
|
},
|
|
},
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
pythonPackageTwo,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "software query works and there are vs code extensions in extra",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "other_detail_query": fleet.StatusOK,
|
|
hostDetailQueryPrefix + "software_macos": fleet.StatusOK,
|
|
hostDetailQueryPrefix + "software_vscode_extensions": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "other_detail_query": []map[string]string{
|
|
someRow,
|
|
},
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
zoobarApp,
|
|
},
|
|
hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{
|
|
foobarVSCodeExtension,
|
|
zoobarVSCodeExtension,
|
|
},
|
|
},
|
|
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "other_detail_query": []map[string]string{
|
|
someRow,
|
|
},
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
zoobarApp,
|
|
foobarVSCodeExtension,
|
|
zoobarVSCodeExtension,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "software query and extra works and there are no vscode extensions",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "other_detail_query": fleet.StatusOK,
|
|
hostDetailQueryPrefix + "software_macos": fleet.StatusOK,
|
|
hostDetailQueryPrefix + "software_vscode_extensions": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "other_detail_query": []map[string]string{
|
|
someRow,
|
|
},
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
zoobarApp,
|
|
},
|
|
hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{},
|
|
},
|
|
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "other_detail_query": []map[string]string{
|
|
someRow,
|
|
},
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
zoobarApp,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "software query works and the software extra status and results are not returned",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "other_detail_query": fleet.StatusOK,
|
|
hostDetailQueryPrefix + "software_macos": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "other_detail_query": []map[string]string{
|
|
someRow,
|
|
},
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
zoobarApp,
|
|
},
|
|
},
|
|
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "other_detail_query": []map[string]string{
|
|
someRow,
|
|
},
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
zoobarApp,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "software doesn't return status or results but the software extra does",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_vscode_extensions": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{
|
|
foobarVSCodeExtension,
|
|
zoobarVSCodeExtension,
|
|
},
|
|
},
|
|
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{},
|
|
},
|
|
{
|
|
name: "software query works, but vscode_extensions table doesn't exist",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_macos": fleet.StatusOK,
|
|
hostDetailQueryPrefix + "software_vscode_extensions": fleet.OsqueryStatus(1),
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
zoobarApp,
|
|
},
|
|
hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{},
|
|
},
|
|
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
zoobarApp,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "software query fails, vscode_extensions table returns results",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_macos": fleet.OsqueryStatus(1),
|
|
hostDetailQueryPrefix + "software_vscode_extensions": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{},
|
|
hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{
|
|
foobarVSCodeExtension,
|
|
zoobarVSCodeExtension,
|
|
},
|
|
},
|
|
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{},
|
|
},
|
|
},
|
|
{
|
|
name: "software query fails, software extra query also fails",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_macos": fleet.OsqueryStatus(1),
|
|
hostDetailQueryPrefix + "software_vscode_extensions": fleet.OsqueryStatus(1),
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{},
|
|
hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{},
|
|
},
|
|
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{},
|
|
},
|
|
},
|
|
{
|
|
name: "software inventory turned off",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "other_detail_query": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "other_detail_query": []map[string]string{
|
|
someRow,
|
|
},
|
|
},
|
|
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "other_detail_query": []map[string]string{
|
|
someRow,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "override works",
|
|
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_macos": fleet.StatusOK,
|
|
hostDetailQueryPrefix + "software_overrideMe": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
appToOverride,
|
|
foobarApp,
|
|
},
|
|
hostDetailQueryPrefix + "software_overrideMe": []map[string]string{
|
|
appThatOverrides,
|
|
},
|
|
},
|
|
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos": []map[string]string{
|
|
foobarApp,
|
|
appThatOverrides,
|
|
},
|
|
},
|
|
overrides: map[string]osquery_utils.DetailQuery{
|
|
"overrideMe": {
|
|
SoftwareOverrideMatch: func(row map[string]string) bool {
|
|
return row["name"] == "OverrideMe.app"
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ubuntu dpkg installed python packages are filtered out",
|
|
host: &fleet.Host{ID: 1, Platform: "ubuntu"},
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_linux": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_linux": []map[string]string{
|
|
{
|
|
"name": "python3-twisted",
|
|
"version": "20.3.0-2",
|
|
"source": "deb_packages",
|
|
},
|
|
{
|
|
"name": "Twisted", // duplicate of python3-twisted
|
|
"version": "20.3.0-2",
|
|
"source": "python_packages",
|
|
},
|
|
{
|
|
"name": "python3-setuptools",
|
|
"version": "50.3.2",
|
|
"source": "deb_packages",
|
|
},
|
|
{
|
|
"name": "setuptools",
|
|
"version": "50.3.2",
|
|
"source": "python_packages",
|
|
},
|
|
{
|
|
"name": "pillow",
|
|
"version": "8.1.0",
|
|
"source": "python_packages",
|
|
},
|
|
{
|
|
"name": "python3-urllib3",
|
|
"version": "1.26.2-2",
|
|
"source": "deb_packages",
|
|
},
|
|
},
|
|
},
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_linux": []map[string]string{
|
|
{
|
|
"name": "python3-twisted",
|
|
"version": "20.3.0-2",
|
|
"source": "deb_packages",
|
|
},
|
|
{
|
|
"name": "python3-setuptools",
|
|
"version": "50.3.2",
|
|
"source": "deb_packages",
|
|
},
|
|
{
|
|
"name": "python3-pillow", // renamed from pillow
|
|
"version": "8.1.0",
|
|
"source": "python_packages",
|
|
},
|
|
{
|
|
"name": "python3-urllib3",
|
|
"version": "1.26.2-2",
|
|
"source": "deb_packages",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "debian dpkg installed python packages are filtered out",
|
|
host: &fleet.Host{ID: 1, Platform: "debian"},
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_linux": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_linux": []map[string]string{
|
|
{
|
|
"name": "python3-twisted",
|
|
"version": "22.4.0-4",
|
|
"source": "deb_packages",
|
|
},
|
|
{
|
|
"name": "Twisted", // duplicate of python3-twisted
|
|
"version": "22.4.0-4",
|
|
"source": "python_packages",
|
|
},
|
|
// known issue below: names don't match so we don't deduplicate
|
|
{
|
|
"name": "python3-attr", // osquery source column is python-attrs
|
|
"version": "22.2.0-1",
|
|
"source": "deb_packages",
|
|
},
|
|
{
|
|
"name": "Attrs",
|
|
"version": "22.2.0",
|
|
"source": "python_packages",
|
|
},
|
|
},
|
|
},
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_linux": []map[string]string{
|
|
{
|
|
"name": "python3-twisted",
|
|
"version": "22.4.0-4",
|
|
"source": "deb_packages",
|
|
},
|
|
{
|
|
"name": "python3-attr",
|
|
"version": "22.2.0-1",
|
|
"source": "deb_packages",
|
|
},
|
|
{
|
|
"name": "python3-attrs",
|
|
"version": "22.2.0",
|
|
"source": "python_packages",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "non-ubuntu/debian installed python packages are NOT filtered out",
|
|
host: &fleet.Host{ID: 1, Platform: "rhel"},
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_linux": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_linux": []map[string]string{
|
|
{
|
|
"name": "python3-twisted",
|
|
"version": "20.3.0-2",
|
|
"source": "rpm_packages",
|
|
},
|
|
{
|
|
"name": "twisted", // duplicate of python3-twisted
|
|
"version": "20.3.0-2",
|
|
"source": "python_packages",
|
|
},
|
|
{
|
|
"name": "pillow",
|
|
"version": "8.1.0",
|
|
"source": "python_packages",
|
|
},
|
|
{
|
|
"name": "python3-urllib3",
|
|
"version": "1.26.2-2",
|
|
"source": "rpm_packages",
|
|
},
|
|
},
|
|
},
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_linux": []map[string]string{
|
|
{
|
|
"name": "python3-twisted",
|
|
"version": "20.3.0-2",
|
|
"source": "rpm_packages",
|
|
},
|
|
{
|
|
"name": "twisted", // duplicate of python3-twisted
|
|
"version": "20.3.0-2",
|
|
"source": "python_packages",
|
|
},
|
|
{
|
|
"name": "pillow",
|
|
"version": "8.1.0",
|
|
"source": "python_packages",
|
|
},
|
|
{
|
|
"name": "python3-urllib3",
|
|
"version": "1.26.2-2",
|
|
"source": "rpm_packages",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "macos codesign query with cdhash_sha256",
|
|
host: &fleet.Host{ID: 1, Platform: "darwin"},
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_macos_codesign": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos_codesign": []map[string]string{
|
|
{
|
|
"path": "/Applications/Slack.app",
|
|
"team_identifier": "com.slack.slack",
|
|
"cdhash_sha256": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
},
|
|
},
|
|
},
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos_codesign": []map[string]string{
|
|
{
|
|
"path": "/Applications/Slack.app",
|
|
"team_identifier": "com.slack.slack",
|
|
"cdhash_sha256": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "macos codesign query with no cdhash_sha256 column",
|
|
host: &fleet.Host{ID: 1, Platform: "darwin"},
|
|
statusesIn: map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_macos_codesign": fleet.StatusOK,
|
|
},
|
|
resultsIn: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos_codesign": []map[string]string{
|
|
{
|
|
"path": "/Applications/Slack.app",
|
|
"team_identifier": "com.slack.slack",
|
|
},
|
|
},
|
|
},
|
|
resultsOut: fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_macos_codesign": []map[string]string{
|
|
{
|
|
"path": "/Applications/Slack.app",
|
|
"team_identifier": "com.slack.slack",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
host := &fleet.Host{ID: 1}
|
|
if tc.host != nil {
|
|
host = tc.host
|
|
}
|
|
preProcessSoftwareResults(host, tc.resultsIn, tc.statusesIn, tc.messagesIn, tc.overrides, log.NewNopLogger())
|
|
require.Equal(t, tc.resultsOut, tc.resultsIn)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetailQueriesLinuxDistros(t *testing.T) {
|
|
for _, linuxPlatform := range fleet.HostLinuxOSs {
|
|
m := expectedDetailQueriesForPlatform(linuxPlatform)
|
|
require.Contains(t, m, "users")
|
|
require.Contains(t, m, "network_interface_unix")
|
|
require.Contains(t, m, "disk_space_unix")
|
|
require.Contains(t, m, "os_unix_like")
|
|
require.Contains(t, m, "orbit_info")
|
|
require.Contains(t, m, "disk_encryption_linux")
|
|
require.Contains(t, m, "software_vscode_extensions")
|
|
require.Contains(t, m, "software_linux")
|
|
}
|
|
}
|
|
|
|
// Benchmark function
|
|
func BenchmarkFindPackDelimiterStringCommon(b *testing.B) {
|
|
// Input data for benchmarking
|
|
input := "pack/Global/Foo"
|
|
|
|
// Run the benchmark
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
findPackDelimiterString(input)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFindPackDelimiterStringTeamPack(b *testing.B) {
|
|
// Input data for benchmarking
|
|
input := "packGlobalGlobalGlobalGlobal" // global pack delimiter, global team, query name global
|
|
|
|
// Run the benchmark
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
findPackDelimiterString(input)
|
|
}
|
|
}
|
|
|
|
func mockUbuntuResults() fleet.OsqueryDistributedQueryResults {
|
|
results := fleet.OsqueryDistributedQueryResults{
|
|
hostDetailQueryPrefix + "software_linux": make([]map[string]string, 0),
|
|
}
|
|
|
|
// Adding 40 python packages with matching deb packages
|
|
// Adding 2 python packages without matching deb packages
|
|
for i := 1; i <= 42; i++ {
|
|
pythonPkg := fmt.Sprintf("package%d", i)
|
|
results[hostDetailQueryPrefix+"software_linux"] = append(results[hostDetailQueryPrefix+"software_linux"], map[string]string{
|
|
"source": "python_packages",
|
|
"name": pythonPkg,
|
|
})
|
|
}
|
|
|
|
// Adding 1500 deb packages, with the first 40 matching python packages
|
|
for i := 1; i <= 1500; i++ {
|
|
var debPkg string
|
|
if i <= 38 { // Match first 38 python packages
|
|
debPkg = fmt.Sprintf("python3-package%d", i)
|
|
} else { // Non-python packages
|
|
debPkg = fmt.Sprintf("unrelated_package%d", i)
|
|
}
|
|
results[hostDetailQueryPrefix+"software_linux"] = append(results[hostDetailQueryPrefix+"software_linux"], map[string]string{
|
|
"source": "deb_packages",
|
|
"name": debPkg,
|
|
})
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func BenchmarkPreprocessUbuntuPythonPackageFilter(b *testing.B) {
|
|
platform := "ubuntu"
|
|
results := mockUbuntuResults()
|
|
statuses := map[string]fleet.OsqueryStatus{
|
|
hostDetailQueryPrefix + "software_linux": fleet.StatusOK,
|
|
}
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
preProcessSoftwareResults(&fleet.Host{ID: 1, Platform: platform}, results, statuses, nil, nil, log.NewNopLogger())
|
|
}
|
|
}
|