2022-03-07 18:10:55 +00:00
package service
import (
2022-03-08 16:27:38 +00:00
"bytes"
2022-03-07 18:10:55 +00:00
"context"
2024-04-09 21:33:44 +00:00
"database/sql"
2022-03-07 18:10:55 +00:00
"encoding/json"
"errors"
2022-03-08 16:27:38 +00:00
"fmt"
2022-08-30 11:13:09 +00:00
"io"
2024-01-30 00:38:10 +00:00
"net/http"
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
"os"
2022-03-08 16:27:38 +00:00
"reflect"
"sort"
"strings"
"sync"
2022-03-07 18:10:55 +00:00
"testing"
2022-03-08 16:27:38 +00:00
"time"
2022-03-07 18:10:55 +00:00
2022-03-08 16:27:38 +00:00
"github.com/WatchBeam/clock"
2025-07-16 18:08:27 +00:00
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/types"
2022-03-08 16:27:38 +00:00
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/config"
2022-03-07 18:10:55 +00:00
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
2022-03-08 16:27:38 +00:00
fleetLogging "github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
2022-06-13 20:29:32 +00:00
"github.com/fleetdm/fleet/v4/server/datastore/mysqlredis"
2022-03-08 16:27:38 +00:00
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
2022-03-07 18:10:55 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2022-06-13 13:18:03 +00:00
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
2022-03-07 18:10:55 +00:00
"github.com/fleetdm/fleet/v4/server/mock"
2022-07-01 11:08:03 +00:00
mockresult "github.com/fleetdm/fleet/v4/server/mock/mockresult"
2022-03-07 18:10:55 +00:00
"github.com/fleetdm/fleet/v4/server/ptr"
2022-03-08 16:27:38 +00:00
"github.com/fleetdm/fleet/v4/server/pubsub"
2022-05-10 15:29:17 +00:00
"github.com/fleetdm/fleet/v4/server/service/async"
2025-02-13 20:32:19 +00:00
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
2022-03-08 16:27:38 +00:00
"github.com/fleetdm/fleet/v4/server/service/osquery_utils"
"github.com/fleetdm/fleet/v4/server/service/redis_policy_set"
2024-06-17 13:27:31 +00:00
"github.com/go-kit/log"
"github.com/go-kit/log/level"
2022-03-07 18:10:55 +00:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetClientConfig ( t * testing . T ) {
ds := new ( mock . Store )
2023-07-14 17:37:09 +00:00
ds . TeamAgentOptionsFunc = func ( ctx context . Context , teamID uint ) ( * json . RawMessage , error ) {
return nil , nil
}
2022-03-07 18:10:55 +00:00
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return [ ] * fleet . Pack { } , nil
}
2022-11-23 15:04:06 +00:00
ds . ListScheduledQueriesInPackFunc = func ( ctx context . Context , pid uint ) ( fleet . ScheduledQueryList , error ) {
2022-03-07 18:10:55 +00:00
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
}
}
2025-03-11 13:45:01 +00:00
ds . ListScheduledQueriesForAgentsFunc = func ( ctx context . Context , teamID * uint , hostID * uint , queryReportsDisabled bool ) ( [ ] * fleet . Query , error ) {
2023-07-20 12:06:43 +00:00
if teamID == nil {
2023-07-14 17:37:09 +00:00
return nil , nil
}
return [ ] * fleet . Query {
{
Query : "SELECT 1 FROM table_1" ,
Name : "Some strings carry more weight than others" ,
2023-07-25 00:17:20 +00:00
Interval : 10 ,
2023-07-14 17:37:09 +00:00
Platform : "linux" ,
MinOsqueryVersion : "5.12.2" ,
2023-07-25 00:17:20 +00:00
Logging : "snapshot" ,
2023-07-14 17:37:09 +00:00
TeamID : ptr . Uint ( 1 ) ,
} ,
{
2023-07-25 00:17:20 +00:00
Query : "SELECT 1 FROM table_2" ,
Name : "You shall not pass" ,
Interval : 20 ,
Platform : "macos" ,
Logging : "differential" ,
TeamID : ptr . Uint ( 1 ) ,
2023-07-14 17:37:09 +00:00
} ,
} , nil
}
2022-03-07 18:10:55 +00:00
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
}
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-07 18:10:55 +00:00
2022-11-15 14:08:05 +00:00
ctx1 := hostctx . NewContext ( ctx , & fleet . Host { ID : 1 } )
ctx2 := hostctx . NewContext ( ctx , & fleet . Host { ID : 2 } )
2023-07-14 17:37:09 +00:00
ctx3 := hostctx . NewContext ( ctx , & fleet . Host { ID : 1 , TeamID : ptr . Uint ( 1 ) } )
2022-03-07 18:10:55 +00:00
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 ) ) ,
)
2023-07-14 17:37:09 +00:00
// Check scheduled queries are loaded properly
conf , err = svc . GetClientConfig ( ctx3 )
require . NoError ( t , err )
2024-12-02 14:14:10 +00:00
assert . JSONEq ( t , ` {
2023-07-14 17:37:09 +00:00
"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 }
}
} ,
2023-07-25 00:17:20 +00:00
"team-1" : {
2023-07-14 17:37:09 +00:00
"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" : ""
}
}
2024-12-02 14:14:10 +00:00
}
2023-07-14 17:37:09 +00:00
} ` ,
string ( conf [ "packs" ] . ( json . RawMessage ) ) ,
)
2022-03-07 18:10:55 +00:00
}
func TestAgentOptionsForHost ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-07 18:10:55 +00:00
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" ,
}
2022-11-15 14:08:05 +00:00
opt , err := svc . AgentOptionsForHost ( ctx , host . TeamID , host . Platform )
2022-03-07 18:10:55 +00:00
require . NoError ( t , err )
assert . JSONEq ( t , ` { "foo":"override"} ` , string ( opt ) )
host . Platform = "windows"
2022-11-15 14:08:05 +00:00
opt , err = svc . AgentOptionsForHost ( ctx , host . TeamID , host . Platform )
2022-03-07 18:10:55 +00:00
require . NoError ( t , err )
assert . JSONEq ( t , ` { "foo":"bar"} ` , string ( opt ) )
// Should take gobal option with no team
host . TeamID = nil
2022-11-15 14:08:05 +00:00
opt , err = svc . AgentOptionsForHost ( ctx , host . TeamID , host . Platform )
2022-03-07 18:10:55 +00:00
require . NoError ( t , err )
assert . JSONEq ( t , ` { "baz":"bar"} ` , string ( opt ) )
host . Platform = "darwin"
2022-11-15 14:08:05 +00:00
opt , err = svc . AgentOptionsForHost ( ctx , host . TeamID , host . Platform )
2022-03-07 18:10:55 +00:00
require . NoError ( t , err )
assert . JSONEq ( t , ` { "foo":"override2"} ` , string ( opt ) )
}
2022-03-08 16:27:38 +00:00
2025-08-22 15:14:32 +00:00
var allDetailQueries = osquery_utils . GetDetailQueries ( context . Background ( ) , config . FleetConfig { Vulnerabilities : config . VulnerabilitiesConfig { DisableWinOSVulnerabilities : true } } , nil , & fleet . Features {
EnableHostUsers : true ,
EnableSoftwareInventory : true ,
} , osquery_utils . Integrations { } , nil )
2022-11-02 19:44:02 +00:00
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
}
2022-03-08 16:27:38 +00:00
2025-09-05 19:05:19 +00:00
func TestEnrollOsquery ( t * testing . T ) {
2022-03-08 16:27:38 +00:00
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" )
}
}
2025-09-05 19:05:19 +00:00
ds . EnrollOsqueryFunc = func ( ctx context . Context , opts ... fleet . DatastoreEnrollOsqueryOption ) ( * fleet . Host , error ) {
enrollConfig := & fleet . DatastoreEnrollOsqueryConfig { }
2025-07-15 20:22:02 +00:00
for _ , opt := range opts {
opt ( enrollConfig )
}
assert . Equal ( t , ptr . Uint ( 3 ) , enrollConfig . TeamID )
2022-03-08 16:27:38 +00:00
return & fleet . Host {
2025-07-15 20:22:02 +00:00
OsqueryHostID : & enrollConfig . OsqueryHostID , NodeKey : & enrollConfig . NodeKey ,
2022-03-08 16:27:38 +00:00
} , nil
}
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
2025-07-16 18:08:27 +00:00
ds . GetHostIdentityCertByNameFunc = func ( ctx context . Context , name string ) ( * types . HostIdentityCertificate , error ) {
return nil , newNotFoundError ( )
}
2022-03-08 16:27:38 +00:00
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-08 16:27:38 +00:00
2025-09-05 19:05:19 +00:00
nodeKey , err := svc . EnrollOsquery ( ctx , "valid_secret" , "host123" , nil )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
assert . NotEmpty ( t , nodeKey )
}
2025-09-05 19:05:19 +00:00
func TestEnrollOsqueryEnforceLimit ( t * testing . T ) {
2022-06-13 20:29:32 +00:00
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" )
}
}
2025-09-05 19:05:19 +00:00
ds . EnrollOsqueryFunc = func ( ctx context . Context , opts ... fleet . DatastoreEnrollOsqueryOption ) ( * fleet . Host , error ) {
enrollConfig := & fleet . DatastoreEnrollOsqueryConfig { }
2025-07-15 20:22:02 +00:00
for _ , opt := range opts {
opt ( enrollConfig )
}
2022-06-13 20:29:32 +00:00
hostIDSeq ++
return & fleet . Host {
2025-07-15 20:22:02 +00:00
ID : hostIDSeq , OsqueryHostID : & enrollConfig . OsqueryHostID , NodeKey : & enrollConfig . NodeKey ,
2022-06-13 20:29:32 +00:00
} , 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
}
2025-07-16 18:08:27 +00:00
ds . GetHostIdentityCertByNameFunc = func ( ctx context . Context , name string ) ( * types . HostIdentityCertificate , error ) {
return nil , newNotFoundError ( )
}
2022-06-13 20:29:32 +00:00
redisWrapDS := mysqlredis . New ( ds , pool , mysqlredis . WithEnforcedHostLimit ( maxHosts ) )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , redisWrapDS , nil , nil , & TestServerOpts {
2022-06-13 20:29:32 +00:00
EnrollHostLimiter : redisWrapDS ,
License : & fleet . LicenseInfo { DeviceCount : maxHosts } ,
} )
2022-11-15 14:08:05 +00:00
ctx = viewer . NewContext ( ctx , viewer . Viewer {
User : & fleet . User {
ID : 0 ,
GlobalRole : ptr . String ( fleet . RoleAdmin ) ,
} ,
} )
2022-06-13 20:29:32 +00:00
2025-09-05 19:05:19 +00:00
nodeKey , err := svc . EnrollOsquery ( ctx , "valid_secret" , "host001" , nil )
2022-06-13 20:29:32 +00:00
require . NoError ( t , err )
assert . NotEmpty ( t , nodeKey )
2025-09-05 19:05:19 +00:00
nodeKey , err = svc . EnrollOsquery ( ctx , "valid_secret" , "host002" , nil )
2022-06-13 20:29:32 +00:00
require . NoError ( t , err )
assert . NotEmpty ( t , nodeKey )
2025-09-05 19:05:19 +00:00
_ , err = svc . EnrollOsquery ( ctx , "valid_secret" , "host003" , nil )
2022-06-13 20:29:32 +00:00
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
2025-09-05 19:05:19 +00:00
nodeKey , err = svc . EnrollOsquery ( ctx , "valid_secret" , "host003" , nil )
2022-06-13 20:29:32 +00:00
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 )
} )
}
2025-09-05 19:05:19 +00:00
func TestEnrollOsqueryIncorrectEnrollSecret ( t * testing . T ) {
2022-03-08 16:27:38 +00:00
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" )
}
}
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-08 16:27:38 +00:00
2025-09-05 19:05:19 +00:00
nodeKey , err := svc . EnrollOsquery ( ctx , "not_correct" , "host123" , nil )
2022-03-08 16:27:38 +00:00
assert . NotNil ( t , err )
assert . Empty ( t , nodeKey )
}
2025-09-05 19:05:19 +00:00
func TestEnrollOsqueryDetails ( t * testing . T ) {
2022-03-08 16:27:38 +00:00
ds := new ( mock . Store )
ds . VerifyEnrollSecretFunc = func ( ctx context . Context , secret string ) ( * fleet . EnrollSecret , error ) {
return & fleet . EnrollSecret { } , nil
}
2025-09-05 19:05:19 +00:00
ds . EnrollOsqueryFunc = func ( ctx context . Context , opts ... fleet . DatastoreEnrollOsqueryOption ) ( * fleet . Host , error ) {
config := & fleet . DatastoreEnrollOsqueryConfig { }
2025-07-15 20:22:02 +00:00
for _ , opt := range opts {
opt ( config )
}
2022-03-08 16:27:38 +00:00
return & fleet . Host {
2025-07-15 20:22:02 +00:00
OsqueryHostID : & config . OsqueryHostID , NodeKey : & config . NodeKey ,
2022-03-08 16:27:38 +00:00
} , 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
}
2025-07-16 18:08:27 +00:00
ds . GetHostIdentityCertByNameFunc = func ( ctx context . Context , name string ) ( * types . HostIdentityCertificate , error ) {
return nil , newNotFoundError ( )
}
2022-03-08 16:27:38 +00:00
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-08 16:27:38 +00:00
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" } ,
}
2025-09-05 19:05:19 +00:00
nodeKey , err := svc . EnrollOsquery ( ctx , "" , "host123" , details )
2022-03-08 16:27:38 +00:00
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 )
OpenTelemetry minor improvements (#32324)
Fixes #32313
OpenTelemetry Tracing
- Added tracing to async task collectors: FlushHostsLastSeen,
collectHostsLastSeen, collectLabelQueryExecutions,
collectPolicyQueryExecutions, collectScheduledQueryStats
- Updated HTTP middleware to use OTEL semantic convention for span names
({method} {route})
- Added OTELEnabled() helper to FleetConfig
Optimizations
- Reduced OTEL batch size from 512 to 256 spans to prevent gRPC message
size errors
- Enabled gzip compression for trace exports
NOTE: I tried to improve OTEL instrumentation for cron jobs, but it got
too complicated due to goroutines in `schedule.go` so that effort should
be separate. We do have SQL instrumentation for cron jobs, but we are
missing root spans for cron jobs as a whole.
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
## Testing
- [x] QA'd all new/changed functionality manually
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Expanded OpenTelemetry tracing for async tasks (host last seen, label
membership, policy membership, scheduled query stats) to provide richer
observability.
* More descriptive HTTP span names using “METHOD /route” for clearer
trace analysis.
* **Bug Fixes**
* Improved OTLP gRPC exporter reliability by enabling gzip compression
and reducing export batch size, mitigating intermittent gRPC errors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-29 00:32:46 +00:00
task := async . NewTask ( ds , nil , clock . C , nil )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil , & TestServerOpts { Task : task } )
2022-03-08 16:27:38 +00:00
var gotKey string
2025-07-16 18:08:27 +00:00
host := fleet . Host { ID : 1 , Hostname : "foobar" , HasHostIdentityCert : ptr . Bool ( false ) }
2022-03-08 16:27:38 +00:00
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
}
2022-11-15 14:08:05 +00:00
_ , _ , err := svc . AuthenticateHost ( ctx , "test" )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
assert . Equal ( t , "test" , gotKey )
assert . False ( t , ds . MarkHostsSeenFuncInvoked )
2025-07-16 18:08:27 +00:00
host = fleet . Host { ID : 7 , Hostname : "foobar" , HasHostIdentityCert : ptr . Bool ( false ) }
2022-11-15 14:08:05 +00:00
_ , _ , err = svc . AuthenticateHost ( ctx , "floobar" )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
assert . Equal ( t , "floobar" , gotKey )
assert . False ( t , ds . MarkHostsSeenFuncInvoked )
// Host checks in twice
2025-07-16 18:08:27 +00:00
host = fleet . Host { ID : 7 , Hostname : "foobar" , HasHostIdentityCert : ptr . Bool ( false ) }
2022-11-15 14:08:05 +00:00
_ , _ , err = svc . AuthenticateHost ( ctx , "floobar" )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
assert . Equal ( t , "floobar" , gotKey )
assert . False ( t , ds . MarkHostsSeenFuncInvoked )
2022-11-15 14:08:05 +00:00
err = task . FlushHostsLastSeen ( ctx , time . Now ( ) )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
assert . True ( t , ds . MarkHostsSeenFuncInvoked )
2022-05-10 15:29:17 +00:00
ds . MarkHostsSeenFuncInvoked = false
2022-03-08 16:27:38 +00:00
assert . ElementsMatch ( t , [ ] uint { 1 , 7 } , gotHostIDs )
2022-11-15 14:08:05 +00:00
err = task . FlushHostsLastSeen ( ctx , time . Now ( ) )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
assert . True ( t , ds . MarkHostsSeenFuncInvoked )
require . Len ( t , gotHostIDs , 0 )
}
func TestAuthenticateHostFailure ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-08 16:27:38 +00:00
ds . LoadHostByNodeKeyFunc = func ( ctx context . Context , nodeKey string ) ( * fleet . Host , error ) {
return nil , errors . New ( "not found" )
}
2022-11-15 14:08:05 +00:00
_ , _ , err := svc . AuthenticateHost ( ctx , "test" )
2022-03-08 16:27:38 +00:00
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 )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-08 16:27:38 +00:00
// Hack to get at the service internals and modify the writer
serv := ( ( svc . ( validationMiddleware ) ) . Service ) . ( * Service )
testLogger := & testJSONLogger { }
2022-12-23 22:04:13 +00:00
serv . osqueryLogWriter = & OsqueryLogger { Status : testLogger }
2022-03-08 16:27:38 +00:00
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 { }
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , & host )
2022-03-08 16:27:38 +00:00
err = serv . SubmitStatusLogs ( ctx , status )
require . NoError ( t , err )
assert . Equal ( t , status , testLogger . logs )
}
2023-12-04 15:31:35 +00:00
func TestSubmitResultLogsToLogDestination ( t * testing . T ) {
2022-03-08 16:27:38 +00:00
ds := new ( mock . Store )
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
svc , ctx := newTestService ( t , ds , nil , nil , & TestServerOpts { Logger : log . NewJSONLogger ( os . Stdout ) } )
2022-03-08 16:27:38 +00:00
2023-10-11 18:20:06 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
2023-10-25 22:20:27 +00:00
ds . QueryByNameFunc = func ( ctx context . Context , teamID * uint , name string ) ( * fleet . Query , error ) {
2023-10-11 18:20:06 +00:00
switch {
2024-05-03 20:37:55 +00:00
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
2023-10-11 18:20:06 +00:00
case teamID == nil && ( name == "time" || name == "system_info" || name == "encrypted" || name == "hosts" ) :
return & fleet . Query {
2025-05-01 19:21:30 +00:00
ID : uint ( name [ 0 ] ) ,
2023-10-11 18:20:06 +00:00
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
2025-01-31 19:26:24 +00:00
case teamID == nil && name == "All USB devices" :
return & fleet . Query {
ID : 777 ,
Name : name ,
AutomationsEnabled : true ,
Logging : fleet . LoggingSnapshot ,
} , nil
2024-01-18 15:41:06 +00:00
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
2023-10-11 18:20:06 +00:00
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 ( )
}
}
2023-10-25 22:20:27 +00:00
ds . ResultCountForQueryFunc = func ( ctx context . Context , queryID uint ) ( int , error ) {
return 0 , nil
}
2024-05-03 20:37:55 +00:00
teamQueryResultsStored := false
2024-06-14 15:24:01 +00:00
ds . OverwriteQueryResultRowsFunc = func ( ctx context . Context , rows [ ] * fleet . ScheduledQueryResultRow , maxQueryReportRows int ) error {
2023-10-11 18:20:06 +00:00
if len ( rows ) == 0 {
return nil
}
2025-01-31 19:26:24 +00:00
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 )
}
2025-07-16 18:08:27 +00:00
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 ) )
2025-01-31 19:26:24 +00:00
}
2023-10-11 18:20:06 +00:00
switch {
2024-05-03 20:37:55 +00:00
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
2023-10-11 18:20:06 +00:00
case rows [ 0 ] . QueryID == 123 :
require . Len ( t , rows , 1 )
require . Equal ( t , uint ( 999 ) , rows [ 0 ] . HostID )
require . NotZero ( t , rows [ 0 ] . LastFetched )
2023-12-11 22:33:31 +00:00
require . JSONEq ( t , ` { "hour":"20","minutes":"8"} ` , string ( * rows [ 0 ] . Data ) )
2023-10-11 18:20:06 +00:00
case rows [ 0 ] . QueryID == 444 :
require . Len ( t , rows , 2 )
require . Equal ( t , uint ( 999 ) , rows [ 0 ] . HostID )
require . NotZero ( t , rows [ 0 ] . LastFetched )
2023-12-11 22:33:31 +00:00
require . JSONEq ( t , ` { "hour":"20","minutes":"8"} ` , string ( * rows [ 0 ] . Data ) )
2023-10-11 18:20:06 +00:00
require . Equal ( t , uint ( 999 ) , rows [ 1 ] . HostID )
require . Equal ( t , uint ( 444 ) , rows [ 1 ] . QueryID )
require . NotZero ( t , rows [ 1 ] . LastFetched )
2023-12-11 22:33:31 +00:00
require . JSONEq ( t , ` { "hour":"21","minutes":"9"} ` , string ( * rows [ 1 ] . Data ) )
2023-10-11 18:20:06 +00:00
}
return nil
}
2022-03-08 16:27:38 +00:00
// Hack to get at the service internals and modify the writer
serv := ( ( svc . ( validationMiddleware ) ) . Service ) . ( * Service )
testLogger := & testJSONLogger { }
2022-12-23 22:04:13 +00:00
serv . osqueryLogWriter = & OsqueryLogger { Result : testLogger }
2022-03-08 16:27:38 +00:00
2023-10-11 18:20:06 +00:00
validLogResults := [ ] string {
2025-05-01 19:21:30 +00:00
` { "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} ` ,
2023-10-11 18:20:06 +00:00
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
` { "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"} ` ,
2023-10-11 18:20:06 +00:00
// These results belong to the same query but have 1 second difference.
2025-05-01 19:21:30 +00:00
` { "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} ` ,
2023-10-11 18:20:06 +00:00
2025-05-01 19:21:30 +00:00
` { "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} ` ,
2023-10-11 18:20:06 +00:00
2025-05-01 19:21:30 +00:00
` { "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} ` ,
2023-10-11 18:20:06 +00:00
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
// 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"}} ` ,
2023-12-07 16:05:59 +00:00
// 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"}} ` ,
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
// 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`).
2023-12-07 16:05:59 +00:00
` { "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"} ` ,
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
` { "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"}} ` ,
2024-05-03 20:37:55 +00:00
// 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).
2025-05-01 19:21:30 +00:00
` { "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} ` ,
2025-01-31 19:26:24 +00:00
// 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.
2025-05-01 19:21:30 +00:00
` { "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} ` ,
2022-03-08 16:27:38 +00:00
}
2023-10-11 18:20:06 +00:00
logJSON := fmt . Sprintf ( "[%s]" , strings . Join ( validLogResults , "," ) )
resultWithInvalidJSON := [ ] byte ( "foobar:\n\t123" )
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
resultWithInvalidJSONLong := [ ] byte ( "foobar:\n\t1233333333333333333333333333333333333333333333333333333333" )
2023-10-11 18:20:06 +00:00
// 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.
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
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"}} ` )
2023-10-11 18:20:06 +00:00
// 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"}} ` )
2022-03-08 16:27:38 +00:00
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
var validResults [ ] json . RawMessage
err := json . Unmarshal ( [ ] byte ( logJSON ) , & validResults )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-10-11 18:20:06 +00:00
host := fleet . Host {
2024-05-03 20:37:55 +00:00
ID : 999 ,
TeamID : nil , // Global host.
2023-10-11 18:20:06 +00:00
}
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , & host )
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
// 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 )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
Fix osquery result logging when queries are configured outside of Fleet (#15393)
#15168
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests.
- [X] Manual QA for all new/changed functionality.
The issue can be reproduced by running `osqueryd` with
`--config_plugin=filesystem --config_path=/path/to/config.json`
This means the osquery config is fetched from a file rather than from
Fleet's agent settings.
The `/path/to/config.json` has the agent settings, e.g.:
```
{
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
},
"options": {
"disable_distributed": false,
"distributed_interval": 10,
"distributed_plugin": "tls",
"distributed_tls_max_attempts": 3,
"logger_tls_endpoint": "/api/osquery/log",
"logger_tls_period": 10,
"pack_delimiter": "/"
},
"schedule": {
"USB devices": {
"query": "SELECT * FROM usb_devices;",
"interval": 15
},
"OS version": {
"query": "SELECT * FROM os_version;",
"interval": 10
}
},
"packs": {
"Elsewhere": {
"queries": {
"Osquery Info": {
"query": "SELECT * FROM osquery_info;",
"interval": 30,
"platform": "",
"version": "",
"snapshot": true
}
}
}
}
}
```
The three queries should be logged to Fleet's configured result logging
destination (default is `filesystem`).
2023-12-04 14:18:49 +00:00
assert . Equal ( t , validResults , testLogger . logs )
2024-05-03 20:37:55 +00:00
//
// 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 )
2022-03-08 16:27:38 +00:00
}
2023-10-10 12:44:03 +00:00
func TestSaveResultLogsToQueryReports ( t * testing . T ) {
ds := new ( mock . Store )
svc , ctx := newTestService ( t , ds , nil , nil )
2023-10-11 18:20:06 +00:00
// Hack to get at the private methods
serv := ( ( svc . ( validationMiddleware ) ) . Service ) . ( * Service )
2023-10-10 12:44:03 +00:00
host := fleet . Host { }
ctx = hostctx . NewContext ( ctx , & host )
2023-10-11 18:20:06 +00:00
results := [ ] * fleet . ScheduledQueryResult {
{
QueryName : "pack/Global/Uptime" ,
OsqueryHostID : "1379f59d98f4" ,
2023-12-11 22:33:31 +00:00
Snapshot : [ ] * json . RawMessage {
ptr . RawMessage ( json . RawMessage ( ` { "hour":"20","minutes":"8"} ` ) ) ,
2023-10-11 18:20:06 +00:00
} ,
UnixTime : 1484078931 ,
} ,
}
2023-10-10 12:44:03 +00:00
// Results not saved if DiscardData is true in Query
2023-10-11 18:20:06 +00:00
discardDataFalse := map [ string ] * fleet . Query {
"pack/Global/Uptime" : {
ID : 1 ,
DiscardData : true ,
Logging : fleet . LoggingSnapshot ,
} ,
2023-10-10 12:44:03 +00:00
}
2024-06-14 15:24:01 +00:00
serv . saveResultLogsToQueryReports ( ctx , results , discardDataFalse , fleet . DefaultMaxQueryReportRows )
2023-10-11 18:20:06 +00:00
assert . False ( t , ds . OverwriteQueryResultRowsFuncInvoked )
2023-10-10 12:44:03 +00:00
// Happy Path: Results saved
2023-10-11 18:20:06 +00:00
discardDataTrue := map [ string ] * fleet . Query {
"pack/Global/Uptime" : {
ID : 1 ,
DiscardData : false ,
Logging : fleet . LoggingSnapshot ,
} ,
2023-10-10 12:44:03 +00:00
}
2024-06-14 15:24:01 +00:00
ds . OverwriteQueryResultRowsFunc = func ( ctx context . Context , rows [ ] * fleet . ScheduledQueryResultRow , maxQueryReportRows int ) error {
2023-10-10 12:44:03 +00:00
return nil
}
2023-10-25 22:20:27 +00:00
ds . ResultCountForQueryFunc = func ( ctx context . Context , queryID uint ) ( int , error ) {
return 0 , nil
}
2024-06-14 15:24:01 +00:00
serv . saveResultLogsToQueryReports ( ctx , results , discardDataTrue , fleet . DefaultMaxQueryReportRows )
2023-10-10 12:44:03 +00:00
require . True ( t , ds . OverwriteQueryResultRowsFuncInvoked )
}
2023-12-04 15:31:35 +00:00
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
}
2024-06-14 15:24:01 +00:00
ds . OverwriteQueryResultRowsFunc = func ( ctx context . Context , rows [ ] * fleet . ScheduledQueryResultRow , maxQueryReportRows int ) error {
2023-12-04 15:31:35 +00:00
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
}
2024-06-14 15:24:01 +00:00
ds . OverwriteQueryResultRowsFunc = func ( ctx context . Context , rows [ ] * fleet . ScheduledQueryResultRow , maxQueryReportRows int ) error {
2023-12-04 15:31:35 +00:00
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 )
}
2024-03-14 19:33:12 +00:00
type failingLogger struct { }
2024-01-30 00:38:10 +00:00
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
}
2024-06-14 15:24:01 +00:00
ds . OverwriteQueryResultRowsFunc = func ( ctx context . Context , rows [ ] * fleet . ScheduledQueryResultRow , maxQueryReportRows int ) error {
2024-01-30 00:38:10 +00:00
return nil
}
// Expect an error when unable to write to logging destination.
err = svc . SubmitResultLogs ( ctx , results )
require . Error ( t , err )
2025-02-13 20:32:19 +00:00
assert . Equal ( t , http . StatusRequestEntityTooLarge , err . ( * endpoint_utils . OsqueryError ) . Status ( ) )
2024-01-30 00:38:10 +00:00
}
2023-10-10 12:44:03 +00:00
func TestGetQueryNameAndTeamIDFromResult ( t * testing . T ) {
tests := [ ] struct {
input string
expectedID * uint
expectedName string
hasErr bool
} {
2024-05-01 15:42:03 +00:00
{ "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
2024-01-18 15:41:06 +00:00
{ "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=//
2023-10-10 12:44:03 +00:00
{ "pack/team-1/QueryWith/Slash" , ptr . Uint ( 1 ) , "QueryWith/Slash" , false } ,
2024-01-18 15:41:06 +00:00
{ "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
2024-05-01 15:42:03 +00:00
{ "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
2023-12-07 16:05:59 +00:00
2023-10-10 12:44:03 +00:00
{ "InvalidString" , nil , "" , true } ,
{ "Invalid/Query" , nil , "" , true } ,
2024-01-18 15:41:06 +00:00
{ "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 } ,
2023-12-07 16:05:59 +00:00
// 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 } ,
2024-01-18 15:41:06 +00:00
{ "packFOOBARPackNameFOOBARQueryWith/Slash" , nil , "" , true } , // pack_delimiter=FOOBAR
2023-10-10 12:44:03 +00:00
}
for _ , tt := range tests {
2024-01-18 15:41:06 +00:00
tt := tt
2023-10-10 12:44:03 +00:00
t . Run ( tt . input , func ( t * testing . T ) {
2024-01-18 15:41:06 +00:00
t . Parallel ( )
2023-10-10 12:44:03 +00:00
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
2023-10-11 18:20:06 +00:00
input [ ] * fleet . ScheduledQueryResult
expected [ ] * fleet . ScheduledQueryResult
2023-10-10 12:44:03 +00:00
} {
{
name : "basic test" ,
2023-10-11 18:20:06 +00:00
input : [ ] * fleet . ScheduledQueryResult {
2023-10-10 12:44:03 +00:00
{ QueryName : "test1" , UnixTime : 1 } ,
{ QueryName : "test1" , UnixTime : 2 } ,
{ QueryName : "test1" , UnixTime : 3 } ,
{ QueryName : "test2" , UnixTime : 1 } ,
{ QueryName : "test2" , UnixTime : 2 } ,
{ QueryName : "test2" , UnixTime : 3 } ,
} ,
2023-10-11 18:20:06 +00:00
expected : [ ] * fleet . ScheduledQueryResult {
2023-10-10 12:44:03 +00:00
{ QueryName : "test1" , UnixTime : 3 } ,
{ QueryName : "test2" , UnixTime : 3 } ,
} ,
} ,
{
name : "out of order test" ,
2023-10-11 18:20:06 +00:00
input : [ ] * fleet . ScheduledQueryResult {
2023-10-10 12:44:03 +00:00
{ QueryName : "test1" , UnixTime : 2 } ,
{ QueryName : "test1" , UnixTime : 3 } ,
{ QueryName : "test1" , UnixTime : 1 } ,
{ QueryName : "test2" , UnixTime : 3 } ,
{ QueryName : "test2" , UnixTime : 2 } ,
{ QueryName : "test2" , UnixTime : 1 } ,
} ,
2023-10-11 18:20:06 +00:00
expected : [ ] * fleet . ScheduledQueryResult {
2023-10-10 12:44:03 +00:00
{ QueryName : "test1" , UnixTime : 3 } ,
{ QueryName : "test2" , UnixTime : 3 } ,
} ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
results := getMostRecentResults ( tt . input )
2023-10-18 14:07:00 +00:00
assert . ElementsMatch ( t , tt . expected , results )
2023-10-10 12:44:03 +00:00
} )
}
}
2022-03-15 19:51:00 +00:00
func verifyDiscovery ( t * testing . T , queries , discovery map [ string ] string ) {
2024-05-30 16:10:16 +00:00
t . Helper ( )
2022-03-15 19:51:00 +00:00
assert . Equal ( t , len ( queries ) , len ( discovery ) )
// discoveryUsed holds the queries where we know use the distributed discovery feature.
discoveryUsed := map [ string ] struct { } {
2025-02-26 20:15:41 +00:00
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" : { } ,
2025-08-12 22:46:04 +00:00
hostDetailQueryPrefix + "software_rpm_last_opened_at" : { } ,
hostDetailQueryPrefix + "software_deb_last_opened_at" : { } ,
2022-03-15 19:51:00 +00:00
}
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 ] )
}
}
}
2022-03-08 16:27:38 +00:00
func TestHostDetailQueries ( t * testing . T ) {
2023-05-17 19:52:45 +00:00
ctx := context . Background ( )
2022-03-08 16:27:38 +00:00
ds := new ( mock . Store )
additional := json . RawMessage ( ` { "foobar": "select foo", "bim": "bam"} ` )
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
2024-04-26 18:05:34 +00:00
return & fleet . AppConfig { Features : fleet . Features {
AdditionalQueries : & additional ,
EnableHostUsers : true ,
EnableSoftwareInventory : true ,
} } , nil
2022-03-08 16:27:38 +00:00
}
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 ( ) ,
2022-12-26 21:32:39 +00:00
NodeKey : ptr . String ( "test_key" ) ,
2022-03-08 16:27:38 +00:00
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 ) ,
}
2023-05-17 19:52:45 +00:00
// detail_updated_at is now, so nothing gets returned by default
queries , discovery , err := svc . detailQueriesForHost ( ctx , & host )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
assert . Empty ( t , queries )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
// With refetch requested detail queries should be returned
host . RefetchRequested = true
2023-05-17 19:52:45 +00:00
queries , discovery , err = svc . detailQueriesForHost ( ctx , & host )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-05-17 19:52:45 +00:00
// +2: additional queries: bim, foobar
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 2 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
host . RefetchRequested = false
// Advance the time
mockClock . AddTime ( 1 * time . Hour + 1 * time . Minute )
2023-05-17 19:52:45 +00:00
// all queries returned now that detail udpated at is in the past
queries , discovery , err = svc . detailQueriesForHost ( ctx , & host )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
// +2: additional queries: bim, foobar
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 2 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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" ] )
2023-05-17 19:52:45 +00:00
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 )
2024-12-02 14:14:10 +00:00
// host is darwin so it gets only the darwin critical query
require . Equal ( t , 1 , len ( queries ) , distQueriesMapKeys ( queries ) )
2023-05-17 19:52:45 +00:00
for name := range criticalDetailQueries {
2024-12-02 14:14:10 +00:00
if strings . HasSuffix ( name , "_windows" ) {
continue
}
2023-05-17 19:52:45 +00:00
assert . Contains ( t , queries , hostDetailQueryPrefix + name )
}
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
}
2022-08-30 11:13:09 +00:00
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" ,
2022-12-26 21:32:39 +00:00
NodeKey : ptr . String ( "test_key" ) ,
2022-08-30 11:13:09 +00:00
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
}
2025-05-16 18:56:27 +00:00
ds . GetHostAwaitingConfigurationFunc = func ( ctx context . Context , hostuuid string ) ( bool , error ) {
return false , nil
}
2022-08-30 11:13:09 +00:00
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 }
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , lq , & TestServerOpts { License : license } )
2022-08-30 11:13:09 +00:00
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , & host )
2022-08-30 11:13:09 +00:00
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 }
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , lq , & TestServerOpts { License : license } )
2022-08-30 11:13:09 +00:00
host . TeamID = nil
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , & host )
2022-08-30 11:13:09 +00:00
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" )
} )
}
2022-03-08 16:27:38 +00:00
func TestGetDistributedQueriesMissingHost ( t * testing . T ) {
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , & mock . Store { } , nil , nil )
2022-03-08 16:27:38 +00:00
2022-11-15 14:08:05 +00:00
_ , _ , _ , err := svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NotNil ( t , err )
assert . Contains ( t , err . Error ( ) , "missing host" )
}
2023-12-14 15:38:54 +00:00
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
}
2025-05-16 18:56:27 +00:00
ds . GetHostAwaitingConfigurationFunc = func ( ctx context . Context , hostuuid string ) ( bool , error ) {
return false , nil
}
2023-12-14 15:38:54 +00:00
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 )
}
}
2022-03-08 16:27:38 +00:00
func TestLabelQueries ( t * testing . T ) {
mockClock := clock . NewMockClock ( )
ds := new ( mock . Store )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithClock ( t , ds , nil , lq , mockClock )
2022-03-08 16:27:38 +00:00
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 ) {
2024-04-26 18:05:34 +00:00
return & fleet . AppConfig { Features : fleet . Features {
EnableHostUsers : true ,
EnableSoftwareInventory : true ,
} } , nil
2022-03-08 16:27:38 +00:00
}
2025-05-16 18:56:27 +00:00
ds . GetHostAwaitingConfigurationFunc = func ( ctx context . Context , hostuuid string ) ( bool , error ) {
return false , nil
}
2022-03-08 16:27:38 +00:00
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 )
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-08 16:27:38 +00:00
// With a new host, we should get the detail queries (and accelerate
// should be turned on so that we can quickly fill labels)
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err := svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
// +1 for the fleet_no_policies_wildcard query.
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 1 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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 )
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
require . Len ( t , queries , 1 ) // fleet_no_policies_wildcard query
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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
}
2022-09-08 18:04:02 +00:00
// Now we should get the label queries
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
// +1 for the fleet_no_policies_wildcard query.
require . Len ( t , queries , 3 + 1 )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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 { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
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 { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
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 )
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
require . Len ( t , queries , 1 ) // fleet_no_policies_wildcard query
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
assert . Zero ( t , acc )
// With refetch requested details+label queries should be returned.
host . RefetchRequested = true
ctx = hostctx . NewContext ( ctx , host )
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
// +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 ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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 { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
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.
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
require . Len ( t , queries , 1 ) // fleet_no_policies_wildcard query
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
assert . Zero ( t , acc )
}
func TestDetailQueriesWithEmptyStrings ( t * testing . T ) {
ds := new ( mock . Store )
mockClock := clock . NewMockClock ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithClock ( t , ds , nil , lq , mockClock )
2022-03-08 16:27:38 +00:00
host := & fleet . Host {
2025-09-18 19:39:15 +00:00
ID : 1 ,
Platform : "windows" ,
OsqueryHostID : ptr . String ( "very_random" ) ,
2022-03-08 16:27:38 +00:00
}
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-08 16:27:38 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
2024-04-26 18:05:34 +00:00
return & fleet . AppConfig { Features : fleet . Features {
EnableHostUsers : true ,
EnableSoftwareInventory : true ,
} } , nil
2022-03-08 16:27:38 +00:00
}
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
}
2025-05-16 18:56:27 +00:00
ds . GetHostAwaitingConfigurationFunc = func ( ctx context . Context , hostuuid string ) ( bool , error ) {
return false , nil
}
2022-03-08 16:27:38 +00:00
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
if id != 1 {
return nil , errors . New ( "not found" )
}
return host , nil
}
2025-09-18 19:39:15 +00:00
ds . ListSetupExperienceResultsByHostUUIDFunc = func ( ctx context . Context , hostUUID string ) ( [ ] * fleet . SetupExperienceStatusResult , error ) {
return nil , nil
}
2022-03-08 16:27:38 +00:00
lq . On ( "QueriesForHost" , host . ID ) . Return ( map [ string ] string { } , nil )
// With a new host, we should get the detail queries (and accelerated
// queries)
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err := svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
// +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 ) ) {
2022-06-28 18:11:49 +00:00
// 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.
2022-11-02 19:44:02 +00:00
require . ElementsMatch ( t , osqueryMapKeys ( expected ) , distQueriesMapKeys ( queries ) )
2022-06-28 18:11:49 +00:00
}
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
assert . NotZero ( t , acc )
resultJSON := `
{
2022-10-27 22:34:49 +00:00
"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" ,
2025-03-26 20:23:50 +00:00
"hardware_serial" : "NEW_HW_SRL" ,
2022-10-27 22:34:49 +00:00
"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"
}
]
2022-03-08 16:27:38 +00:00
}
`
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
2023-12-13 20:46:59 +00:00
require . NoError (
t , svc . SubmitDistributedQueryResults ( ctx , results , map [ string ] fleet . OsqueryStatus { } , map [ string ] string { } , map [ string ] * fleet . Stats { } ) ,
)
2022-03-08 16:27:38 +00:00
// 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 )
2025-03-26 20:23:50 +00:00
assert . Equal ( t , "NEW_HW_SRL" , gotHost . HardwareSerial )
2022-03-08 16:27:38 +00:00
// 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 )
2022-09-08 18:04:02 +00:00
// Now no detail queries should be required
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
require . Len ( t , queries , 1 ) // fleet_no_policies_wildcard query
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
assert . Zero ( t , acc )
// Advance clock and queries should exist again
mockClock . AddTime ( 1 * time . Hour + 1 * time . Minute )
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-06-28 18:11:49 +00:00
// somehow confusingly, the query response above changed the host's platform
2022-10-27 22:34:49 +00:00
// from windows to darwin
2023-08-31 13:58:50 +00:00
// +1 due to fleet_no_policies_wildcard query.
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( gotHost . Platform ) ) + 1 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
assert . Zero ( t , acc )
}
func TestDetailQueries ( t * testing . T ) {
ds := new ( mock . Store )
mockClock := clock . NewMockClock ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithClock ( t , ds , nil , lq , mockClock )
2022-03-08 16:27:38 +00:00
host := & fleet . Host {
2024-07-10 14:01:25 +00:00
ID : 1 ,
Platform : "linux" ,
2025-03-26 20:23:50 +00:00
HardwareSerial : "HW_SRL" ,
2025-09-04 15:58:47 +00:00
OsqueryHostID : ptr . String ( "foobar" ) ,
2022-03-08 16:27:38 +00:00
}
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-08 16:27:38 +00:00
lq . On ( "QueriesForHost" , host . ID ) . Return ( map [ string ] string { } , nil )
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
2024-04-26 18:05:34 +00:00
return & fleet . AppConfig { Features : fleet . Features {
EnableHostUsers : true ,
EnableSoftwareInventory : true ,
} } , nil
2022-03-08 16:27:38 +00:00
}
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
}
2025-07-16 18:08:27 +00:00
ds . SetOrUpdateMDMDataFunc = func ( ctx context . Context , hostID uint , isServer , enrolled bool , serverURL string , installedFromDep bool , name string ,
2025-07-22 21:24:19 +00:00
fleetEnrollmentRef string , isPersonalEnrollment bool ,
) error {
2022-03-08 16:27:38 +00:00
require . True ( t , enrolled )
require . False ( t , installedFromDep )
require . Equal ( t , "hi.com" , serverURL )
2023-12-07 18:36:32 +00:00
require . Empty ( t , fleetEnrollmentRef )
2025-07-22 21:24:19 +00:00
require . False ( t , isPersonalEnrollment )
2022-03-08 16:27:38 +00:00
return nil
}
2022-08-29 18:40:16 +00:00
ds . SetOrUpdateMunkiInfoFunc = func ( ctx context . Context , hostID uint , version string , errs , warns [ ] string ) error {
2022-03-08 16:27:38 +00:00
require . Equal ( t , "3.4.5" , version )
return nil
}
2024-04-09 21:33:44 +00:00
ds . SetOrUpdateHostOrbitInfoFunc = func (
ctx context . Context , hostID uint , version string , desktopVersion sql . NullString , scriptsEnabled sql . NullBool ,
) error {
2022-10-24 16:12:56 +00:00
require . Equal ( t , "42" , version )
2024-04-09 21:33:44 +00:00
require . Equal ( t , sql . NullString { String : "1.2.3" , Valid : true } , desktopVersion )
require . Equal ( t , sql . NullBool { Bool : true , Valid : true } , scriptsEnabled )
2022-10-24 16:12:56 +00:00
return nil
}
2022-03-15 19:51:00 +00:00
ds . SetOrUpdateDeviceAuthTokenFunc = func ( ctx context . Context , hostID uint , authToken string ) error {
require . Equal ( t , uint ( 1 ) , hostID )
require . Equal ( t , "foo" , authToken )
return nil
}
2023-12-22 18:46:33 +00:00
ds . SetOrUpdateHostDisksSpaceFunc = func ( ctx context . Context , hostID uint , gigsAvailable , percentAvailable , gigsTotal float64 ) error {
2022-09-21 19:16:31 +00:00
require . Equal ( t , 277.0 , gigsAvailable )
require . Equal ( t , 56.0 , percentAvailable )
2023-12-22 18:46:33 +00:00
require . Equal ( t , 500.1 , gigsTotal )
2022-09-21 19:16:31 +00:00
return nil
}
2025-08-26 15:31:06 +00:00
ds . GetNanoMDMUserEnrollmentUsernameAndUUIDFunc = func ( ctx context . Context , hostUUID string ) ( string , string , error ) {
return "" , "" , nil
}
2022-03-08 16:27:38 +00:00
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
if id != 1 {
return nil , errors . New ( "not found" )
}
return host , nil
}
2025-05-16 18:56:27 +00:00
ds . GetHostAwaitingConfigurationFunc = func ( ctx context . Context , hostuuid string ) ( bool , error ) {
return false , nil
}
2025-09-04 15:58:47 +00:00
ds . ListSetupExperienceResultsByHostUUIDFunc = func ( ctx context . Context , hostUUID string ) ( [ ] * fleet . SetupExperienceStatusResult , error ) {
return nil , nil
}
2022-03-08 16:27:38 +00:00
// With a new host, we should get the detail queries (and accelerated
// queries)
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err := svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2024-03-14 19:33:12 +00:00
// +1 for fleet_no_policies_wildcard
2024-04-26 18:05:34 +00:00
if expected := expectedDetailQueriesForPlatform ( host . Platform ) ; ! assert . Equal ( t , len ( expected ) + 1 , len ( queries ) ) {
2022-06-28 18:11:49 +00:00
// 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.
2022-11-02 19:44:02 +00:00
require . ElementsMatch ( t , osqueryMapKeys ( expected ) , distQueriesMapKeys ( queries ) )
2022-06-28 18:11:49 +00:00
}
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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" ,
2024-07-10 14:01:25 +00:00
"hardware_serial" : "-1" ,
2022-03-08 16:27:38 +00:00
"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" ,
2023-12-22 18:46:33 +00:00
"gigs_disk_space_available" : "277.0" ,
"gigs_total_disk_space" : "500.1"
2022-03-08 16:27:38 +00:00
}
] ,
"fleet_detail_query_mdm" : [
{
"enrolled" : "true" ,
"server_url" : "hi.com" ,
"installed_from_dep" : "false"
}
] ,
"fleet_detail_query_munki_info" : [
{
"version" : "3.4.5"
}
2022-03-15 19:51:00 +00:00
] ,
"fleet_detail_query_orbit_info" : [
{
2024-04-09 21:33:44 +00:00
"version" : "42" ,
"desktop_version" : "1.2.3" ,
"scripts_enabled" : "1"
2022-03-15 19:51:00 +00:00
}
2022-03-08 16:27:38 +00:00
]
}
`
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
2023-05-17 18:49:09 +00:00
ds . UpdateHostSoftwareFunc = func ( ctx context . Context , hostID uint , software [ ] fleet . Software ) ( * fleet . UpdateHostSoftwareDBResult , error ) {
2022-03-08 16:27:38 +00:00
if hostID != 1 {
2023-05-17 18:49:09 +00:00
return nil , errors . New ( "not found" )
2022-03-08 16:27:38 +00:00
}
gotSoftware = software
2023-05-17 18:49:09 +00:00
return nil , nil
}
2025-07-16 18:08:27 +00:00
ds . UpdateHostSoftwareInstalledPathsFunc = func ( ctx context . Context , hostID uint , paths map [ string ] struct { } ,
2025-07-22 21:24:19 +00:00
result * fleet . UpdateHostSoftwareDBResult ,
) error {
2022-03-08 16:27:38 +00:00
return nil
}
// Verify that results are ingested properly
2023-12-13 20:46:59 +00:00
require . NoError (
t , svc . SubmitDistributedQueryResults ( ctx , results , map [ string ] fleet . OsqueryStatus { } , map [ string ] string { } , map [ string ] * fleet . Stats { } ) ,
)
2022-03-08 16:27:38 +00:00
require . NotNil ( t , gotHost )
require . True ( t , ds . SetOrUpdateMDMDataFuncInvoked )
2022-08-29 18:40:16 +00:00
require . True ( t , ds . SetOrUpdateMunkiInfoFuncInvoked )
2022-09-21 19:16:31 +00:00
require . True ( t , ds . SetOrUpdateHostDisksSpaceFuncInvoked )
2022-03-08 16:27:38 +00:00
// 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 )
2024-07-10 14:01:25 +00:00
// The hardware serial should not have updated because return value was -1. See: https://github.com/fleetdm/fleet/issues/19789
2025-03-26 20:23:50 +00:00
assert . Equal ( t , "HW_SRL" , gotHost . HardwareSerial )
2022-03-08 16:27:38 +00:00
// 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 )
2022-09-08 18:04:02 +00:00
// Now no detail queries should be required
2022-03-08 16:27:38 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
require . Len ( t , queries , 1 ) // fleet_no_policies_wildcard query
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
assert . Zero ( t , acc )
// Advance clock and queries should exist again
mockClock . AddTime ( 1 * time . Hour + 1 * time . Minute )
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2024-03-14 19:33:12 +00:00
// +1 fleet_no_policies_wildcard query
2024-04-26 18:05:34 +00:00
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 1 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
assert . Zero ( t , acc )
}
2023-06-05 17:05:28 +00:00
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" } ,
2023-06-15 15:23:59 +00:00
{ "fleet_detail_query_mdm_disk_encryption_key_file_darwin" , "filevault_prk" } ,
{ "fleet_detail_query_mdm_disk_encryption_key_file_lines_darwin" , "file_lines" } ,
2023-06-05 17:05:28 +00:00
}
mdmEnabled := true
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { MDM : fleet . MDM { EnabledAndConfigured : mdmEnabled } } , nil
}
2025-08-26 15:31:06 +00:00
ds . GetNanoMDMUserEnrollmentUsernameAndUUIDFunc = func ( ctx context . Context , deviceID string ) ( string , string , error ) {
return "" , "" , nil
2025-06-25 13:51:43 +00:00
}
2023-06-05 17:05:28 +00:00
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 )
}
}
2022-03-08 16:27:38 +00:00
func TestNewDistributedQueryCampaign ( t * testing . T ) {
ds := new ( mock . Store )
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
2022-07-01 11:08:03 +00:00
rs := & mockresult . QueryResultStore {
2022-03-08 16:27:38 +00:00
HealthCheckFunc : func ( ) error {
return nil
} ,
}
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
mockClock := clock . NewMockClock ( )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithClock ( t , ds , rs , lq , mockClock )
2022-03-08 16:27:38 +00:00
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
2025-07-16 18:08:27 +00:00
ds . NewDistributedQueryCampaignTargetFunc = func ( ctx context . Context ,
2025-07-22 21:24:19 +00:00
target * fleet . DistributedQueryCampaignTarget ,
) ( * fleet . DistributedQueryCampaignTarget , error ) {
2022-03-08 16:27:38 +00:00
gotTargets = append ( gotTargets , target )
return target , nil
}
2025-07-16 18:08:27 +00:00
ds . CountHostsInTargetsFunc = func ( ctx context . Context , filter fleet . TeamFilter , targets fleet . HostTargets , now time . Time ) ( fleet . TargetMetrics ,
2025-07-22 21:24:19 +00:00
error ,
) {
2022-03-08 16:27:38 +00:00
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 )
2022-11-15 14:08:05 +00:00
viewerCtx := viewer . NewContext ( ctx , viewer . Viewer {
2022-03-08 16:27:38 +00:00
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 ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithClock ( t , ds , rs , lq , mockClock )
2022-03-08 16:27:38 +00:00
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 {
2025-09-18 19:39:15 +00:00
ID : 1 ,
Platform : "windows" ,
OsqueryHostID : ptr . String ( "other_random_value" ) ,
2022-03-08 16:27:38 +00:00
}
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 ) {
2024-04-26 18:05:34 +00:00
return & fleet . AppConfig { Features : fleet . Features {
EnableHostUsers : true ,
EnableSoftwareInventory : true ,
} } , nil
2022-03-08 16:27:38 +00:00
}
2025-09-18 19:39:15 +00:00
ds . ListSetupExperienceResultsByHostUUIDFunc = func ( ctx context . Context , hostUUID string ) ( [ ] * fleet . SetupExperienceStatusResult , error ) {
return nil , nil
}
2022-03-08 16:27:38 +00:00
2022-11-15 14:08:05 +00:00
hostCtx := hostctx . NewContext ( ctx , host )
2022-03-08 16:27:38 +00:00
lq . On ( "QueriesForHost" , uint ( 1 ) ) . Return (
map [ string ] string {
2024-10-18 17:38:26 +00:00
fmt . Sprint ( campaign . ID ) : "select * from time" ,
2022-03-08 16:27:38 +00:00
} ,
nil ,
)
2024-10-18 17:38:26 +00:00
lq . On ( "QueryCompletedByHost" , fmt . Sprint ( campaign . ID ) , host . ID ) . Return ( nil )
2022-03-08 16:27:38 +00:00
// Now we should get the active distributed query
2022-03-15 19:51:00 +00:00
queries , discovery , acc , err := svc . GetDistributedQueries ( hostCtx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
// +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 ) ) {
2022-06-28 18:11:49 +00:00
// 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.
2022-11-02 19:44:02 +00:00
require . ElementsMatch ( t , osqueryMapKeys ( expected ) , distQueriesMapKeys ( queries ) )
2022-06-28 18:11:49 +00:00
}
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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 ,
}
2023-12-13 20:46:59 +00:00
expectedStats := fleet . Stats {
UserTime : uint64 ( 1 ) ,
}
stats := map [ string ] * fleet . Stats {
queryKey : & expectedStats ,
}
2022-03-08 16:27:38 +00:00
// 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 )
Reduce size of `DistributedQueryResult` to improve live query performance (#11882)
This was found while working on #10957.
When running a live query, a lot of unused host data is stored in Redis
and sent on every live query result message via websockets. The frontend
and fleetctl just need `id`, `hostname` and `display_name`. (This
becomes worse every time we add new fields to the `Host` struct.)
Sample of one websocket message result when running `SELECT * from
osquery_info;`:
size in `main`: 2234 bytes
```
a["{\"type\":\"result\",\"data\":{\"distributed_query_execution_id\":57,\"host\":
{\"created_at\":\"2023-05-22T12:14:11Z\",\"updated_at\":\"2023-05-23T12:31:51Z\",
\"software_updated_at\":\"0001-01-01T00:00:00Z\",\"id\":106,\"detail_updated_at\":\"2023-05-23T11:50:04Z\",
\"label_updated_at\":\"2023-05-23T11:50:04Z\",\"policy_updated_at\":\"1970-01-02T00:00:00Z\",
\"last_enrolled_at\":\"2023-05-22T12:14:12Z\",
\"seen_time\":\"2023-05-23T09:52:23.876311-03:00\",\"refetch_requested\":false,
\"hostname\":\"lucass-macbook-pro.local\",\"uuid\":\"BD4DFA10-E334-41D9-8136-D2163A8FE588\",\"platform\":\"darwin\",\"osquery_version\":\"5.8.2\",\"os_version\":\"macOS 13.3.1\",\"build\":\"22E261\",\"platform_like\":\"darwin\",\"code_name\":\"\",
\"uptime\":91125000000000,\"memory\":34359738368,\"cpu_type\":\"x86_64h\",\"cpu_subtype\":\"Intel x86-64h Haswell\",\"cpu_brand\":\"Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz\",\"cpu_physical_cores\":4,\"cpu_logical_cores\":8,\"hardware_vendor\":\"Apple Inc.\",\"hardware_model\":\"MacBookPro16,2\",\"hardware_version\":\"1.0\",
\"hardware_serial\":\"0DPQR4HMD1FZ\",
\"computer_name\":\"Lucas’s MacBook Pro\",\"public_ip\":\"\",
\"primary_ip\":\"192.168.0.230\",\"primary_mac\":\"68:2f:67:8e:b6:1f\",
\"distributed_interval\":1,\"config_tls_refresh\":60,\"logger_tls_period\":10,\"team_id\":null,
\"pack_stats\":null,\"team_name\":null,
\"gigs_disk_space_available\":386.23,\"percent_disk_space_available\":40,
\"issues\":{\"total_issues_count\":0,\"failing_policies_count\":0},
\"mdm\":{\"enrollment_status\":null,\"server_url\":null,\"name\":\"\",\"encryption_key_available\":false},
\"status\":\"online\",\"display_text\":\"lucass-macbook-pro.local\",\"display_name\":\"Lucas’s MacBook Pro\"},
\"rows\":[{\"build_distro\":\"10.14\",\"build_platform\":\"darwin\",
\"config_hash\":\"b7ee9363a7c686e76e99ffb122e9c5241a791e69\",\"config_valid\":\"1\",
\"extensions\":\"active\",\"host_display_name\":\"Lucas’s MacBook Pro\",
\"host_hostname\":\"lucass-macbook-pro.local\",\"instance_id\":\"cde5de81-344b-4c76-b1c5-dae964fdd4f2\",\"pid\":\"8370\",\"platform_mask\":\"21\",\"start_time\":\"1684757652\",
\"uuid\":\"BD4DFA10-E334-41D9-8136-D2163A8FE588\",
\"version\":\"5.8.2\",\"watcher\":\"8364\"}],\"error\":null}}"]
```
vs. size of the message result on this branch: 675 bytes
```
a["{\"type\":\"result\",\"data\":{\"distributed_query_execution_id\":59,
\"host\":{\"id\":106,\"hostname\":\"lucass-macbook-pro.local\",
\"display_name\":\"Lucas’s MacBook Pro\"},
\"rows\":[{\"build_distro\":\"10.14\",\"build_platform\":\"darwin\",
\"config_hash\":\"f80dee827635db39077a458243379b3ad63311fd\",
\"config_valid\":\"1\",\"extensions\":\"active\",\"host_display_name\":\"Lucas’s MacBook Pro\",
\"host_hostname\":\"lucass-macbook-pro.local\",
\"instance_id\":\"cde5de81-344b-4c76-b1c5-dae964fdd4f2\",\"pid\":\"8370\",\"platform_mask\":\"21\",
\"start_time\":\"1684757652\",\"uuid\":\"BD4DFA10-E334-41D9-8136-D2163A8FE588\",\"version\":\"5.8.2\",
\"watcher\":\"8364\"}]}}"]
```
Manual tests included running with an old fleetctl running with a new
fleet server, and vice-versa, a new fleetctl running against an old
fleet server.
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- ~For Orbit and Fleet Desktop changes:~
- ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.~
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-05-25 11:11:53 +00:00
assert . Equal ( t , host . ID , res . Host . ID )
assert . Equal ( t , host . Hostname , res . Host . Hostname )
assert . Equal ( t , host . DisplayName ( ) , res . Host . DisplayName )
2023-12-13 20:46:59 +00:00
assert . Equal ( t , & expectedStats , res . Stats )
2022-03-08 16:27:38 +00:00
} 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 )
2023-12-13 20:46:59 +00:00
err = svc . SubmitDistributedQueryResults (
hostCtx , results , map [ string ] fleet . OsqueryStatus { } , map [ string ] string { } , stats ,
)
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-12-13 20:46:59 +00:00
// Sleep to ensure checks in the goroutine are actually done.
time . Sleep ( 10 * time . Millisecond )
2022-03-08 16:27:38 +00:00
}
func TestIngestDistributedQueryParseIdError ( t * testing . T ) {
mockClock := clock . NewMockClock ( )
ds := new ( mock . Store )
rs := pubsub . NewInmemQueryResults ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
svc := & Service {
ds : ds ,
resultStore : rs ,
liveQueryStore : lq ,
logger : log . NewNopLogger ( ) ,
clock : mockClock ,
}
host := fleet . Host { ID : 1 }
2023-12-13 20:46:59 +00:00
err := svc . ingestDistributedQuery ( context . Background ( ) , host , "bad_name" , [ ] map [ string ] string { } , "" , nil )
2022-03-08 16:27:38 +00:00
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 ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
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 }
2023-12-13 20:46:59 +00:00
err := svc . ingestDistributedQuery ( context . Background ( ) , host , "fleet_distributed_query_42" , [ ] map [ string ] string { } , "" , nil )
2022-03-08 16:27:38 +00:00
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 ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
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 }
2023-12-13 20:46:59 +00:00
err := svc . ingestDistributedQuery ( context . Background ( ) , host , "fleet_distributed_query_42" , [ ] map [ string ] string { } , "" , nil )
2022-03-08 16:27:38 +00:00
require . Error ( t , err )
2023-06-01 19:11:55 +00:00
assert . Contains ( t , err . Error ( ) , "campaignID=42 waiting for listener" )
2022-03-08 16:27:38 +00:00
}
func TestIngestDistributedQueryOrphanedCloseError ( t * testing . T ) {
mockClock := clock . NewMockClock ( )
ds := new ( mock . Store )
rs := pubsub . NewInmemQueryResults ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
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 }
2023-12-13 20:46:59 +00:00
err := svc . ingestDistributedQuery ( context . Background ( ) , host , "fleet_distributed_query_42" , [ ] map [ string ] string { } , "" , nil )
2022-03-08 16:27:38 +00:00
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 ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
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
}
2024-10-18 17:38:26 +00:00
lq . On ( "StopQuery" , fmt . Sprint ( campaign . ID ) ) . Return ( errors . New ( "failed" ) )
2022-03-08 16:27:38 +00:00
host := fleet . Host { ID : 1 }
2023-12-13 20:46:59 +00:00
err := svc . ingestDistributedQuery ( context . Background ( ) , host , "fleet_distributed_query_42" , [ ] map [ string ] string { } , "" , nil )
2022-03-08 16:27:38 +00:00
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 ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
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
}
2024-10-18 17:38:26 +00:00
lq . On ( "StopQuery" , fmt . Sprint ( campaign . ID ) ) . Return ( nil )
2022-03-08 16:27:38 +00:00
host := fleet . Host { ID : 1 }
2023-12-13 20:46:59 +00:00
err := svc . ingestDistributedQuery ( context . Background ( ) , host , "fleet_distributed_query_42" , [ ] map [ string ] string { } , "" , nil )
2022-03-08 16:27:38 +00:00
require . Error ( t , err )
2023-06-01 19:11:55 +00:00
assert . Contains ( t , err . Error ( ) , "campaignID=42 stopped" )
2022-03-08 16:27:38 +00:00
lq . AssertExpectations ( t )
}
func TestIngestDistributedQueryRecordCompletionError ( t * testing . T ) {
mockClock := clock . NewMockClock ( )
ds := new ( mock . Store )
rs := pubsub . NewInmemQueryResults ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
svc := & Service {
ds : ds ,
resultStore : rs ,
liveQueryStore : lq ,
logger : log . NewNopLogger ( ) ,
clock : mockClock ,
}
campaign := & fleet . DistributedQueryCampaign { ID : 42 }
host := fleet . Host { ID : 1 }
2024-10-18 17:38:26 +00:00
lq . On ( "QueryCompletedByHost" , fmt . Sprint ( campaign . ID ) , host . ID ) . Return ( errors . New ( "fail" ) )
2022-03-08 16:27:38 +00:00
go func ( ) {
ch , err := rs . ReadChannel ( context . Background ( ) , * campaign )
require . NoError ( t , err )
<- ch
} ( )
time . Sleep ( 10 * time . Millisecond )
2023-12-13 20:46:59 +00:00
err := svc . ingestDistributedQuery ( context . Background ( ) , host , "fleet_distributed_query_42" , [ ] map [ string ] string { } , "" , nil )
2022-03-08 16:27:38 +00:00
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 ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
svc := & Service {
ds : ds ,
resultStore : rs ,
liveQueryStore : lq ,
logger : log . NewNopLogger ( ) ,
clock : mockClock ,
}
campaign := & fleet . DistributedQueryCampaign { ID : 42 }
host := fleet . Host { ID : 1 }
2024-10-18 17:38:26 +00:00
lq . On ( "QueryCompletedByHost" , fmt . Sprint ( campaign . ID ) , host . ID ) . Return ( nil )
2022-03-08 16:27:38 +00:00
go func ( ) {
ch , err := rs . ReadChannel ( context . Background ( ) , * campaign )
require . NoError ( t , err )
<- ch
} ( )
time . Sleep ( 10 * time . Millisecond )
2023-12-13 20:46:59 +00:00
err := svc . ingestDistributedQuery ( context . Background ( ) , host , "fleet_distributed_query_42" , [ ] map [ string ] string { } , "" , nil )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
lq . AssertExpectations ( t )
}
func TestUpdateHostIntervals ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-08 16:27:38 +00:00
2025-03-11 13:45:01 +00:00
ds . ListScheduledQueriesForAgentsFunc = func ( ctx context . Context , teamID * uint , hostID * uint , queryReportsDisabled bool ) ( [ ] * fleet . Query , error ) {
2023-07-20 12:06:43 +00:00
return nil , nil
}
2022-03-08 16:27:38 +00:00
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return [ ] * fleet . Pack { } , nil
}
2024-12-11 18:50:28 +00:00
ds . ListQueriesFunc = func ( ctx context . Context , opt fleet . ListQueryOptions ) ( [ ] * fleet . Query , int , * fleet . PaginationMetadata , error ) {
return nil , 0 , nil , nil
2023-07-25 00:17:20 +00:00
}
2022-03-08 16:27:38 +00:00
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 ) {
2022-11-15 14:08:05 +00:00
ctx := hostctx . NewContext ( ctx , & fleet . Host {
2022-03-08 16:27:38 +00:00
ID : 1 ,
2022-12-26 21:32:39 +00:00
NodeKey : ptr . String ( "123456" ) ,
2022-03-08 16:27:38 +00:00
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
}
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ms , nil , nil )
2022-03-08 16:27:38 +00:00
_ , _ , err := svc . AuthenticateHost ( ctx , "" )
require . Error ( t , err )
2025-02-13 20:32:19 +00:00
require . True ( t , err . ( * endpoint_utils . OsqueryError ) . NodeInvalid ( ) )
2022-03-08 16:27:38 +00:00
ms . LoadHostByNodeKeyFunc = func ( ctx context . Context , nodeKey string ) ( * fleet . Host , error ) {
2025-07-16 18:08:27 +00:00
return & fleet . Host { ID : 1 , HasHostIdentityCert : ptr . Bool ( false ) } , nil
2022-03-08 16:27:38 +00:00
}
ms . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
2025-07-16 18:08:27 +00:00
ms . GetHostIdentityCertByNameFunc = func ( ctx context . Context , name string ) ( * types . HostIdentityCertificate , error ) {
return nil , newNotFoundError ( )
}
2022-03-08 16:27:38 +00:00
_ , _ , err = svc . AuthenticateHost ( ctx , "foo" )
require . NoError ( t , err )
// return not found error
ms . LoadHostByNodeKeyFunc = func ( ctx context . Context , nodeKey string ) ( * fleet . Host , error ) {
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , newNotFoundError ( )
2022-03-08 16:27:38 +00:00
}
_ , _ , err = svc . AuthenticateHost ( ctx , "foo" )
require . Error ( t , err )
2025-02-13 20:32:19 +00:00
require . True ( t , err . ( * endpoint_utils . OsqueryError ) . NodeInvalid ( ) )
2022-03-08 16:27:38 +00:00
// 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 )
2025-02-13 20:32:19 +00:00
require . False ( t , err . ( * endpoint_utils . OsqueryError ) . NodeInvalid ( ) )
2022-03-08 16:27:38 +00:00
}
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 )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-08 16:27:38 +00:00
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 { }
2022-11-15 14:08:05 +00:00
ctx = fleetLogging . NewContext ( ctx , lCtx )
2022-03-08 16:27:38 +00:00
ctx = hostctx . NewContext ( ctx , host )
err := svc . SubmitDistributedQueryResults (
ctx ,
map [ string ] [ ] map [ string ] string {
2022-10-27 22:34:49 +00:00
hostDetailQueryPrefix + "network_interface_unix" : { { "col1" : "val1" } } , // we need one detail query that updates hosts.
hostLabelQueryPrefix + "1" : { { "col1" : "val1" } } ,
hostAdditionalQueryPrefix + "1" : { { "col1" : "val1" } } ,
2022-03-08 16:27:38 +00:00
} ,
map [ string ] fleet . OsqueryStatus { } ,
map [ string ] string { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
require . NoError ( t , err )
lCtx . Log ( ctx , logger )
logs := buf . String ( )
parts := strings . Split ( strings . TrimSpace ( logs ) , "\n" )
require . Len ( t , parts , 1 )
2022-10-27 22:34:49 +00:00
var logData map [ string ] interface { }
2022-03-08 16:27:38 +00:00
err = json . Unmarshal ( [ ] byte ( parts [ 0 ] ) , & logData )
require . NoError ( t , err )
2022-10-27 22:34:49 +00:00
assert . Equal ( t , "something went wrong || something went wrong" , logData [ "err" ] )
assert . Equal ( t , "Missing authorization check" , logData [ "internal" ] )
2022-03-08 16:27:38 +00:00
}
func TestDistributedQueriesReloadsHostIfDetailsAreIn ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-03-08 16:27:38 +00:00
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
}
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-08 16:27:38 +00:00
err := svc . SubmitDistributedQueryResults (
ctx ,
map [ string ] [ ] map [ string ] string {
2022-10-27 22:34:49 +00:00
hostDetailQueryPrefix + "network_interface_unix" : { { "col1" : "val1" } } ,
2022-03-08 16:27:38 +00:00
} ,
map [ string ] fleet . OsqueryStatus { } ,
map [ string ] string { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
require . NoError ( t , err )
assert . True ( t , ds . UpdateHostFuncInvoked )
}
func TestObserversCanOnlyRunDistributedCampaigns ( t * testing . T ) {
ds := new ( mock . Store )
2022-07-01 11:08:03 +00:00
rs := & mockresult . QueryResultStore {
2022-03-08 16:27:38 +00:00
HealthCheckFunc : func ( ) error {
return nil
} ,
}
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
mockClock := clock . NewMockClock ( )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithClock ( t , ds , rs , lq , mockClock )
2022-03-08 16:27:38 +00:00
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
}
2022-11-15 14:08:05 +00:00
viewerCtx := viewer . NewContext ( ctx , viewer . Viewer {
2022-03-08 16:27:38 +00:00
User : & fleet . User { ID : 0 , GlobalRole : ptr . String ( fleet . RoleObserver ) } ,
} )
q := "select year, month, day, hour, minutes, seconds from time"
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2022-03-08 16:27:38 +00:00
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
}
2025-07-16 18:08:27 +00:00
ds . NewDistributedQueryCampaignTargetFunc = func ( ctx context . Context ,
2025-07-22 21:24:19 +00:00
target * fleet . DistributedQueryCampaignTarget ,
) ( * fleet . DistributedQueryCampaignTarget , error ) {
2022-03-08 16:27:38 +00:00
return target , nil
}
2025-07-16 18:08:27 +00:00
ds . CountHostsInTargetsFunc = func ( ctx context . Context , filter fleet . TeamFilter , targets fleet . HostTargets , now time . Time ) ( fleet . TargetMetrics ,
2025-07-22 21:24:19 +00:00
error ,
) {
2022-03-08 16:27:38 +00:00
return fleet . TargetMetrics { } , nil
}
ds . HostIDsInTargetsFunc = func ( ctx context . Context , filter fleet . TeamFilter , targets fleet . HostTargets ) ( [ ] uint , error ) {
return [ ] uint { 1 , 3 , 5 } , nil
}
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2022-03-08 16:27:38 +00:00
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 )
2022-07-01 11:08:03 +00:00
rs := & mockresult . QueryResultStore {
2022-03-08 16:27:38 +00:00
HealthCheckFunc : func ( ) error {
return nil
} ,
}
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
mockClock := clock . NewMockClock ( )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithClock ( t , ds , rs , lq , mockClock )
2022-03-08 16:27:38 +00:00
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
}
2022-11-15 14:08:05 +00:00
viewerCtx := viewer . NewContext ( ctx , viewer . Viewer {
2022-03-08 16:27:38 +00:00
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"
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2022-03-08 16:27:38 +00:00
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
}
2025-07-16 18:08:27 +00:00
ds . NewDistributedQueryCampaignTargetFunc = func ( ctx context . Context ,
2025-07-22 21:24:19 +00:00
target * fleet . DistributedQueryCampaignTarget ,
) ( * fleet . DistributedQueryCampaignTarget , error ) {
2022-03-08 16:27:38 +00:00
return target , nil
}
2025-07-16 18:08:27 +00:00
ds . CountHostsInTargetsFunc = func ( ctx context . Context , filter fleet . TeamFilter , targets fleet . HostTargets , now time . Time ) ( fleet . TargetMetrics ,
2025-07-22 21:24:19 +00:00
error ,
) {
2022-03-08 16:27:38 +00:00
return fleet . TargetMetrics { } , nil
}
ds . HostIDsInTargetsFunc = func ( ctx context . Context , filter fleet . TeamFilter , targets fleet . HostTargets ) ( [ ] uint , error ) {
return [ ] uint { 1 , 3 , 5 } , nil
}
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2022-03-08 16:27:38 +00:00
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 )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithClock ( t , ds , nil , lq , mockClock )
2022-03-08 16:27:38 +00:00
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 ) {
2024-04-26 18:05:34 +00:00
return & fleet . AppConfig { Features : fleet . Features {
EnableHostUsers : true ,
EnableSoftwareInventory : true ,
} } , nil
2022-03-08 16:27:38 +00:00
}
2025-05-16 18:56:27 +00:00
ds . GetHostAwaitingConfigurationFunc = func ( ctx context . Context , hostuuid string ) ( bool , error ) {
return false , nil
}
2022-03-08 16:27:38 +00:00
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 )
2025-07-16 18:08:27 +00:00
ds . RecordPolicyQueryExecutionsFunc = func ( ctx context . Context , gotHost * fleet . Host , results map [ uint ] * bool , updated time . Time ,
2025-07-22 21:24:19 +00:00
deferred bool ,
) error {
2022-03-08 16:27:38 +00:00
recordedResults = results
host = gotHost
return nil
}
2025-07-16 18:08:27 +00:00
ds . FlippingPoliciesForHostFunc = func ( ctx context . Context , hostID uint , incomingResults map [ uint ] * bool ) ( newFailing [ ] uint , newPassing [ ] uint ,
2025-07-22 21:24:19 +00:00
err error ,
) {
2022-03-08 16:27:38 +00:00
return nil , nil , nil
}
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
ds . TeamWithoutExtrasFunc = func ( ctx context . Context , id uint ) ( * fleet . Team , error ) {
return & fleet . Team { ID : 0 } , nil
}
2022-03-08 16:27:38 +00:00
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-08 16:27:38 +00:00
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err := svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
// +2 policy queries
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 2 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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 { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
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.
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
noPolicyResults ( queries )
// Let's move time forward, there should be policies now.
mockClock . AddTime ( 2 * time . Hour )
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
// +2 policy queries
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 2 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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 { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
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.
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
noPolicyResults ( queries )
// With refetch requested policy queries should be returned.
host . RefetchRequested = true
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
// +2 policy queries
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 2 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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 { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
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.
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
noPolicyResults ( queries )
}
2025-05-16 18:56:27 +00:00
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 )
}
2022-03-08 16:27:38 +00:00
func TestPolicyWebhooks ( t * testing . T ) {
mockClock := clock . NewMockClock ( )
ds := new ( mock . Store )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
pool := redistest . SetupRedis ( t , t . Name ( ) , false , false , false )
failingPolicySet := redis_policy_set . NewFailingTest ( t , pool )
testConfig := config . TestConfig ( )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithConfig ( t , ds , testConfig , nil , lq , & TestServerOpts {
2022-03-08 16:27:38 +00:00
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
}
2025-05-16 18:56:27 +00:00
ds . GetHostAwaitingConfigurationFunc = func ( ctx context . Context , hostuuid string ) ( bool , error ) {
return false , nil
}
2022-03-08 16:27:38 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig {
2022-08-25 16:41:50 +00:00
Features : fleet . Features {
2024-04-26 18:05:34 +00:00
EnableHostUsers : true ,
EnableSoftwareInventory : true ,
2022-03-08 16:27:38 +00:00
} ,
WebhookSettings : fleet . WebhookSettings {
FailingPoliciesWebhook : fleet . FailingPoliciesWebhookSettings {
Enable : true ,
PolicyIDs : [ ] uint { 1 , 2 , 3 } ,
} ,
} ,
} , nil
}
Allow configuring webhook policy automations for "No team" (#32129)
Fixes #32060
This PR adds:
- new default_team_config_json table
- caching of config from that table, including deep copy methods -- all
of this is not absolutely needed for this change since we are only using
`webhook_settings.failing_policies_webhook` here but added for
completeness/future
- teams/0 API updates
- GitOps updates
- generate gitops updates
Future PRs will add:
- ticket automation
- primo mode migration
- frontend changes
- documentation
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked table schema to confirm autoupdate
## New Fleet configuration settings
- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Configure failing-policy webhooks for “No team” via GitOps
(no-team.yml) and API, including enable/disable, destination URL, policy
IDs, and batch size; settings clear when omitted.
- GitOps and CLI now read/apply the real “No team” settings with dry-run
support.
- Policy automation evaluates hosts without a team and triggers “No
team” webhooks when applicable.
- GET/PATCH team 0 returns/accepts a minimal, webhook-focused config.
- Chores
- Added persistence and caching for the default “No team” configuration.
- Introduced a database table to store the default configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-28 21:38:27 +00:00
// Since the host doesn't have a team, DefaultTeamConfig will be called
ds . TeamWithoutExtrasFunc = func ( ctx context . Context , id uint ) ( * fleet . Team , error ) {
if id == 0 {
return & fleet . Team {
ID : 0 ,
Config : fleet . TeamConfig {
WebhookSettings : fleet . TeamWebhookSettings {
FailingPoliciesWebhook : fleet . FailingPoliciesWebhookSettings {
Enable : true ,
PolicyIDs : [ ] uint { 1 , 2 , 3 } ,
} ,
} ,
} ,
} , nil
}
return nil , nil
}
2022-03-08 16:27:38 +00:00
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 )
2025-07-16 18:08:27 +00:00
ds . RecordPolicyQueryExecutionsFunc = func ( ctx context . Context , gotHost * fleet . Host , results map [ uint ] * bool , updated time . Time ,
2025-07-22 21:24:19 +00:00
deferred bool ,
) error {
2022-03-08 16:27:38 +00:00
recordedResults = results
host = gotHost
return nil
}
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-08 16:27:38 +00:00
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err := svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
// +3 for policies
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 3 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
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 )
2025-07-16 18:08:27 +00:00
ds . FlippingPoliciesForHostFunc = func ( ctx context . Context , hostID uint , incomingResults map [ uint ] * bool ) ( newFailing [ ] uint , newPassing [ ] uint ,
2025-07-22 21:24:19 +00:00
err error ,
) {
2022-03-08 16:27:38 +00:00
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 { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
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.
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
noPolicyResults ( queries )
// Let's move time forward, there should be policies now.
mockClock . AddTime ( 2 * time . Hour )
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err = svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2022-11-02 19:44:02 +00:00
// +3 for policies
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 3 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
checkPolicyResults ( queries )
2025-07-16 18:08:27 +00:00
ds . FlippingPoliciesForHostFunc = func ( ctx context . Context , hostID uint , incomingResults map [ uint ] * bool ) ( newFailing [ ] uint , newPassing [ ] uint ,
2025-07-22 21:24:19 +00:00
err error ,
) {
2022-03-08 16:27:38 +00:00
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 { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
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 )
2025-07-16 18:08:27 +00:00
ds . FlippingPoliciesForHostFunc = func ( ctx context . Context , hostID uint , incomingResults map [ uint ] * bool ) ( newFailing [ ] uint , newPassing [ ] uint ,
2025-07-22 21:24:19 +00:00
err error ,
) {
2022-03-08 16:27:38 +00:00
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 { } ,
2023-12-13 20:46:59 +00:00
map [ string ] * fleet . Stats { } ,
2022-03-08 16:27:38 +00:00
)
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 )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
2022-03-08 16:27:38 +00:00
cfg := config . TestConfig ( )
buf := new ( bytes . Buffer )
logger := log . NewLogfmtLogger ( buf )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestServiceWithConfig ( t , ds , cfg , nil , lq , & TestServerOpts {
2022-03-08 16:27:38 +00:00
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 ) {
2024-04-26 18:05:34 +00:00
return & fleet . AppConfig { Features : fleet . Features {
EnableHostUsers : true ,
EnableSoftwareInventory : true ,
} } , nil
2022-03-08 16:27:38 +00:00
}
ds . PolicyQueriesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( map [ string ] string , error ) {
return map [ string ] string { } , nil
}
2025-05-16 18:56:27 +00:00
ds . GetHostAwaitingConfigurationFunc = func ( ctx context . Context , hostuuid string ) ( bool , error ) {
return false , nil
}
2022-03-08 16:27:38 +00:00
2022-11-15 14:08:05 +00:00
ctx = hostctx . NewContext ( ctx , host )
2022-03-08 16:27:38 +00:00
2022-03-15 19:51:00 +00:00
queries , discovery , _ , err := svc . GetDistributedQueries ( ctx )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
2023-08-31 13:58:50 +00:00
// +1 to account for the fleet_no_policies_wildcard query.
require . Equal ( t , len ( expectedDetailQueriesForPlatform ( host . Platform ) ) + 1 , len ( queries ) , distQueriesMapKeys ( queries ) )
2022-03-15 19:51:00 +00:00
verifyDiscovery ( t , queries , discovery )
2022-03-08 16:27:38 +00:00
2022-08-30 11:13:09 +00:00
logs , err := io . ReadAll ( buf )
2022-03-08 16:27:38 +00:00
require . NoError ( t , err )
require . Contains ( t , string ( logs ) , "level=error" )
require . Contains ( t , string ( logs ) , "failed to get queries for host" )
}
2022-05-31 15:56:51 +00:00
2022-06-28 18:11:49 +00:00
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
}
2024-03-14 19:33:12 +00:00
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" ,
}
2024-05-30 16:10:16 +00:00
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" ,
}
2024-03-14 19:33:12 +00:00
2025-02-26 20:15:41 +00:00
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" ,
}
2024-03-14 19:33:12 +00:00
for _ , tc := range [ ] struct {
2024-09-16 16:01:21 +00:00
name string
host * fleet . Host
2024-03-14 19:33:12 +00:00
resultsIn fleet . OsqueryDistributedQueryResults
statusesIn map [ string ] fleet . OsqueryStatus
messagesIn map [ string ] string
2024-05-30 16:10:16 +00:00
overrides map [ string ] osquery_utils . DetailQuery
2024-03-14 19:33:12 +00:00
resultsOut fleet . OsqueryDistributedQueryResults
} {
2025-02-26 20:15:41 +00:00
{
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 ,
} ,
} ,
} ,
2024-03-14 19:33:12 +00:00
{
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 ,
} ,
} ,
} ,
2024-05-30 16:10:16 +00:00
{
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"
} ,
} ,
} ,
} ,
2024-09-16 16:01:21 +00:00
{
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" ,
} ,
} ,
} ,
} ,
{
2024-10-29 16:16:09 +00:00
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" ,
} ,
} ,
} ,
} ,
{
2025-09-18 14:23:21 +00:00
name : "non-ubuntu/debian installed python packages are filtered out" ,
2024-09-16 16:01:21 +00:00
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" ,
} ,
{
2025-09-18 14:23:21 +00:00
"name" : "python3-pillow" ,
2024-09-16 16:01:21 +00:00
"version" : "8.1.0" ,
"source" : "python_packages" ,
} ,
{
"name" : "python3-urllib3" ,
"version" : "1.26.2-2" ,
"source" : "rpm_packages" ,
} ,
} ,
} ,
} ,
2025-05-21 04:38:59 +00:00
{
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" ,
} ,
} ,
} ,
} ,
2024-03-14 19:33:12 +00:00
} {
tc := tc
t . Run ( tc . name , func ( t * testing . T ) {
2024-09-16 16:01:21 +00:00
host := & fleet . Host { ID : 1 }
if tc . host != nil {
host = tc . host
}
2025-07-03 13:08:51 +00:00
preProcessSoftwareResults ( host , tc . resultsIn , tc . statusesIn , tc . messagesIn , tc . overrides , log . NewNopLogger ( ) )
2024-03-14 19:33:12 +00:00
require . Equal ( t , tc . resultsOut , tc . resultsIn )
} )
}
}
2024-04-26 18:05:34 +00:00
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" )
}
}
2024-05-01 15:42:03 +00:00
// 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 )
}
}
2024-09-16 16:01:21 +00:00
2025-07-03 13:08:51 +00:00
func mockUbuntuResults ( ) fleet . OsqueryDistributedQueryResults {
results := fleet . OsqueryDistributedQueryResults {
2024-09-16 16:01:21 +00:00
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 )
2025-07-03 13:08:51 +00:00
results [ hostDetailQueryPrefix + "software_linux" ] = append ( results [ hostDetailQueryPrefix + "software_linux" ] , map [ string ] string {
2024-09-16 16:01:21 +00:00
"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 )
}
2025-07-03 13:08:51 +00:00
results [ hostDetailQueryPrefix + "software_linux" ] = append ( results [ hostDetailQueryPrefix + "software_linux" ] , map [ string ] string {
2024-09-16 16:01:21 +00:00
"source" : "deb_packages" ,
"name" : debPkg ,
} )
}
return results
}
func BenchmarkPreprocessUbuntuPythonPackageFilter ( b * testing . B ) {
platform := "ubuntu"
results := mockUbuntuResults ( )
2025-07-03 13:08:51 +00:00
statuses := map [ string ] fleet . OsqueryStatus {
2024-09-16 16:01:21 +00:00
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 ( ) )
}
}