diff --git a/assets/images/phone-home.svg b/assets/images/phone-home.svg deleted file mode 100644 index a0335a1113..0000000000 --- a/assets/images/phone-home.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/changes/13489-implement-api-changes b/changes/13489-implement-api-changes new file mode 100644 index 0000000000..9b02ad5d73 --- /dev/null +++ b/changes/13489-implement-api-changes @@ -0,0 +1,2 @@ +* Add `GET /api/_version_/fleet/queries/{id}/report` API endpoint to retrieve the stored results of a given query. +* Add `discard_data` field to API query endpoints. diff --git a/changes/14286-detail-query-overrides b/changes/14286-detail-query-overrides new file mode 100644 index 0000000000..176dde9dbd --- /dev/null +++ b/changes/14286-detail-query-overrides @@ -0,0 +1 @@ +* Fixed a bug that would cause live queries to stall if a detail query override was set for a team. diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 50120b0434..b1a41965af 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -804,6 +804,20 @@ func newCleanupsAndAggregationSchedule( return verifyDiskEncryptionKeys(ctx, logger, ds, config) }, ), + schedule.WithJob("query_results_cleanup", func(ctx context.Context) error { + config, err := ds.AppConfig(ctx) + if err != nil { + return err + } + + if config.ServerSettings.QueryReportsDisabled { + if err = ds.CleanupGlobalDiscardQueryResults(ctx); err != nil { + return err + } + } + + return nil + }), ) return s, nil diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 8110239cac..62872eb917 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/sha256" - "database/sql" "encoding/json" "errors" "fmt" @@ -1204,9 +1203,9 @@ spec: // Apply queries. var appliedQueries []*fleet.Query ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { - return nil, sql.ErrNoRows + return nil, ¬FoundError{} } - ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error { + ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]bool) error { appliedQueries = queries return nil } @@ -1305,9 +1304,9 @@ func TestApplyQueries(t *testing.T) { var appliedQueries []*fleet.Query ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { - return nil, sql.ErrNoRows + return nil, ¬FoundError{} } - ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error { + ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]bool) error { appliedQueries = queries return nil } diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index baa6cf5c61..e94f145db2 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -15,14 +15,13 @@ import ( "github.com/fatih/color" "github.com/fleetdm/fleet/v4/pkg/rawjson" "github.com/fleetdm/fleet/v4/pkg/secure" - kithttp "github.com/go-kit/kit/transport/http" - "gopkg.in/guregu/null.v3" - "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/service" "github.com/ghodss/yaml" + kithttp "github.com/go-kit/kit/transport/http" "github.com/olekukonko/tablewriter" "github.com/urfave/cli/v2" + "gopkg.in/guregu/null.v3" ) const ( @@ -462,6 +461,7 @@ func getQueriesCommand() *cli.Command { MinOsqueryVersion: query.MinOsqueryVersion, AutomationsEnabled: query.AutomationsEnabled, Logging: query.Logging, + DiscardData: query.DiscardData, }); err != nil { return fmt.Errorf("unable to print query: %w", err) } diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index ac66265eae..1bd33060cd 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -1128,6 +1128,7 @@ kind: query spec: automations_enabled: false description: some desc + discard_data: false interval: 0 logging: "" min_osquery_version: "" @@ -1142,6 +1143,7 @@ kind: query spec: automations_enabled: false description: some desc 2 + discard_data: false interval: 0 logging: "" min_osquery_version: "" @@ -1156,6 +1158,7 @@ kind: query spec: automations_enabled: true description: some desc 4 + discard_data: false interval: 60 logging: differential_ignore_removals min_osquery_version: 5.3.0 @@ -1165,9 +1168,9 @@ spec: query: select 4; team: "" ` - expectedJSONGlobal := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}} -{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}} -{"kind":"query","apiVersion":"v1","spec":{"name":"query4","description":"some desc 4","query":"select 4;","team":"","interval":60,"observer_can_run":true,"platform":"darwin,windows","min_osquery_version":"5.3.0","automations_enabled":true,"logging":"differential_ignore_removals"}} + expectedJSONGlobal := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}} +{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}} +{"kind":"query","apiVersion":"v1","spec":{"name":"query4","description":"some desc 4","query":"select 4;","team":"","interval":60,"observer_can_run":true,"platform":"darwin,windows","min_osquery_version":"5.3.0","automations_enabled":true,"logging":"differential_ignore_removals","discard_data":false}} ` expectedTeam := `+--------+-------------+-----------+--------+----------------------------+ @@ -1191,6 +1194,7 @@ kind: query spec: automations_enabled: false description: some desc 3 + discard_data: false interval: 3600 logging: snapshot min_osquery_version: 5.4.0 @@ -1200,7 +1204,7 @@ spec: query: select 3; team: Foobar ` - expectedJSONTeam := `{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;","team":"Foobar","interval":3600,"observer_can_run":true,"platform":"darwin","min_osquery_version":"5.4.0","automations_enabled":false,"logging":"snapshot"}} + expectedJSONTeam := `{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;","team":"Foobar","interval":3600,"observer_can_run":true,"platform":"darwin","min_osquery_version":"5.4.0","automations_enabled":false,"logging":"snapshot","discard_data":false}} ` assert.Equal(t, expectedGlobal, runAppForTest(t, []string{"get", "queries"})) @@ -1276,6 +1280,7 @@ kind: query spec: automations_enabled: false description: some desc + discard_data: false interval: 0 logging: "" min_osquery_version: "" @@ -1285,7 +1290,7 @@ spec: query: select 1; team: "" ` - expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"globalQuery1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}} + expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"globalQuery1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}} ` assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "globalQuery1"})) @@ -1298,6 +1303,7 @@ kind: query spec: automations_enabled: true description: some team desc + discard_data: false interval: 3600 logging: differential min_osquery_version: 5.2.0 @@ -1307,7 +1313,7 @@ spec: query: select 2; team: Foobar ` - expectedJson = `{"kind":"query","apiVersion":"v1","spec":{"name":"teamQuery1","description":"some team desc","query":"select 2;","team":"Foobar","interval":3600,"observer_can_run":true,"platform":"linux","min_osquery_version":"5.2.0","automations_enabled":true,"logging":"differential"}} + expectedJson = `{"kind":"query","apiVersion":"v1","spec":{"name":"teamQuery1","description":"some team desc","query":"select 2;","team":"Foobar","interval":3600,"observer_can_run":true,"platform":"linux","min_osquery_version":"5.2.0","automations_enabled":true,"logging":"differential","discard_data":false}} ` assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--team", "1", "teamQuery1"})) @@ -1432,6 +1438,7 @@ kind: query spec: automations_enabled: false description: some desc 2 + discard_data: false interval: 0 logging: "" min_osquery_version: "" @@ -1441,7 +1448,7 @@ spec: query: select 2; team: "" ` - expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":true,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}} + expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":true,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}} ` assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"})) @@ -1509,6 +1516,7 @@ kind: query spec: automations_enabled: false description: some desc + discard_data: false interval: 0 logging: "" min_osquery_version: "" @@ -1523,6 +1531,7 @@ kind: query spec: automations_enabled: false description: some desc 2 + discard_data: false interval: 0 logging: "" min_osquery_version: "" @@ -1537,6 +1546,7 @@ kind: query spec: automations_enabled: false description: some desc 3 + discard_data: false interval: 0 logging: "" min_osquery_version: "" @@ -1546,9 +1556,9 @@ spec: query: select 3; team: "" ` - expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}} -{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":true,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}} -{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}} + expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}} +{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":true,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}} +{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}} ` assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"})) diff --git a/cmd/fleetctl/testdata/convert_output.yml b/cmd/fleetctl/testdata/convert_output.yml index b09a8a0847..b2181e2763 100644 --- a/cmd/fleetctl/testdata/convert_output.yml +++ b/cmd/fleetctl/testdata/convert_output.yml @@ -4,6 +4,7 @@ kind: query spec: automations_enabled: false description: Retrieves the list of application scheme/protocol-based IPC handlers. + discard_data: false interval: 86400 logging: "" min_osquery_version: 1.4.7 @@ -18,6 +19,7 @@ kind: query spec: automations_enabled: false description: Retrieves the current disk encryption status for the target system. + discard_data: false interval: 86400 logging: "" min_osquery_version: 1.4.5 @@ -32,6 +34,7 @@ kind: query spec: automations_enabled: false description: Retrieves the current disk encryption status for the target system. + discard_data: false interval: 300 logging: "" min_osquery_version: 1.4.5 @@ -46,6 +49,7 @@ kind: query spec: automations_enabled: false description: Retrieve basic information about the physical disks of a system. + discard_data: false interval: 86400 logging: "" min_osquery_version: 1.4.7 @@ -60,6 +64,7 @@ kind: query spec: automations_enabled: false description: Retrieves the current filters and chains per filter in the target system. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.5 @@ -76,6 +81,7 @@ spec: description: Retrieves all the daemons that will run in the start of the target OSX system. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.5 @@ -90,6 +96,7 @@ kind: query spec: automations_enabled: false description: Retrieves the list of listening ports. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.7 @@ -104,6 +111,7 @@ kind: query spec: automations_enabled: false description: Retrieves the list of listening ports. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.7 @@ -118,6 +126,7 @@ kind: query spec: automations_enabled: false description: Lists the application bundle that owns a sandbox label. + discard_data: false interval: 86400 logging: "" min_osquery_version: 1.4.7 @@ -132,6 +141,7 @@ kind: query spec: automations_enabled: false description: System resource usage limits. + discard_data: false interval: 300 logging: "" min_osquery_version: 1.4.7 @@ -146,6 +156,7 @@ kind: query spec: automations_enabled: false description: System uptime. + discard_data: false interval: 600 logging: "" min_osquery_version: 1.4.7 @@ -160,6 +171,7 @@ kind: query spec: automations_enabled: false description: System uptime. + discard_data: false interval: 600 logging: "" min_osquery_version: 1.4.7 @@ -174,6 +186,7 @@ kind: query spec: automations_enabled: false description: System uptime. + discard_data: false interval: 600 logging: "" min_osquery_version: 1.4.7 @@ -188,6 +201,7 @@ kind: query spec: automations_enabled: false description: System uptime. + discard_data: false interval: 600 logging: "" min_osquery_version: 1.4.7 @@ -202,6 +216,7 @@ kind: query spec: automations_enabled: false description: Lists the application bundle that owns a sandbox label. + discard_data: false interval: 86400 logging: "" min_osquery_version: 1.4.7 @@ -216,6 +231,7 @@ kind: query spec: automations_enabled: false description: List of all user groups. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.7 @@ -230,6 +246,7 @@ kind: query spec: automations_enabled: false description: List of all user groups. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.7 @@ -244,6 +261,7 @@ kind: query spec: automations_enabled: false description: List of all user groups. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.7 @@ -258,6 +276,7 @@ kind: query spec: automations_enabled: false description: List of all user groups. + discard_data: false interval: 3600 logging: "" min_osquery_version: "" @@ -272,6 +291,7 @@ kind: query spec: automations_enabled: false description: List of all user groups. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.7 @@ -286,6 +306,7 @@ kind: query spec: automations_enabled: false description: List of all user groups. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.7 @@ -300,6 +321,7 @@ kind: query spec: automations_enabled: false description: List of all user groups. + discard_data: false interval: 3600 logging: "" min_osquery_version: "" @@ -314,6 +336,7 @@ kind: query spec: automations_enabled: false description: List of all user groups. + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.7 @@ -328,6 +351,7 @@ kind: query spec: automations_enabled: false description: Extracted information from Windows crash logs (Minidumps). + discard_data: false interval: 3600 logging: "" min_osquery_version: 1.4.7 @@ -344,6 +368,7 @@ spec: description: Triggers one-off YARA query for files at the specified path. Requires one of sig_group, sigfile, or sigrule. + discard_data: false interval: 0 logging: "" min_osquery_version: 1.4.7 diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index ee97dd0701..146f64e82a 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -11,6 +11,7 @@ "server_settings": { "server_url": "", "live_query_disabled": false, + "query_reports_disabled": false, "enable_analytics": false, "deferred_save_host": false }, @@ -113,4 +114,4 @@ }, "scripts": null } -} +} \ No newline at end of file diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index 6f0a50d944..c6d273df1c 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -49,6 +49,7 @@ spec: deferred_save_host: false enable_analytics: false live_query_disabled: false + query_reports_disabled: false server_url: "" smtp_settings: authentication_method: "" diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index 681793c803..e6b915b875 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -11,6 +11,7 @@ "server_settings": { "server_url": "", "live_query_disabled": false, + "query_reports_disabled": false, "enable_analytics": false, "deferred_save_host": false }, @@ -175,4 +176,4 @@ } } } -} +} \ No newline at end of file diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index 28d3095a21..1ddb36b944 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -88,6 +88,7 @@ spec: deferred_save_host: false enable_analytics: false live_query_disabled: false + query_reports_disabled: false server_url: "" smtp_settings: authentication_method: "" diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 50d9b0a56e..c48bef7a3e 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -49,6 +49,7 @@ spec: deferred_save_host: false enable_analytics: false live_query_disabled: false + query_reports_disabled: false server_url: https://example.org smtp_settings: authentication_method: "" diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index f007e87ee8..05207bb30e 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -49,6 +49,7 @@ spec: deferred_save_host: false enable_analytics: false live_query_disabled: false + query_reports_disabled: false server_url: https://example.org smtp_settings: authentication_method: "" diff --git a/cmd/fleetctl/upgrade_packs_test.go b/cmd/fleetctl/upgrade_packs_test.go index 1c903737d7..c30ae6e2c3 100644 --- a/cmd/fleetctl/upgrade_packs_test.go +++ b/cmd/fleetctl/upgrade_packs_test.go @@ -255,7 +255,7 @@ func TestFleetctlUpgradePacks_EmptyPacks(t *testing.T) { outputFile := filepath.Join(tempDir, "output.yml") // write some dummy data in the file, it should be overwritten - err := os.WriteFile(outputFile, []byte("dummy"), 0644) + err := os.WriteFile(outputFile, []byte("dummy"), 0o644) require.NoError(t, err) got := runAppForTest(t, []string{"upgrade-packs", "-o", outputFile}) @@ -328,6 +328,7 @@ kind: query spec: automations_enabled: false description: (converted from pack "p1", query "q1") + discard_data: false interval: 0 logging: snapshot min_osquery_version: "" @@ -342,6 +343,7 @@ kind: query spec: automations_enabled: true description: (converted from pack "p2", query "q2") + discard_data: false interval: 90 logging: differential min_osquery_version: "" @@ -356,6 +358,7 @@ kind: query spec: automations_enabled: true description: (converted from pack "p2", query "q2") + discard_data: false interval: 90 logging: differential min_osquery_version: "" @@ -371,7 +374,7 @@ spec: outputFile := filepath.Join(tempDir, "output.yml") // write some dummy data in the file, it should be overwritten - err := os.WriteFile(outputFile, []byte("dummy"), 0644) + err := os.WriteFile(outputFile, []byte("dummy"), 0o644) require.NoError(t, err) testUpgradePacksTimestamp = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) @@ -394,7 +397,7 @@ func TestFleetctlUpgradePacks_NotAdmin(t *testing.T) { outputFile := filepath.Join(tempDir, "output.yml") // write some dummy data in the file, it should NOT be overwritten - err := os.WriteFile(outputFile, []byte("dummy"), 0644) + err := os.WriteFile(outputFile, []byte("dummy"), 0o644) require.NoError(t, err) // first try without the required output file flag @@ -422,7 +425,7 @@ func TestFleetctlUpgradePacks_NoPack(t *testing.T) { outputFile := filepath.Join(tempDir, "output.yml") // write some dummy data in the file, it should NOT be overwritten - err := os.WriteFile(outputFile, []byte("dummy"), 0644) + err := os.WriteFile(outputFile, []byte("dummy"), 0o644) require.NoError(t, err) got := runAppForTest(t, []string{"upgrade-packs", "-o", outputFile}) diff --git a/docker-compose.yml b/docker-compose.yml index c129bd7e80..8da77cbea6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,8 +20,7 @@ services: # Required for storage of Apple MDM installers. "--max_allowed_packet=536870912" ] - environment: - &mysql-default-environment + environment: &mysql-default-environment MYSQL_ROOT_PASSWORD: toor MYSQL_DATABASE: fleet MYSQL_USER: fleet @@ -41,7 +40,7 @@ services: "--log_output=TABLE", "--log-queries-not-using-indexes", "--innodb-file-per-table=OFF", - "--table-definition-cache=2048", + "--table-definition-cache=8192", # These 3 keys run MySQL with GTID consistency enforced to avoid issues with production deployments that use it. "--enforce-gtid-consistency=ON", "--log-bin=bin.log", @@ -142,3 +141,4 @@ volumes: mysql-persistent-volume: data-minio: + diff --git a/frontend/__mocks__/configMock.ts b/frontend/__mocks__/configMock.ts index f225356c8d..7b5131a2da 100644 --- a/frontend/__mocks__/configMock.ts +++ b/frontend/__mocks__/configMock.ts @@ -12,6 +12,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = { live_query_disabled: false, enable_analytics: true, deferred_save_host: false, + query_reports_disabled: false, }, smtp_settings: { enable_smtp: false, diff --git a/frontend/__mocks__/queryMock.ts b/frontend/__mocks__/queryMock.ts index df90eae93a..4d44c53347 100644 --- a/frontend/__mocks__/queryMock.ts +++ b/frontend/__mocks__/queryMock.ts @@ -12,6 +12,7 @@ const DEFAULT_QUERY_MOCK: ISchedulableQuery = { author_name: "Test User", author_email: "test@example.com", observer_can_run: false, + discard_data: false, interval: 300, packs: [], team_id: null, diff --git a/frontend/__mocks__/queryReportMock.ts b/frontend/__mocks__/queryReportMock.ts new file mode 100644 index 0000000000..eb538473d9 --- /dev/null +++ b/frontend/__mocks__/queryReportMock.ts @@ -0,0 +1,331 @@ +import { IQueryReport } from "interfaces/query_report"; + +const DEFAULT_QUERY_REPORT_MOCK: IQueryReport = { + query_id: 31, + results: [ + { + host_id: 1, + host_name: "foo", + last_fetched: "2021-01-19T17:08:31Z", + columns: { + model: "Razer Viper", + vendor: "Razer", + model_id: "0078", + }, + }, + { + host_id: 1, + host_name: "foo", + last_fetched: "2021-01-19T17:08:31Z", + columns: { + model: "USB Keyboard", + vendor: "VIA Labs, Inc.", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB Reciever", + vendor: "Logitech", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB Keyboard", + vendor: "Logitech", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "YubiKey OTP+FIDO+CCID", + vendor: "Yubico", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Lenovo USB Optical Mouse", + vendor: "PixArt", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Lenovo Traditional USB Keyboard", + vendor: "Lenovo", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Display Audio", + vendor: "Bose", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB-C Digital AV Multiport Adapter", + vendor: "Apple, Inc.", + model_id: "1460", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB Reciever", + vendor: "Logitech", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB-C Digital AV Multiport Adapter", + vendor: "Apple Inc.", + model_id: "1460", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB Reciever", + vendor: "Logitech", + }, + }, + { + host_id: 3, + host_name: "zoo", + last_fetched: "2022-04-09T17:20:00Z", + columns: { + model: "Logitech Webcam C925e", + model_id: "085b", + }, + }, + { + host_id: 3, + host_name: "zoo", + last_fetched: "2022-04-09T17:20:00Z", + columns: { + model: "Display Audio", + vendor: "Apple Inc.", + }, + }, + { + host_id: 3, + host_name: "zoo", + last_fetched: "2022-04-09T17:20:00Z", + columns: { + model: "Ambient Light Sensor", + vendor: "Apple Inc.", + }, + }, + { + host_id: 3, + host_name: "zoo", + last_fetched: "2022-04-09T17:20:00Z", + columns: { + model: "DELL Laser Mouse", + model_id: "4d51", + }, + }, + { + host_id: 7, + host_name: "Rachel's Magnificent Testing Computer of All Computers", + last_fetched: "2023-09-21T19:03:30Z", + columns: { + model: "AppleUSBVHCIBCE Root Hub Simulation", + vendor: "Apple Inc.", + }, + }, + { + host_id: 7, + host_name: "Rachel's Magnificent Testing Computer of All Computers", + last_fetched: "2023-09-21T19:03:30Z", + columns: { + model: "QuickFire Rapid keyboard", + vendor: "CM Storm", + model_id: "0004", + }, + }, + { + host_id: 7, + host_name: "Rachel's Magnificent Testing Computer of All Computers", + last_fetched: "2023-09-21T19:03:30Z", + columns: { + model: "Lenovo USB Optical Mouse", + vendor: "Lenovo", + }, + }, + { + host_id: 7, + host_name: "Rachel's Magnificent Testing Computer of All Computers", + last_fetched: "2023-09-21T19:03:30Z", + columns: { + model: "YubiKey FIDO+CCID", + vendor: "Yubico", + }, + }, + { + host_id: 4, + host_name: "car", + last_fetched: "2023-01-14T12:40:30Z", + columns: { + model: "USB2.0 Hub", + vendor: "Apple Inc.", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "FaceTime HD Camera (Display)", + vendor: "Apple Inc.", + model_id: "1112", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Apple Internal Keyboard / Trackpad", + model_id: "027e", + vendor: "Apple Inc.", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Apple Thunderbolt Display", + vendor: "Apple Inc.", + model_id: "9227", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "AppleUSBXHCI Root Hub Simulation", + vendor: "Apple Inc.", + model_id: "8007", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Apple T2 Controller", + vendor: "Apple Inc.", + model_id: "8233", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "4-Port USB 2.0 Hub", + vendor: "Generic", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "USB 10_100_1000 LAN", + vendor: "Realtek", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "Display Audio", + vendor: "Apple Inc.", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "USB Mouse", + vendor: "Razor", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "USB Audio", + vendor: "Apple, Inc.", + }, + }, + { + host_id: 6, + host_name: "moo", + last_fetched: "2023-09-20T07:02:34Z", + columns: { + model: "Display Audio", + vendor: "Apple Inc.", + }, + }, + { + host_id: 6, + host_name: "moo", + last_fetched: "2023-09-20T07:02:34Z", + columns: { + model: "USB Reciever", + vendor: "Logitech", + }, + }, + { + host_id: 6, + host_name: "moo", + last_fetched: "2023-09-20T07:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + ], +}; + +const createMockQueryReport = ( + overrides?: Partial +): IQueryReport => { + return { ...DEFAULT_QUERY_REPORT_MOCK, ...overrides }; +}; + +export default createMockQueryReport; diff --git a/frontend/__mocks__/scheduleableQueryMock.ts b/frontend/__mocks__/scheduleableQueryMock.ts index edc82a61da..3ac57bb49d 100644 --- a/frontend/__mocks__/scheduleableQueryMock.ts +++ b/frontend/__mocks__/scheduleableQueryMock.ts @@ -20,6 +20,7 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { author_name: "Test User", author_email: "test@example.com", observer_can_run: false, + discard_data: false, packs: [], stats: { system_time_p50: 28.1053, diff --git a/frontend/components/EmptyTable/_styles.scss b/frontend/components/EmptyTable/_styles.scss index c24d1c9543..15517d8d8f 100644 --- a/frontend/components/EmptyTable/_styles.scss +++ b/frontend/components/EmptyTable/_styles.scss @@ -57,7 +57,6 @@ &__container { align-self: center; justify-content: center; - margin: 0; margin-bottom: 20px; min-height: 155px; max-width: none; diff --git a/frontend/components/InfoBanner/InfoBanner.tsx b/frontend/components/InfoBanner/InfoBanner.tsx index 9873e9d55e..1e02b38b6e 100644 --- a/frontend/components/InfoBanner/InfoBanner.tsx +++ b/frontend/components/InfoBanner/InfoBanner.tsx @@ -11,7 +11,7 @@ export interface IInfoBannerProps { children?: React.ReactNode; className?: string; /** default light purple */ - color?: "yellow" | "grey"; + color?: "purple" | "purple-bold-border" | "yellow" | "grey"; /** default 4px */ borderRadius?: "large" | "xlarge"; pageLevel?: boolean; @@ -26,7 +26,7 @@ export interface IInfoBannerProps { const InfoBanner = ({ children, className, - color, + color = "purple", borderRadius, pageLevel, cta, @@ -36,6 +36,7 @@ const InfoBanner = ({ }: IInfoBannerProps): JSX.Element => { const wrapperClasses = classNames( baseClass, + `${baseClass}__${color}`, { [`${baseClass}__${color}`]: !!color, [`${baseClass}__border-radius-${borderRadius}`]: !!borderRadius, diff --git a/frontend/components/InfoBanner/_styles.scss b/frontend/components/InfoBanner/_styles.scss index c2b25a6a01..41c6bf0a71 100644 --- a/frontend/components/InfoBanner/_styles.scss +++ b/frontend/components/InfoBanner/_styles.scss @@ -5,11 +5,19 @@ padding: $pad-medium; border-radius: $border-radius; border: 1px solid $ui-vibrant-blue-50; - background-color: $ui-vibrant-blue-10; font-size: $x-small; font-weight: $regular; color: $core-fleet-black; + &__purple { + background-color: $ui-vibrant-blue-10; + } + + &__purple-bold-border { + background-color: $ui-vibrant-blue-10; + border-color: $core-vibrant-blue; + } + &__yellow { background-color: $ui-yellow-banner; border-color: $ui-yellow-banner-outline; @@ -35,6 +43,9 @@ } &__info { + display: flex; + flex-direction: column; + gap: $pad-small; p { margin: $pad-small 0 0 0; } @@ -46,6 +57,8 @@ color: $core-fleet-black; text-align: right; gap: $pad-small; + min-width: max-content; + margin-left: $pad-small; button { margin-left: $pad-small; @@ -65,4 +78,8 @@ } } } + + p { + margin: 0; + } } diff --git a/frontend/components/LiveQuery/SelectTargets.tsx b/frontend/components/LiveQuery/SelectTargets.tsx index c65a3e0e60..998e4e6d52 100644 --- a/frontend/components/LiveQuery/SelectTargets.tsx +++ b/frontend/components/LiveQuery/SelectTargets.tsx @@ -12,7 +12,7 @@ import { ISelectLabel, ISelectTeam, ISelectTargetsEntity, - ISelectedTargets, + ISelectedTargetsForApi, } from "interfaces/target"; import { ITeam } from "interfaces/team"; @@ -48,7 +48,9 @@ interface ISelectTargetsProps { targetedTeams: ITeam[]; goToQueryEditor: () => void; goToRunQuery: () => void; - setSelectedTargets: React.Dispatch>; + setSelectedTargets: // TODO: Refactor policy targets to streamline selectedTargets/selectedTargetsByType + | React.Dispatch> // Used for policies page level useState hook + | ((value: ITarget[]) => void); // Used for queries app level QueryContext setTargetedHosts: React.Dispatch>; setTargetedLabels: React.Dispatch>; setTargetedTeams: React.Dispatch>; @@ -65,7 +67,7 @@ interface ITargetsQueryKey { scope: string; query_id?: number | null; query?: string | null; - selected?: ISelectedTargets | null; + selected?: ISelectedTargetsForApi | null; } const DEBOUNCE_DELAY = 500; @@ -379,12 +381,22 @@ const SelectTargets = ({ } const { targets_count: total, targets_online: online } = counts; - const onlinePercentage = total > 0 ? Math.round((online / total) * 100) : 0; + const onlinePercentage = () => { + if (total === 0) { + return 0; + } + // If at least 1 host is online, displays <1% instead of 0% + const roundPercentage = + Math.round((online / total) * 100) === 0 + ? "<1" + : Math.round((online / total) * 100) === 0; + return roundPercentage; + }; return ( <> {total} host{total > 1 ? `s` : ``} targeted  ( - {onlinePercentage} + {onlinePercentage()} %  have recently checked
into Fleet.`} diff --git a/frontend/components/LiveQuery/TargetsInput/_styles.scss b/frontend/components/LiveQuery/TargetsInput/_styles.scss index e2f14ce6e1..137525dd04 100644 --- a/frontend/components/LiveQuery/TargetsInput/_styles.scss +++ b/frontend/components/LiveQuery/TargetsInput/_styles.scss @@ -79,4 +79,7 @@ overflow: auto; } } + .input-icon-field__icon { + top: 34px; // Override styling to include label header + } } diff --git a/frontend/components/TableContainer/DataTable/DefaultColumnFilter/DefaultColumnFilter.tsx b/frontend/components/TableContainer/DataTable/DefaultColumnFilter/DefaultColumnFilter.tsx index facc7e72dc..df3b609b78 100644 --- a/frontend/components/TableContainer/DataTable/DefaultColumnFilter/DefaultColumnFilter.tsx +++ b/frontend/components/TableContainer/DataTable/DefaultColumnFilter/DefaultColumnFilter.tsx @@ -8,6 +8,11 @@ const DefaultColumnFilter = ({ }: FilterProps): JSX.Element => { const { setFilter } = column; + // Remove last_fetched filter per design as it is confusing to filter by a non-displayed date-string + if (column.id === "last_fetched") { + return <>; + } + return (
{ return ( { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default CollectingResults; diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts index cd18850643..36ac93a842 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -4,6 +4,7 @@ import ArrowInternalLink from "./ArrowInternalLink"; import CalendarCheck from "./CalendarCheck"; import Check from "./Check"; import Chevron from "./Chevron"; +import CollectingResults from "./CollectingResults"; import Columns from "./Columns"; import CriticalPolicy from "./CriticalPolicy"; import Disable from "./Disable"; @@ -84,6 +85,7 @@ export const ICON_MAP = { "calendar-check": CalendarCheck, chevron: Chevron, check: Check, + "collecting-results": CollectingResults, columns: Columns, "critical-policy": CriticalPolicy, disable: Disable, diff --git a/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx b/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx index ed2bf4953a..e807a83b6a 100644 --- a/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx +++ b/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx @@ -1,19 +1,18 @@ import React from "react"; -import PhoneHome from "../../../../../assets/images/phone-home.svg"; +import EmptyTable from "components/EmptyTable/EmptyTable"; const baseClass = "awaiting-results"; const AwaitingResults = () => { return ( -
- awaiting results - Phoning home... -

- There are currently no results to your query. Please wait while we talk - to more hosts. -

-
+ ); }; diff --git a/frontend/components/queries/queryResults/AwaitingResults/_styles.scss b/frontend/components/queries/queryResults/AwaitingResults/_styles.scss index 12463a3844..ba2b2dad9b 100644 --- a/frontend/components/queries/queryResults/AwaitingResults/_styles.scss +++ b/frontend/components/queries/queryResults/AwaitingResults/_styles.scss @@ -5,19 +5,4 @@ flex-direction: column; align-items: center; text-align: center; - - img { - margin-bottom: $pad-medium; - } - - &__title { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-small; - } - - &__description { - font-size: $x-small; - margin: 0; - } } diff --git a/frontend/context/query.tsx b/frontend/context/query.tsx index 992e152cad..73bd7a297e 100644 --- a/frontend/context/query.tsx +++ b/frontend/context/query.tsx @@ -6,6 +6,12 @@ import { DEFAULT_QUERY } from "utilities/constants"; import { DEFAULT_OSQUERY_TABLE, IOsQueryTable } from "interfaces/osquery_table"; import { SelectedPlatformString } from "interfaces/platform"; import { QueryLoggingOption } from "interfaces/schedulable_query"; +import { + DEFAULT_TARGETS, + DEFAULT_TARGETS_BY_TYPE, + ISelectedTargetsByType, + ITarget, +} from "interfaces/target"; type Props = { children: ReactNode; @@ -22,6 +28,9 @@ type InitialStateType = { lastEditedQueryPlatforms: SelectedPlatformString; lastEditedQueryMinOsqueryVersion: string; lastEditedQueryLoggingType: QueryLoggingOption; + lastEditedQueryDiscardData: boolean; + selectedQueryTargets: ITarget[]; // Mimicks old selectedQueryTargets still used for policies for SelectTargets.tsx and running a live query + selectedQueryTargetsByType: ISelectedTargetsByType; // New format by type for cleaner app wide state setLastEditedQueryId: (value: number | null) => void; setLastEditedQueryName: (value: string) => void; setLastEditedQueryDescription: (value: string) => void; @@ -31,7 +40,10 @@ type InitialStateType = { setLastEditedQueryPlatforms: (value: SelectedPlatformString) => void; setLastEditedQueryMinOsqueryVersion: (value: string) => void; setLastEditedQueryLoggingType: (value: string) => void; + setLastEditedQueryDiscardData: (value: boolean) => void; setSelectedOsqueryTable: (tableName: string) => void; + setSelectedQueryTargets: (value: ITarget[]) => void; + setSelectedQueryTargetsByType: (value: ISelectedTargetsByType) => void; }; export type IQueryContext = InitialStateType; @@ -48,6 +60,9 @@ const initialState = { lastEditedQueryPlatforms: DEFAULT_QUERY.platform, lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version, lastEditedQueryLoggingType: DEFAULT_QUERY.logging, + lastEditedQueryDiscardData: DEFAULT_QUERY.discard_data, + selectedQueryTargets: DEFAULT_TARGETS, + selectedQueryTargetsByType: DEFAULT_TARGETS_BY_TYPE, setLastEditedQueryId: () => null, setLastEditedQueryName: () => null, setLastEditedQueryDescription: () => null, @@ -57,12 +72,17 @@ const initialState = { setLastEditedQueryPlatforms: () => null, setLastEditedQueryMinOsqueryVersion: () => null, setLastEditedQueryLoggingType: () => null, + setLastEditedQueryDiscardData: () => null, setSelectedOsqueryTable: () => null, + setSelectedQueryTargets: () => null, + setSelectedQueryTargetsByType: () => null, }; const actions = { SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE", SET_LAST_EDITED_QUERY_INFO: "SET_LAST_EDITED_QUERY_INFO", + SET_SELECTED_QUERY_TARGETS: "SET_SELECTED_QUERY_TARGETS", + SET_SELECTED_QUERY_TARGETS_BY_TYPE: "SET_SELECTED_QUERY_TARGETS_BY_TYPE", } as const; const reducer = (state: InitialStateType, action: any) => { @@ -113,6 +133,26 @@ const reducer = (state: InitialStateType, action: any) => { typeof action.lastEditedQueryLoggingType === "undefined" ? state.lastEditedQueryLoggingType : action.lastEditedQueryLoggingType, + lastEditedQueryDiscardData: + typeof action.lastEditedQueryDiscardData === "undefined" + ? state.lastEditedQueryDiscardData + : action.lastEditedQueryDiscardData, + }; + case actions.SET_SELECTED_QUERY_TARGETS: + return { + ...state, + selectedQueryTargets: + typeof action.selectedQueryTargets === "undefined" + ? state.selectedQueryTargets + : action.selectedQueryTargets, + }; + case actions.SET_SELECTED_QUERY_TARGETS_BY_TYPE: + return { + ...state, + selectedQueryTargetsByType: + typeof action.selectedQueryTargetsByType === "undefined" + ? state.selectedQueryTargetsByType + : action.selectedQueryTargetsByType, }; default: return state; @@ -135,6 +175,9 @@ const QueryProvider = ({ children }: Props) => { lastEditedQueryPlatforms: state.lastEditedQueryPlatforms, lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion, lastEditedQueryLoggingType: state.lastEditedQueryLoggingType, + lastEditedQueryDiscardData: state.lastEditedQueryDiscardData, + selectedQueryTargets: state.selectedQueryTargets, + selectedQueryTargetsByType: state.selectedQueryTargetsByType, setLastEditedQueryId: (lastEditedQueryId: number | null) => { dispatch({ type: actions.SET_LAST_EDITED_QUERY_INFO, @@ -193,6 +236,26 @@ const QueryProvider = ({ children }: Props) => { lastEditedQueryLoggingType, }); }, + setLastEditedQueryDiscardData: (lastEditedQueryDiscardData: boolean) => { + dispatch({ + type: actions.SET_LAST_EDITED_QUERY_INFO, + lastEditedQueryDiscardData, + }); + }, + setSelectedQueryTargets: (selectedQueryTargets: ITarget[]) => { + dispatch({ + type: actions.SET_SELECTED_QUERY_TARGETS, + selectedQueryTargets, + }); + }, + setSelectedQueryTargetsByType: ( + selectedQueryTargetsByType: ISelectedTargetsByType + ) => { + dispatch({ + type: actions.SET_SELECTED_QUERY_TARGETS_BY_TYPE, + selectedQueryTargetsByType, + }); + }, setSelectedOsqueryTable: (tableName: string) => { dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName }); }, diff --git a/frontend/hooks/useQueryTargets.ts b/frontend/hooks/useQueryTargets.ts index 081b473afe..d594ab8566 100644 --- a/frontend/hooks/useQueryTargets.ts +++ b/frontend/hooks/useQueryTargets.ts @@ -4,7 +4,7 @@ import { filter, uniqueId } from "lodash"; import { IHost } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { ITeam } from "interfaces/team"; -import { ISelectedTargets } from "interfaces/target"; +import { ISelectedTargetsForApi } from "interfaces/target"; import targetsAPI from "services/entities/targets"; export interface ITargetsLabels { @@ -25,7 +25,7 @@ export interface ITargetsQueryKey { scope: string; query: string; queryId: number | null; - selected: ISelectedTargets; + selected: ISelectedTargetsForApi; includeLabels: boolean; } diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index 2bfbe6fada..cf15ccca64 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -113,6 +113,7 @@ export interface IConfig { live_query_disabled: boolean; enable_analytics: boolean; deferred_save_host: boolean; + query_reports_disabled: boolean; }; smtp_settings: { enable_smtp: boolean; diff --git a/frontend/interfaces/query.ts b/frontend/interfaces/query.ts index d6a948cd25..cad78f3745 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -3,7 +3,7 @@ import { IPack } from "./pack"; import { ISchedulableQuery } from "./schedulable_query"; import { IScheduledQueryStats } from "./scheduled_query_stats"; -export interface IQueryFormData { +export interface IEditQueryFormData { description?: string | number | boolean | undefined; name?: string | number | boolean | undefined; query?: string | number | boolean | undefined; @@ -35,7 +35,7 @@ export interface IQuery { stats?: IScheduledQueryStats; } -export interface IQueryFormFields { +export interface IEditQueryFormFields { description: IFormField; name: IFormField; query: IFormField; diff --git a/frontend/interfaces/query_report.ts b/frontend/interfaces/query_report.ts new file mode 100644 index 0000000000..9310fcc4e8 --- /dev/null +++ b/frontend/interfaces/query_report.ts @@ -0,0 +1,12 @@ +export interface IQueryReportResultRow { + host_id: number; + host_name: string; + last_fetched: string; + columns: any; +} + +// Query report +export interface IQueryReport { + query_id: number; + results: IQueryReportResultRow[]; +} diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index 9d86c98b85..1a57fb8a72 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -21,6 +21,7 @@ export interface ISchedulableQuery { author_name: string; author_email: string; observer_can_run: boolean; + discard_data: boolean; packs: IPack[]; stats: ISchedulableQueryStats; } @@ -62,6 +63,7 @@ export interface ICreateQueryRequestBody { query: string; description?: string; observer_can_run?: boolean; + discard_data?: boolean; team_id?: number; // global query if ommitted interval?: number; // default 0 means never run platform?: SelectedPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted @@ -81,6 +83,7 @@ export interface IModifyQueryRequestBody query?: string; description?: string; observer_can_run?: boolean; + discard_data?: boolean; frequency?: number; platform?: SelectedPlatformString; min_osquery_version?: string; @@ -108,11 +111,12 @@ export interface IDeleteQueriesResponse { deleted: number; // number of queries deleted } -export interface IQueryFormFields { +export interface IEditQueryFormFields { name: IFormField; description: IFormField; query: IFormField; observer_can_run: IFormField; + discard_data: IFormField; frequency: IFormField; platforms: IFormField; min_osquery_version: IFormField; diff --git a/frontend/interfaces/target.ts b/frontend/interfaces/target.ts index 7526719212..06f464cf51 100644 --- a/frontend/interfaces/target.ts +++ b/frontend/interfaces/target.ts @@ -38,14 +38,29 @@ export interface ISelectTeam extends ITeam { export type ISelectTargetsEntity = ISelectHost | ISelectLabel | ISelectTeam; -export interface ISelectedTargets { +export interface ISelectedTargetsForApi { hosts: number[]; labels: number[]; teams: number[]; } +export interface ISelectedTargetsByType { + hosts: IHost[]; + labels: ILabel[]; + teams: ITeam[]; +} + export interface IPackTargets { host_ids: (number | string)[]; label_ids: (number | string)[]; team_ids: (number | string)[]; } + +// TODO: Also use for testing +export const DEFAULT_TARGETS: ITarget[] = []; + +export const DEFAULT_TARGETS_BY_TYPE: ISelectedTargetsByType = { + hosts: [], + labels: [], + teams: [], +}; diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx index 61da4d5b8d..62f6d4d4b5 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx @@ -18,7 +18,7 @@ const Advanced = ({ handleSubmit, isUpdatingSettings, }: IAppConfigFormProps): JSX.Element => { - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ domain: appConfig.smtp_settings.domain || "", verifySSLCerts: appConfig.smtp_settings.verify_ssl_certs || false, enableStartTLS: appConfig.smtp_settings.enable_start_tls, @@ -26,6 +26,8 @@ const Advanced = ({ appConfig.host_expiry_settings.host_expiry_enabled || false, hostExpiryWindow: appConfig.host_expiry_settings.host_expiry_window || 0, disableLiveQuery: appConfig.server_settings.live_query_disabled || false, + disableQueryReports: + appConfig.server_settings.query_reports_disabled || false, }); const { @@ -35,6 +37,7 @@ const Advanced = ({ enableHostExpiry, hostExpiryWindow, disableLiveQuery, + disableQueryReports, } = formData; const [formErrors, setFormErrors] = useState({}); @@ -69,6 +72,7 @@ const Advanced = ({ server_url: appConfig.server_settings.server_url || "", live_query_disabled: disableLiveQuery, enable_analytics: appConfig.server_settings.enable_analytics, + query_reports_disabled: disableQueryReports, }, smtp_settings: { enable_smtp: appConfig.smtp_settings.enable_smtp || false, @@ -172,6 +176,24 @@ const Advanced = ({ > Disable live queries + Disabling query reports will decrease database usage,
\ + but will prevent you from accessing query results in
\ + Fleet and will delete existing reports. This can also be
\ + disabled on a per-query basis by enabling "Discard
\ + data". (Default: Off)

' + } + > + Disable query reports +
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 7051ff4086..db5bf09b87 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -13,6 +13,7 @@ import queryAPI from "services/entities/queries"; import teamAPI, { ILoadTeamsResponse } from "services/entities/teams"; import { AppContext } from "context/app"; import { PolicyContext } from "context/policy"; +import { QueryContext } from "context/query"; import { NotificationContext } from "context/notification"; import { IHost, @@ -26,6 +27,7 @@ import { ILabel } from "interfaces/label"; import { IHostPolicy } from "interfaces/policy"; import { IQueryStats } from "interfaces/query_stats"; import { ISoftware } from "interfaces/software"; +import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target"; import { ITeam } from "interfaces/team"; import { IListQueriesResponse, @@ -39,8 +41,13 @@ import MainContent from "components/MainContent"; import InfoBanner from "components/InfoBanner"; import BackLink from "components/BackLink"; -import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers"; +import { + normalizeEmptyValues, + wrapFleetHelper, + TAGGED_TEMPLATES, +} from "utilities/helpers"; import permissions from "utilities/permissions"; +import { DEFAULT_QUERY } from "utilities/constants"; import HostSummaryCard from "../cards/HostSummary"; import AboutCard from "../cards/About"; @@ -101,12 +108,6 @@ interface IHostDetailsSubNavItem { pathname: string; } -const TAGGED_TEMPLATES = { - queryByHostRoute: (hostId: number | undefined | null) => { - return `${hostId ? `?host_ids=${hostId}` : ""}`; - }, -}; - const HostDetailsPage = ({ route, router, @@ -137,6 +138,7 @@ const HostDetailsPage = ({ setLastEditedQueryCritical, setPolicyTeamId, } = useContext(PolicyContext); + const { setSelectedQueryTargetsByType } = useContext(QueryContext); const { renderFlash } = useContext(NotificationContext); const handlePageError = useErrorHandler(); @@ -523,12 +525,15 @@ const HostDetailsPage = ({ }; const onQueryHostCustom = () => { + setLastEditedQueryBody(DEFAULT_QUERY.query); + setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE); router.push( PATHS.NEW_QUERY() + TAGGED_TEMPLATES.queryByHostRoute(host?.id) ); }; const onQueryHostSaved = (selectedQuery: ISchedulableQuery) => { + setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE); router.push( PATHS.EDIT_QUERY(selectedQuery.id) + TAGGED_TEMPLATES.queryByHostRoute(host?.id) diff --git a/frontend/pages/policies/PolicyPage/PolicyPage.tsx b/frontend/pages/policies/PolicyPage/PolicyPage.tsx index ef3fc1eb4c..5d10551229 100644 --- a/frontend/pages/policies/PolicyPage/PolicyPage.tsx +++ b/frontend/pages/policies/PolicyPage/PolicyPage.tsx @@ -19,7 +19,7 @@ import globalPoliciesAPI from "services/entities/global_policies"; import teamPoliciesAPI from "services/entities/team_policies"; import hostAPI from "services/entities/hosts"; import statusAPI from "services/entities/status"; -import { QUERIES_PAGE_STEPS } from "utilities/constants"; +import { LIVE_POLICY_STEPS } from "utilities/constants"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; import QueryEditor from "pages/policies/PolicyPage/screens/QueryEditor"; @@ -127,7 +127,7 @@ const PolicyPage = ({ }; }, []); - const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]); + const [step, setStep] = useState(LIVE_POLICY_STEPS[1]); const [selectedTargets, setSelectedTargets] = useState([]); const [targetedHosts, setTargetedHosts] = useState([]); const [targetedLabels, setTargetedLabels] = useState([]); @@ -260,7 +260,7 @@ const PolicyPage = ({ storedPolicyError, createPolicy, onOsqueryTableSelect, - goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]), + goToSelectTargets: () => setStep(LIVE_POLICY_STEPS[2]), onOpenSchemaSidebar, renderLiveQueryWarning, }; @@ -272,8 +272,8 @@ const PolicyPage = ({ targetedLabels, targetedTeams, targetsTotalCount, - goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]), - goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]), + goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]), + goToRunQuery: () => setStep(LIVE_POLICY_STEPS[3]), setSelectedTargets, setTargetedHosts, setTargetedLabels, @@ -285,21 +285,21 @@ const PolicyPage = ({ selectedTargets, storedPolicy, setSelectedTargets, - goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]), + goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]), targetsTotalCount, }; switch (step) { - case QUERIES_PAGE_STEPS[2]: + case LIVE_POLICY_STEPS[2]: return ; - case QUERIES_PAGE_STEPS[3]: + case LIVE_POLICY_STEPS[3]: return ; default: return ; } }; - const isFirstStep = step === QUERIES_PAGE_STEPS[1]; + const isFirstStep = step === LIVE_POLICY_STEPS[1]; const showSidebar = isFirstStep && isSidebarOpen && diff --git a/frontend/pages/policies/PolicyPage/_styles.scss b/frontend/pages/policies/PolicyPage/_styles.scss index da213b9c8d..f069352178 100644 --- a/frontend/pages/policies/PolicyPage/_styles.scss +++ b/frontend/pages/policies/PolicyPage/_styles.scss @@ -34,33 +34,6 @@ } } - &__observer-query-details { - padding: 0 2rem; - - h1 { - margin: $pad-large 0; - font-size: $large; - } - - p { - margin-bottom: $pad-small; - } - - .sql-button { - color: $core-vibrant-blue; - font-weight: $bold; - font-size: $x-small; - } - } - - &__query-preview { - margin-top: 15px; - - .fleet-ace__label { - display: none; - } - } - .ace_content { min-height: 500px !important; } @@ -177,9 +150,4 @@ margin-bottom: 0; } } - .targets-input { - .input-icon-field__icon { - top: 34px; // Override styling to include label header - } - } } diff --git a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx index 89e46d024a..5e42e673de 100644 --- a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx +++ b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx @@ -173,7 +173,7 @@ const QueryEditor = ({ return null; } - // Function instead of constant eliminates race condition with filteredSoftwarePath + // Function instead of constant eliminates race condition with filteredPoliciesPath const backToPoliciesPath = () => { return filteredPoliciesPath || PATHS.MANAGE_POLICIES; }; diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index b83ea47c99..73290cecab 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -10,6 +10,7 @@ import { useQuery } from "react-query"; import { pick } from "lodash"; import { AppContext } from "context/app"; +import { QueryContext } from "context/query"; import { TableContext } from "context/table"; import { NotificationContext } from "context/notification"; import { performanceIndicator } from "utilities/helpers"; @@ -20,8 +21,10 @@ import { IQueryKeyQueriesLoadAll, ISchedulableQuery, } from "interfaces/schedulable_query"; +import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target"; import queriesAPI from "services/entities/queries"; import PATHS from "router/paths"; +import { DEFAULT_QUERY } from "utilities/constants"; import { checkPlatformCompatibility } from "utilities/sql_tools"; import Button from "components/buttons/Button"; import Spinner from "components/Spinner"; @@ -87,6 +90,9 @@ const ManageQueriesPage = ({ isSandboxMode, config, } = useContext(AppContext); + const { setLastEditedQueryBody, setSelectedQueryTargetsByType } = useContext( + QueryContext + ); const { setResetSelectedRows } = useContext(TableContext); const { renderFlash } = useContext(NotificationContext); @@ -178,7 +184,15 @@ const ManageQueriesPage = ({ } }, [location, filteredQueriesPath, setFilteredQueriesPath]); - const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY(currentTeamId)); + // Reset selected targets when returned to this page + useEffect(() => { + setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE); + }, []); + + const onCreateQueryClick = () => { + setLastEditedQueryBody(DEFAULT_QUERY.query); + router.push(PATHS.NEW_QUERY(currentTeamId)); + }; const toggleDeleteQueryModal = useCallback(() => { setShowDeleteQueryModal(!showDeleteQueryModal); diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index f24c17af24..29d25358de 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -152,7 +152,7 @@ const generateTableHeaders = ({ )} } - path={PATHS.EDIT_QUERY( + path={PATHS.QUERY( cellProps.row.original.id, cellProps.row.original.team_id ?? undefined )} @@ -183,10 +183,7 @@ const generateTableHeaders = ({ - Assign a frequency and turn automations on to - collect data at an interval. - + <>Assign a frequency to collect data at an interval. } /> ); diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx deleted file mode 100644 index ca93efe3d4..0000000000 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import React, { useState, useEffect, useContext, useCallback } from "react"; -import { useQuery, useMutation } from "react-query"; -import { useErrorHandler } from "react-error-boundary"; -import { InjectedRouter, Params } from "react-router/lib/Router"; - -import { AppContext } from "context/app"; -import { QueryContext } from "context/query"; -import { QUERIES_PAGE_STEPS, DEFAULT_QUERY } from "utilities/constants"; -import queryAPI from "services/entities/queries"; -import hostAPI from "services/entities/hosts"; -import statusAPI from "services/entities/status"; -import { IHost, IHostResponse } from "interfaces/host"; -import { ILabel } from "interfaces/label"; -import { ITeam } from "interfaces/team"; -import { - IGetQueryResponse, - ISchedulableQuery, -} from "interfaces/schedulable_query"; -import { ITarget } from "interfaces/target"; - -import QuerySidePanel from "components/side_panels/QuerySidePanel"; -import MainContent from "components/MainContent"; -import SidePanelContent from "components/SidePanelContent"; -import SelectTargets from "components/LiveQuery/SelectTargets"; -import CustomLink from "components/CustomLink"; - -import QueryEditor from "pages/queries/QueryPage/screens/QueryEditor"; -import RunQuery from "pages/queries/QueryPage/screens/RunQuery"; -import useTeamIdParam from "hooks/useTeamIdParam"; - -interface IQueryPageProps { - router: InjectedRouter; - params: Params; - location: { - pathname: string; - query: { host_ids: string; team_id?: string }; - search: string; - }; -} - -const baseClass = "query-page"; - -const QueryPage = ({ - router, - params: { id: paramsQueryId }, - location, -}: IQueryPageProps): JSX.Element => { - const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null; - const { - currentTeamName: teamNameForQuery, - teamIdForApi: apiTeamIdForQuery, - } = useTeamIdParam({ - location, - router, - includeAllTeams: true, - includeNoTeam: false, - }); - - const handlePageError = useErrorHandler(); - const { - isGlobalAdmin, - isGlobalMaintainer, - isAnyTeamMaintainerOrTeamAdmin, - isObserverPlus, - isAnyTeamObserverPlus, - } = useContext(AppContext); - const { - selectedOsqueryTable, - setSelectedOsqueryTable, - setLastEditedQueryId, - setLastEditedQueryName, - setLastEditedQueryDescription, - setLastEditedQueryBody, - setLastEditedQueryObserverCanRun, - setLastEditedQueryFrequency, - setLastEditedQueryLoggingType, - setLastEditedQueryMinOsqueryVersion, - setLastEditedQueryPlatforms, - } = useContext(QueryContext); - - const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false); - const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]); - const [selectedTargets, setSelectedTargets] = useState([]); - const [targetedHosts, setTargetedHosts] = useState([]); - const [targetedLabels, setTargetedLabels] = useState([]); - const [targetedTeams, setTargetedTeams] = useState([]); - const [targetsTotalCount, setTargetsTotalCount] = useState(0); - const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true); - const [isSidebarOpen, setIsSidebarOpen] = useState(true); - const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState( - false - ); - - // disabled on page load so we can control the number of renders - // else it will re-populate the context on occasion - const { - isLoading: isStoredQueryLoading, - data: storedQuery, - error: storedQueryError, - } = useQuery( - ["query", queryId], - () => queryAPI.load(queryId as number), - { - enabled: !!queryId, - refetchOnWindowFocus: false, - select: (data) => data.query, - onSuccess: (returnedQuery) => { - setLastEditedQueryId(returnedQuery.id); - setLastEditedQueryName(returnedQuery.name); - setLastEditedQueryDescription(returnedQuery.description); - setLastEditedQueryBody(returnedQuery.query); - setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run); - setLastEditedQueryFrequency(returnedQuery.interval); - setLastEditedQueryPlatforms(returnedQuery.platform); - setLastEditedQueryLoggingType(returnedQuery.logging); - setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version); - }, - onError: (error) => handlePageError(error), - } - ); - - useQuery( - "hostFromURL", - () => - hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)), - { - enabled: !!location.query.host_ids && !queryParamHostsAdded, - select: (data: IHostResponse) => data.host, - onSuccess: (host) => { - setTargetedHosts((prevHosts) => - prevHosts.filter((h) => h.id !== host.id).concat(host) - ); - const targets = selectedTargets; - host.target_type = "hosts"; - targets.push(host); - setSelectedTargets([...targets]); - if (!queryParamHostsAdded) { - setQueryParamHostsAdded(true); - } - router.replace(location.pathname); - }, - } - ); - - const detectIsFleetQueryRunnable = () => { - statusAPI.live_query().catch(() => { - setIsLiveQueryRunnable(false); - }); - }; - - useEffect(() => { - detectIsFleetQueryRunnable(); - if (!queryId) { - setLastEditedQueryId(DEFAULT_QUERY.id); - setLastEditedQueryName(DEFAULT_QUERY.name); - setLastEditedQueryDescription(DEFAULT_QUERY.description); - setLastEditedQueryBody(DEFAULT_QUERY.query); - setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run); - setLastEditedQueryFrequency(DEFAULT_QUERY.interval); - setLastEditedQueryLoggingType(DEFAULT_QUERY.logging); - setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version); - setLastEditedQueryPlatforms(DEFAULT_QUERY.platform); - } - }, [queryId]); - - // Updates title that shows up on browser tabs - useEffect(() => { - // e.g., Query details | Discover TLS certificates | Fleet for osquery - document.title = `Query details | ${storedQuery?.name} | Fleet for osquery`; - }, [location.pathname, storedQuery?.name]); - - useEffect(() => { - setShowOpenSchemaActionText(!isSidebarOpen); - }, [isSidebarOpen]); - - const onOsqueryTableSelect = (tableName: string) => { - setSelectedOsqueryTable(tableName); - }; - - const onCloseSchemaSidebar = () => { - setIsSidebarOpen(false); - }; - - const onOpenSchemaSidebar = () => { - setIsSidebarOpen(true); - }; - - const renderLiveQueryWarning = (): JSX.Element | null => { - if (isLiveQueryRunnable) { - return null; - } - - return ( -
-
-

- Fleet is unable to run a live query. Refresh the page or log in - again. If this keeps happening please{" "} - -

-
-
- ); - }; - - const goToQueryEditor = useCallback(() => setStep(QUERIES_PAGE_STEPS[1]), []); - - const renderScreen = () => { - const step1Props = { - router, - baseClass, - queryIdForEdit: queryId, - teamNameForQuery, - apiTeamIdForQuery, - showOpenSchemaActionText, - storedQuery, - isStoredQueryLoading, - storedQueryError, - onOsqueryTableSelect, - goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]), - onOpenSchemaSidebar, - renderLiveQueryWarning, - }; - - const step2Props = { - baseClass, - queryId, - selectedTargets, - targetedHosts, - targetedLabels, - targetedTeams, - targetsTotalCount, - goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]), - goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]), - setSelectedTargets, - setTargetedHosts, - setTargetedLabels, - setTargetedTeams, - setTargetsTotalCount, - }; - - const step3Props = { - queryId, - selectedTargets, - storedQuery, - setSelectedTargets, - goToQueryEditor, - targetsTotalCount, - }; - - switch (step) { - case QUERIES_PAGE_STEPS[2]: - return ; - case QUERIES_PAGE_STEPS[3]: - return ; - default: - return ; - } - }; - - const isFirstStep = step === QUERIES_PAGE_STEPS[1]; - const showSidebar = - isFirstStep && - isSidebarOpen && - (isGlobalAdmin || - isGlobalMaintainer || - isAnyTeamMaintainerOrTeamAdmin || - isObserverPlus || - isAnyTeamObserverPlus); - - return ( - <> - -
{renderScreen()}
-
- {showSidebar && ( - - - - )} - - ); -}; - -export default QueryPage; diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/index.ts b/frontend/pages/queries/QueryPage/components/QueryForm/index.ts deleted file mode 100644 index 6bc72d25d4..0000000000 --- a/frontend/pages/queries/QueryPage/components/QueryForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./QueryForm"; diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss b/frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss deleted file mode 100644 index a4d1337350..0000000000 --- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss +++ /dev/null @@ -1,32 +0,0 @@ -.save-query-modal { - .fleet-checkbox { - display: flex; - align-items: center; - } - - .help-text { - margin-top: $pad-small; - margin-bottom: $pad-large; - font-weight: $regular; - font-size: 0.75rem; - color: $ui-fleet-black-75; - } - - &__form-field { - &--frequency { - margin-bottom: 0; - } - &--platform { - margin-bottom: 0; - margin-top: $pad-large; - } - } - - &__observer-can-run-wrapper { - margin-bottom: 0; - } - - &__advanced-options-toggle { - font-weight: $xbold; - } -} diff --git a/frontend/pages/queries/QueryPage/index.ts b/frontend/pages/queries/QueryPage/index.ts deleted file mode 100644 index 8d00fa0475..0000000000 --- a/frontend/pages/queries/QueryPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./QueryPage"; diff --git a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx deleted file mode 100644 index 7b94956efd..0000000000 --- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; - -import { InjectedRouter } from "react-router/lib/Router"; -import { UseMutateAsyncFunction } from "react-query"; - -import queryAPI from "services/entities/queries"; -import { AppContext } from "context/app"; -import { QueryContext } from "context/query"; -import { NotificationContext } from "context/notification"; -import { - ICreateQueryRequestBody, - ISchedulableQuery, -} from "interfaces/schedulable_query"; -import PATHS from "router/paths"; -import debounce from "utilities/debounce"; -import deepDifference from "utilities/deep_difference"; - -import BackLink from "components/BackLink"; -import QueryForm from "pages/queries/QueryPage/components/QueryForm"; - -interface IQueryEditorProps { - router: InjectedRouter; - baseClass: string; - queryIdForEdit: number | null; - teamNameForQuery?: string; - apiTeamIdForQuery?: number; - storedQuery: ISchedulableQuery | undefined; - storedQueryError: Error | null; - showOpenSchemaActionText: boolean; - isStoredQueryLoading: boolean; - onOsqueryTableSelect: (tableName: string) => void; - goToSelectTargets: () => void; - onOpenSchemaSidebar: () => void; - renderLiveQueryWarning: () => JSX.Element | null; -} - -const QueryEditor = ({ - router, - baseClass, - queryIdForEdit, - teamNameForQuery, - apiTeamIdForQuery, - storedQuery, - storedQueryError, - showOpenSchemaActionText, - isStoredQueryLoading, - onOsqueryTableSelect, - goToSelectTargets, - onOpenSchemaSidebar, - renderLiveQueryWarning, -}: IQueryEditorProps): JSX.Element | null => { - const { currentUser, filteredQueriesPath } = useContext(AppContext); - const { renderFlash } = useContext(NotificationContext); - - // Note: The QueryContext values should always be used for any mutable query data such as query name - // The storedQuery prop should only be used to access immutable metadata such as author id - const { - lastEditedQueryName, - lastEditedQueryDescription, - lastEditedQueryBody, - lastEditedQueryObserverCanRun, - lastEditedQueryFrequency, - lastEditedQueryLoggingType, - lastEditedQueryPlatforms, - lastEditedQueryMinOsqueryVersion, - } = useContext(QueryContext); - - const [isQuerySaving, setIsQuerySaving] = useState(false); - const [isQueryUpdating, setIsQueryUpdating] = useState(false); - - useEffect(() => { - if (storedQueryError) { - renderFlash( - "error", - "Something went wrong retrieving your query. Please try again." - ); - } - }, []); - - const [backendValidators, setBackendValidators] = useState<{ - [key: string]: string; - }>({}); - - const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => { - setIsQuerySaving(true); - try { - const { query } = await queryAPI.create(formData); - router.push(PATHS.EDIT_QUERY(query.id)); - renderFlash("success", "Query created!"); - setBackendValidators({}); - } catch (createError: any) { - if (createError.data.errors[0].reason.includes("already exists")) { - const teamErrorText = - teamNameForQuery && apiTeamIdForQuery !== 0 - ? `the ${teamNameForQuery} team` - : "all teams"; - setBackendValidators({ - name: `A query with that name already exists for ${teamErrorText}.`, - }); - } else { - renderFlash( - "error", - "Something went wrong creating your query. Please try again." - ); - setBackendValidators({}); - } - } finally { - setIsQuerySaving(false); - } - }); - - const onUpdateQuery = async (formData: ICreateQueryRequestBody) => { - if (!queryIdForEdit) { - return false; - } - - setIsQueryUpdating(true); - - const updatedQuery = deepDifference(formData, { - lastEditedQueryName, - lastEditedQueryDescription, - lastEditedQueryBody, - lastEditedQueryObserverCanRun, - lastEditedQueryFrequency, - lastEditedQueryPlatforms, - lastEditedQueryLoggingType, - lastEditedQueryMinOsqueryVersion, - }); - - try { - await queryAPI.update(queryIdForEdit, updatedQuery); - renderFlash("success", "Query updated!"); - } catch (updateError: any) { - console.error(updateError); - if (updateError.data.errors[0].reason.includes("Duplicate")) { - renderFlash("error", "A query with this name already exists."); - } else { - renderFlash( - "error", - "Something went wrong updating your query. Please try again." - ); - } - } - - setIsQueryUpdating(false); - - return false; - }; - - if (!currentUser) { - return null; - } - - // Function instead of constant eliminates race condition with filteredSoftwarePath - const backToQueriesPath = () => { - return filteredQueriesPath || PATHS.MANAGE_QUERIES; - }; - - return ( -
-
- -
- -
- ); -}; - -export default QueryEditor; diff --git a/frontend/pages/queries/QueryPage/screens/test.js b/frontend/pages/queries/QueryPage/screens/test.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx new file mode 100644 index 0000000000..fd8392d2e8 --- /dev/null +++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx @@ -0,0 +1,298 @@ +import React, { useContext } from "react"; +import { useQuery } from "react-query"; +import { InjectedRouter, Params } from "react-router/lib/Router"; +import { useErrorHandler } from "react-error-boundary"; + +import PATHS from "router/paths"; +import { AppContext } from "context/app"; +import { QueryContext } from "context/query"; + +import { + IGetQueryResponse, + ISchedulableQuery, +} from "interfaces/schedulable_query"; +import { IQueryReport } from "interfaces/query_report"; + +import queryAPI from "services/entities/queries"; +import queryReportAPI, { ISortOption } from "services/entities/query_report"; + +import Spinner from "components/Spinner/Spinner"; +import Button from "components/buttons/Button"; +import BackLink from "components/BackLink"; +import MainContent from "components/MainContent"; +import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper"; +import QueryAutomationsStatusIndicator from "pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator"; +import DataError from "components/DataError/DataError"; +import LogDestinationIndicator from "components/LogDestinationIndicator/LogDestinationIndicator"; +import CustomLink from "components/CustomLink"; +import InfoBanner from "components/InfoBanner"; +import QueryReport from "../components/QueryReport/QueryReport"; +import NoResults from "../components/NoResults/NoResults"; + +import { + DEFAULT_SORT_HEADER, + DEFAULT_SORT_DIRECTION, + QUERY_REPORT_RESULTS_LIMIT, +} from "./QueryDetailsPageConfig"; + +interface IQueryDetailsPageProps { + router: InjectedRouter; // v3 + params: Params; + location: { + pathname: string; + query: { team_id?: string; order_key?: string; order_direction?: string }; + search: string; + }; +} + +const baseClass = "query-details-page"; + +const QueryDetailsPage = ({ + router, + params: { id: paramsQueryId }, + location, +}: IQueryDetailsPageProps): JSX.Element => { + const queryId = parseInt(paramsQueryId, 10); + const queryParams = location.query; + + // Functions to avoid race conditions + const serverSortBy: ISortOption[] = (() => { + return [ + { + key: queryParams?.order_key ?? DEFAULT_SORT_HEADER, + direction: queryParams?.order_direction ?? DEFAULT_SORT_DIRECTION, + }, + ]; + })(); + + const handlePageError = useErrorHandler(); + const { + isGlobalAdmin, + isGlobalMaintainer, + isAnyTeamMaintainerOrTeamAdmin, + isObserverPlus, + isAnyTeamObserverPlus, + config, + filteredQueriesPath, + } = useContext(AppContext); + const { + lastEditedQueryName, + lastEditedQueryDescription, + lastEditedQueryObserverCanRun, + setLastEditedQueryId, + setLastEditedQueryName, + setLastEditedQueryDescription, + setLastEditedQueryBody, + setLastEditedQueryObserverCanRun, + setLastEditedQueryFrequency, + setLastEditedQueryLoggingType, + setLastEditedQueryMinOsqueryVersion, + setLastEditedQueryPlatforms, + setLastEditedQueryDiscardData, + } = useContext(QueryContext); + + // Title that shows up on browser tabs (e.g., Query details | Discover TLS certificates | Fleet for osquery) + document.title = `Query details | ${lastEditedQueryName} | Fleet for osquery`; + + // disabled on page load so we can control the number of renders + // else it will re-populate the context on occasion + const { + isLoading: isStoredQueryLoading, + data: storedQuery, + error: storedQueryError, + } = useQuery( + ["query", queryId], + () => queryAPI.load(queryId), + { + enabled: !!queryId, + refetchOnWindowFocus: false, + select: (data) => data.query, + onSuccess: (returnedQuery) => { + setLastEditedQueryId(returnedQuery.id); + setLastEditedQueryName(returnedQuery.name); + setLastEditedQueryDescription(returnedQuery.description); + setLastEditedQueryBody(returnedQuery.query); + setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run); + setLastEditedQueryFrequency(returnedQuery.interval); + setLastEditedQueryPlatforms(returnedQuery.platform); + setLastEditedQueryLoggingType(returnedQuery.logging); + setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version); + setLastEditedQueryDiscardData(returnedQuery.discard_data); + }, + onError: (error) => handlePageError(error), + } + ); + + const { + isLoading: isQueryReportLoading, + data: queryReport, + error: queryReportError, + } = useQuery( + [], + () => + queryReportAPI.load({ + sortBy: serverSortBy, + id: queryId, + }), + { + enabled: !!queryId, + refetchOnWindowFocus: false, + onError: (error) => handlePageError(error), + } + ); + + const isLoading = isStoredQueryLoading || isQueryReportLoading; + const isApiError = storedQueryError || queryReportError; + const isClipped = + (queryReport?.results?.length ?? 0) >= QUERY_REPORT_RESULTS_LIMIT; + + const renderHeader = () => { + const canEditQuery = + isGlobalAdmin || isGlobalMaintainer || isAnyTeamMaintainerOrTeamAdmin; + + // Function instead of constant eliminates race condition with filteredQueriesPath + const backToQueriesPath = () => { + return filteredQueriesPath || PATHS.MANAGE_QUERIES; + }; + + return ( + <> +
+ +
+
+ {!isLoading && !isApiError && ( +
+
+

+ {lastEditedQueryName} +

+

+ {lastEditedQueryDescription} +

+
+
+ {canEditQuery && ( + + )} + {(lastEditedQueryObserverCanRun || + isObserverPlus || + isAnyTeamObserverPlus || + canEditQuery) && ( +
+ +
+ )} +
+
+ )} + {!isLoading && !isApiError && ( +
+
+ on, data is sent according to a query’s frequency.`} + > + Automations: + + +
+
+ Log destination:{" "} + +
+
+ )} +
+ + ); + }; + + const renderClippedBanner = () => ( + + } + > +
+ Report clipped. A sample of this query's results is included + below. You can still use query automations to complete this report in + your log destination. +
+
+ ); + + const renderReport = () => { + const disabledCachingGlobally = + config?.server_settings.query_reports_disabled || true; + const discardDataEnabled = storedQuery?.discard_data || true; + const loggingSnapshot = storedQuery?.logging === "snapshot"; + const disabledCaching = + disabledCachingGlobally || discardDataEnabled || !loggingSnapshot; + const emptyCache = (queryReport?.results?.length ?? 0) === 0; + + // Loading state + if (isLoading) { + return ; + } + + // Error state + if (isApiError) { + return ; + } + + // Empty state with varying messages explaining why there's no results + if (emptyCache) { + return ( + + ); + } + return ; + }; + + return ( + +
+ {renderHeader()} + {isClipped && renderClippedBanner()} + {renderReport()} +
+
+ ); +}; + +export default QueryDetailsPage; diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPageConfig.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPageConfig.tsx new file mode 100644 index 0000000000..05ef2ba604 --- /dev/null +++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPageConfig.tsx @@ -0,0 +1,15 @@ +// TODO +export const QUERY_DETAILS_PAGE_FILTER_KEYS = ["model", "vendor"] as const; + +// TODO: refactor to use this type as the location.query prop of the page +export type QueryDetailsPageQueryParams = Record< + | "order_key" + | "order_direction" + | typeof QUERY_DETAILS_PAGE_FILTER_KEYS[number], + string +>; + +export const DEFAULT_SORT_HEADER = "host_name"; +export const DEFAULT_SORT_DIRECTION = "asc"; + +export const QUERY_REPORT_RESULTS_LIMIT = 1000; diff --git a/frontend/pages/queries/details/QueryDetailsPage/_styles.scss b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss new file mode 100644 index 0000000000..cb560d4bf7 --- /dev/null +++ b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss @@ -0,0 +1,79 @@ +.query-details-page { + &__wrapper { + display: flex; + flex-direction: column; + gap: $pad-large; + } + &__title-bar { + display: flex; + justify-content: space-between; + margin-top: $pad-small; + + .name-description { + display: flex; + flex-direction: column; + gap: $pad-small; + } + } + + &__action-button-container { + display: flex; + justify-content: flex-end; + min-width: 266px; + gap: $pad-medium; + } + + &__query-name { + margin-top: 0; + font-size: $large; + } + + &__query-description { + margin-top: 0; + margin-bottom: $pad-small; + font-size: $x-small; + } + + &__settings { + display: flex; + gap: $pad-large; + font-size: $x-small; + // TODO - remove once refactored tooltip wrapper is merged + .component__tooltip-wrapper__element__underline::after { + bottom: 0px; + } + } + + &__automations, + &__log-destination { + display: flex; + gap: $pad-small; + align-items: center; + + .component__tooltip-wrapper__element { + font-weight: $bold; + } + } + + .empty-table__inner { + .component__tooltip-wrapper__tip-text { + text-align: left; + width: 320px; + } + + ul { + color: $core-white; + + li { + &::before { + content: "•"; + color: $core-white; + } + } + } + } + + .data-error { + padding-top: $pad-xxxlarge; + } +} diff --git a/frontend/pages/queries/details/QueryDetailsPage/index.ts b/frontend/pages/queries/details/QueryDetailsPage/index.ts new file mode 100644 index 0000000000..9bb526e7b5 --- /dev/null +++ b/frontend/pages/queries/details/QueryDetailsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./QueryDetailsPage"; diff --git a/frontend/pages/queries/details/components/NoResults/NoResults.tsx b/frontend/pages/queries/details/components/NoResults/NoResults.tsx new file mode 100644 index 0000000000..1657923339 --- /dev/null +++ b/frontend/pages/queries/details/components/NoResults/NoResults.tsx @@ -0,0 +1,115 @@ +import React from "react"; + +import differenceInSeconds from "date-fns/differenceInSeconds"; +import formatDistance from "date-fns/formatDistance"; +import add from "date-fns/add"; + +import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper"; +import EmptyTable from "components/EmptyTable/EmptyTable"; + +interface INoResultsProps { + queryInterval?: number; + queryUpdatedAt?: string; + disabledCaching: boolean; + disabledCachingGlobally: boolean; + discardDataEnabled: boolean; + loggingSnapshot: boolean; +} + +const baseClass = "no-results"; + +const NoResults = ({ + queryInterval, + queryUpdatedAt, + disabledCaching, + disabledCachingGlobally, + discardDataEnabled, + loggingSnapshot, +}: INoResultsProps): JSX.Element => { + // Returns how many seconds it takes to expect a cached update + const secondsCheckbackTime = () => { + const secondsSinceUpdate = queryUpdatedAt + ? differenceInSeconds(new Date(), new Date(queryUpdatedAt)) + : 0; + const secondsUpdateWaittime = (queryInterval || 0) + 60; + return secondsUpdateWaittime - secondsSinceUpdate; + }; + + // Update status of collecting cached results + const collectingResults = secondsCheckbackTime() > 0; + + // Converts seconds takes to update to human readable format + const readableCheckbackTime = formatDistance( + add(new Date(), { seconds: secondsCheckbackTime() }), + new Date() + ); + + // Collecting results state + if (collectingResults) { + const collectingResultsInfo = () => + `Fleet is collecting query results. Check back in about ${readableCheckbackTime}.`; + + return ( + + ); + } + + const noResultsInfo = () => { + if (!queryInterval) { + return ( + <> + This query does not collect data on a schedule. Add a{" "} + frequency or run this as a live query to see results. + + ); + } + if (disabledCaching) { + const tipContent = () => { + if (disabledCachingGlobally) { + return "The following setting prevents saving this query's results in Fleet:
  • Query reports are globally disabled in organization settings.
"; + } + if (discardDataEnabled) { + return "The following setting prevents saving this query's results in Fleet:
  • This query has Discard data enabled.
"; + } + if (!loggingSnapshot) { + return "The following setting prevents saving this query's results in Fleet:
  • The logging setting for this query is not Snapshot.
"; + } + return "Unknown"; + }; + return ( + <> + Results from this query are{" "} + + not reported in Fleet + + . + + ); + } + // No errors will be reported in V1 + // if (errorsOnly) { + // return ( + // <> + // This query had trouble collecting data on some hosts. Check out the{" "} + // Errors tab to see why. + // + // ); + // } + return "This query has returned no data so far."; + }; + + return ( + + ); +}; + +export default NoResults; diff --git a/frontend/pages/queries/details/components/NoResults/index.ts b/frontend/pages/queries/details/components/NoResults/index.ts new file mode 100644 index 0000000000..04bef19e77 --- /dev/null +++ b/frontend/pages/queries/details/components/NoResults/index.ts @@ -0,0 +1 @@ +export { default } from "./NoResults"; diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx new file mode 100644 index 0000000000..ca51bdc450 --- /dev/null +++ b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx @@ -0,0 +1,172 @@ +import React, { useState, useContext, useEffect, useCallback } from "react"; + +import { Row, Column } from "react-table"; +import FileSaver from "file-saver"; +import { QueryContext } from "context/query"; + +import { + generateCSVFilename, + generateCSVQueryResults, +} from "utilities/generate_csv"; +import { IQueryReport, IQueryReportResultRow } from "interfaces/query_report"; + +import Button from "components/buttons/Button"; +import Icon from "components/Icon/Icon"; +import TableContainer from "components/TableContainer"; +import ShowQueryModal from "components/modals/ShowQueryModal"; +import TooltipWrapper from "components/TooltipWrapper"; + +import generateResultsTableHeaders from "./QueryReportTableConfig"; + +interface IQueryReportProps { + queryReport?: IQueryReport; + isClipped?: boolean; +} + +const baseClass = "query-report"; +const CSV_TITLE = "Query"; + +const tableResults = (results: IQueryReportResultRow[]) => { + return results.map((result: IQueryReportResultRow) => { + const hostInfoColumns = { + host_display_name: result.host_name, + last_fetched: result.last_fetched, + }; + + // hostInfoColumns displays the host metadata that is returned with every query + // result.columns are the variable columns returned by the API that differ per query + return { ...hostInfoColumns, ...result.columns }; + }); +}; + +const QueryReport = ({ + queryReport, + isClipped, +}: IQueryReportProps): JSX.Element => { + const { lastEditedQueryName, lastEditedQueryBody } = useContext(QueryContext); + + const [showQueryModal, setShowQueryModal] = useState(false); + const [filteredResults, setFilteredResults] = useState( + tableResults(queryReport?.results || []) + ); + const [tableHeaders, setTableHeaders] = useState([]); + + useEffect(() => { + if (queryReport && queryReport.results && queryReport.results.length > 0) { + const generatedTableHeaders = generateResultsTableHeaders( + tableResults(queryReport.results) + ); + // Update tableHeaders if new headers are found + if (generatedTableHeaders !== tableHeaders) { + setTableHeaders(generatedTableHeaders); + } + } + }, [queryReport]); // Cannot use tableHeaders as it will cause infinite loop with setTableHeaders + + const onExportQueryResults = (evt: React.MouseEvent) => { + evt.preventDefault(); + FileSaver.saveAs( + generateCSVQueryResults( + filteredResults, + generateCSVFilename( + `${lastEditedQueryName || CSV_TITLE} - Query Report` + ), + tableHeaders + ) + ); + }; + + const onShowQueryModal = () => { + setShowQueryModal(!showQueryModal); + }; + + const renderNoResults = () => { + return

TODO

; + }; + + const renderTableButtons = () => { + return ( +
+ + +
+ ); + }; + + const renderResultsCount = useCallback(() => { + const count = filteredResults.length; + + if (isClipped) { + return ( +
+
+ You can reset this report by updating the query's SQL, or by + temporarily enabling the discard data setting and disabling it again.`} + > + {`${count} result${count === 1 ? "" : "s"}`} +
+
+ ); + } + return ( +
+ {`${count} result${count === 1 ? "" : "s"}`} +
+ ); + }, [filteredResults.length, isClipped]); + + const renderTable = () => { + return ( +
+ renderTableButtons()} + setExportRows={setFilteredResults} + renderCount={renderResultsCount} + /> +
+ ); + }; + + return ( +
+ {renderTable()} + {showQueryModal && ( + + )} +
+ ); +}; + +export default QueryReport; diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx new file mode 100644 index 0000000000..05a906fae7 --- /dev/null +++ b/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx @@ -0,0 +1,93 @@ +/* eslint-disable react/prop-types */ +// disable this rule as it was throwing an error in Header and Cell component +// definitions for the selection row for some reason when we dont really need it. +import React from "react"; + +import { + CellProps, + Column, + ColumnInstance, + ColumnInterface, + HeaderProps, + TableInstance, +} from "react-table"; + +import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColumnFilter"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; + +import { humanHostLastSeen } from "utilities/helpers"; + +type IHeaderProps = HeaderProps & { + column: ColumnInstance & IDataColumn; +}; + +type ICellProps = CellProps; + +interface IDataColumn extends ColumnInterface { + title?: string; + accessor: string; +} + +const _unshiftHostname = (headers: IDataColumn[]) => { + const newHeaders = [...headers]; + const displayNameIndex = headers.findIndex( + (h) => h.id === "host_display_name" + ); + if (displayNameIndex >= 0) { + // remove hostname header from headers + const [displayNameHeader] = newHeaders.splice(displayNameIndex, 1); + // reformat title and insert at start of headers array + newHeaders.unshift({ ...displayNameHeader, title: "Host" }); + } + // TODO: Remove after v5 when host_hostname is removed rom API response. + const hostNameIndex = headers.findIndex((h) => h.id === "host_hostname"); + if (hostNameIndex >= 0) { + newHeaders.splice(hostNameIndex, 1); + } + // end remove + return newHeaders; +}; + +const generateResultsTableHeaders = (results: any[]): Column[] => { + /* Results include an array of objects, each representing a table row + Each key value pair in an object represents a column name and value + To create headers, use JS set to create an array of all unique column names */ + const uniqueColumnNames = Array.from( + results.reduce( + (s, o) => Object.keys(o).reduce((t, k) => t.add(k), s), + new Set() // Set prevents listing duplicate headers + ) + ); + + const headers = uniqueColumnNames.map((key) => { + return { + id: key as string, + title: key as string, + Header: (headerProps: IHeaderProps) => ( + + ), + accessor: key as string, + Cell: (cellProps: ICellProps) => { + // Sorts chronologically by date, but UI displays readable last fetched + if (cellProps.column.id === "last_fetched") { + return humanHostLastSeen(cellProps?.cell?.value); + } + return cellProps?.cell?.value || null; + }, + Filter: DefaultColumnFilter, // Component hides filter for last_fetched + filterType: "text", + disableSortBy: false, + }; + }); + return _unshiftHostname(headers); +}; + +export default generateResultsTableHeaders; diff --git a/frontend/pages/queries/details/components/QueryReport/_styles.scss b/frontend/pages/queries/details/components/QueryReport/_styles.scss new file mode 100644 index 0000000000..1d60937eeb --- /dev/null +++ b/frontend/pages/queries/details/components/QueryReport/_styles.scss @@ -0,0 +1,22 @@ +.query-report { + &__wrapper { + .host_id__header { + width: 95px; // Min width for 6 digits host IDs + } + + .last_fetched__header { + .column-header { + margin-bottom: 44px; // Fills space where filter is removed + } + } + + .data-table__wrapper { + overflow-x: scroll; + } + } + + &__results-cta { + display: flex; + gap: $pad-medium; + } +} diff --git a/frontend/pages/queries/details/components/QueryReport/index.ts b/frontend/pages/queries/details/components/QueryReport/index.ts new file mode 100644 index 0000000000..7e9fe702db --- /dev/null +++ b/frontend/pages/queries/details/components/QueryReport/index.ts @@ -0,0 +1 @@ +export { default } from "./QueryReport"; diff --git a/frontend/pages/queries/edit/EditQueryPage.tsx b/frontend/pages/queries/edit/EditQueryPage.tsx new file mode 100644 index 0000000000..03d1ef212b --- /dev/null +++ b/frontend/pages/queries/edit/EditQueryPage.tsx @@ -0,0 +1,351 @@ +import React, { useState, useEffect, useContext } from "react"; +import { useQuery } from "react-query"; +import { useErrorHandler } from "react-error-boundary"; +import { InjectedRouter, Params } from "react-router/lib/Router"; + +import { AppContext } from "context/app"; +import { QueryContext } from "context/query"; +import { DEFAULT_QUERY } from "utilities/constants"; +import configAPI from "services/entities/config"; +import queryAPI from "services/entities/queries"; +import statusAPI from "services/entities/status"; +import { + IGetQueryResponse, + ICreateQueryRequestBody, + ISchedulableQuery, +} from "interfaces/schedulable_query"; + +import QuerySidePanel from "components/side_panels/QuerySidePanel"; +import MainContent from "components/MainContent"; +import SidePanelContent from "components/SidePanelContent"; +import CustomLink from "components/CustomLink"; + +import useTeamIdParam from "hooks/useTeamIdParam"; + +import { NotificationContext } from "context/notification"; + +import PATHS from "router/paths"; +import debounce from "utilities/debounce"; +import deepDifference from "utilities/deep_difference"; + +import BackLink from "components/BackLink"; +import EditQueryForm from "pages/queries/edit/components/EditQueryForm"; +import { IConfig } from "interfaces/config"; + +interface IEditQueryPageProps { + router: InjectedRouter; + params: Params; + location: { + pathname: string; + query: { host_ids: string; team_id?: string }; + search: string; + }; +} + +const baseClass = "edit-query-page"; + +const EditQueryPage = ({ + router, + params: { id: paramsQueryId }, + location, +}: IEditQueryPageProps): JSX.Element => { + const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null; + const { + currentTeamName: teamNameForQuery, + teamIdForApi: apiTeamIdForQuery, + } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); + + const handlePageError = useErrorHandler(); + const { + isGlobalAdmin, + isGlobalMaintainer, + isAnyTeamMaintainerOrTeamAdmin, + isObserverPlus, + isAnyTeamObserverPlus, + } = useContext(AppContext); + const { + selectedOsqueryTable, + setSelectedOsqueryTable, + lastEditedQueryName, + lastEditedQueryDescription, + lastEditedQueryBody, + lastEditedQueryObserverCanRun, + lastEditedQueryFrequency, + lastEditedQueryPlatforms, + lastEditedQueryLoggingType, + lastEditedQueryMinOsqueryVersion, + lastEditedQueryDiscardData, + setLastEditedQueryId, + setLastEditedQueryName, + setLastEditedQueryDescription, + setLastEditedQueryBody, + setLastEditedQueryObserverCanRun, + setLastEditedQueryFrequency, + setLastEditedQueryLoggingType, + setLastEditedQueryMinOsqueryVersion, + setLastEditedQueryPlatforms, + setLastEditedQueryDiscardData, + } = useContext(QueryContext); + const { setConfig } = useContext(AppContext); + const { renderFlash } = useContext(NotificationContext); + + const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState( + false + ); + const [ + showConfirmSaveChangesModal, + setShowConfirmSaveChangesModal, + ] = useState(false); + + const { data: appConfig } = useQuery( + ["config"], + () => configAPI.loadAll(), + { + select: (data: IConfig) => data, + onSuccess: (data) => { + setConfig(data); + }, + } + ); + + // disabled on page load so we can control the number of renders + // else it will re-populate the context on occasion + const { + isLoading: isStoredQueryLoading, + data: storedQuery, + refetch: refetchStoredQuery, + } = useQuery( + ["query", queryId], + () => queryAPI.load(queryId as number), + { + enabled: !!queryId, + refetchOnWindowFocus: false, + select: (data) => data.query, + onSuccess: (returnedQuery) => { + setLastEditedQueryId(returnedQuery.id); + setLastEditedQueryName(returnedQuery.name); + setLastEditedQueryDescription(returnedQuery.description); + setLastEditedQueryBody(returnedQuery.query); + setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run); + setLastEditedQueryFrequency(returnedQuery.interval); + setLastEditedQueryPlatforms(returnedQuery.platform); + setLastEditedQueryLoggingType(returnedQuery.logging); + setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version); + setLastEditedQueryDiscardData(returnedQuery.discard_data); + }, + onError: (error) => handlePageError(error), + } + ); + + const detectIsFleetQueryRunnable = () => { + statusAPI.live_query().catch(() => { + setIsLiveQueryRunnable(false); + }); + }; + + useEffect(() => { + detectIsFleetQueryRunnable(); + if (!queryId) { + setLastEditedQueryId(DEFAULT_QUERY.id); + setLastEditedQueryName(DEFAULT_QUERY.name); + setLastEditedQueryDescription(DEFAULT_QUERY.description); + // Persist lastEditedQueryBody through live query flow instead of resetting to DEFAULT_QUERY.query + setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run); + setLastEditedQueryFrequency(DEFAULT_QUERY.interval); + setLastEditedQueryLoggingType(DEFAULT_QUERY.logging); + setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version); + setLastEditedQueryPlatforms(DEFAULT_QUERY.platform); + setLastEditedQueryDiscardData(DEFAULT_QUERY.discard_data); + } + }, [queryId]); + + const [isQuerySaving, setIsQuerySaving] = useState(false); + const [isQueryUpdating, setIsQueryUpdating] = useState(false); + const [backendValidators, setBackendValidators] = useState<{ + [key: string]: string; + }>({}); + + // Updates title that shows up on browser tabs + useEffect(() => { + // e.g., Query details | Discover TLS certificates | Fleet for osquery + document.title = `Edit query | ${storedQuery?.name} | Fleet for osquery`; + }, [location.pathname, storedQuery?.name]); + + useEffect(() => { + setShowOpenSchemaActionText(!isSidebarOpen); + }, [isSidebarOpen]); + + const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => { + setIsQuerySaving(true); + try { + const { query } = await queryAPI.create(formData); + router.push(PATHS.EDIT_QUERY(query.id)); + renderFlash("success", "Query created!"); + setBackendValidators({}); + } catch (createError: any) { + if (createError.data.errors[0].reason.includes("already exists")) { + const teamErrorText = + teamNameForQuery && apiTeamIdForQuery !== 0 + ? `the ${teamNameForQuery} team` + : "all teams"; + setBackendValidators({ + name: `A query with that name already exists for ${teamErrorText}.`, + }); + } else { + renderFlash( + "error", + "Something went wrong creating your query. Please try again." + ); + setBackendValidators({}); + } + } finally { + setIsQuerySaving(false); + } + }); + + const onUpdateQuery = async (formData: ICreateQueryRequestBody) => { + if (!queryId) { + return false; + } + + setIsQueryUpdating(true); + + const updatedQuery = deepDifference(formData, { + lastEditedQueryName, + lastEditedQueryDescription, + lastEditedQueryBody, + lastEditedQueryObserverCanRun, + lastEditedQueryFrequency, + lastEditedQueryPlatforms, + lastEditedQueryLoggingType, + lastEditedQueryMinOsqueryVersion, + lastEditedQueryDiscardData, + }); + + try { + await queryAPI.update(queryId, updatedQuery); + renderFlash("success", "Query updated!"); + refetchStoredQuery(); // Required to compare recently saved query to a subsequent save to the query + } catch (updateError: any) { + console.error(updateError); + if (updateError.data.errors[0].reason.includes("Duplicate")) { + renderFlash("error", "A query with this name already exists."); + } else { + renderFlash( + "error", + "Something went wrong updating your query. Please try again." + ); + } + } + + setIsQueryUpdating(false); + setShowConfirmSaveChangesModal(false); // Closes conditionally opened modal when discarding previous results + + return false; + }; + + const onOsqueryTableSelect = (tableName: string) => { + setSelectedOsqueryTable(tableName); + }; + + const onCloseSchemaSidebar = () => { + setIsSidebarOpen(false); + }; + + const onOpenSchemaSidebar = () => { + setIsSidebarOpen(true); + }; + + const renderLiveQueryWarning = (): JSX.Element | null => { + if (isLiveQueryRunnable) { + return null; + } + + return ( +
+
+

+ Fleet is unable to run a live query. Refresh the page or log in + again. If this keeps happening please{" "} + +

+
+
+ ); + }; + + // Function instead of constant eliminates race condition + const backToQueriesPath = () => { + return queryId ? PATHS.QUERY(queryId) : PATHS.MANAGE_QUERIES; + }; + + const showSidebar = + isSidebarOpen && + (isGlobalAdmin || + isGlobalMaintainer || + isAnyTeamMaintainerOrTeamAdmin || + isObserverPlus || + isAnyTeamObserverPlus); + + return ( + <> + +
+
+
+ +
+ +
+
+
+ {showSidebar && ( + + + + )} + + ); +}; + +export default EditQueryPage; diff --git a/frontend/pages/queries/edit/_styles.scss b/frontend/pages/queries/edit/_styles.scss new file mode 100644 index 0000000000..944afd1396 --- /dev/null +++ b/frontend/pages/queries/edit/_styles.scss @@ -0,0 +1,86 @@ +.edit-query-page { + .help-text { + margin-top: $pad-small; + margin-bottom: $pad-large; + font-weight: $regular; + font-size: $xx-small; + color: $ui-fleet-black-75; + } + + .fleet-checkbox { + display: flex; + align-items: center; + } + + .form-field { + &--frequency { + margin-bottom: 0; + } + &--platform { + margin-bottom: 0; + margin-top: $pad-large; + } + } + + .advanced-options-toggle { + font-weight: $xbold; + } + + .observer-can-run-wrapper { + margin-bottom: 0; + font-weight: bold; + } + + .body-wrap { + min-width: 0; + } + + &__warning { + padding: $pad-medium; + font-size: $x-small; + color: $core-fleet-black; + background-color: #fff0b9; + border: 1px solid #f2c94c; + border-radius: $border-radius; + margin: 0; + margin-top: $pad-large; + + p { + margin: 0; + line-height: 20px; + } + } + + .ace_content { + min-height: 500px !important; + } + + &__count-spinner { + margin-right: $pad-small; + } + &__page-loading { + .loading-spinner { + margin: $pad-large 0 0; + } + } + &__page-error { + h4 { + margin: 0; + margin-top: 28px; + margin-left: -7px; + font-size: $small; + + img { + transform: scale(0.5); + vertical-align: middle; + position: relative; + top: -2px; + } + } + p { + margin: 0; + margin-top: $pad-medium; + font-size: $x-small; + } + } +} diff --git a/frontend/pages/queries/edit/components/ConfirmSaveChangesModal/ConfirmSaveChangesModal.tsx b/frontend/pages/queries/edit/components/ConfirmSaveChangesModal/ConfirmSaveChangesModal.tsx new file mode 100644 index 0000000000..e09da21090 --- /dev/null +++ b/frontend/pages/queries/edit/components/ConfirmSaveChangesModal/ConfirmSaveChangesModal.tsx @@ -0,0 +1,49 @@ +import React from "react"; + +import Button from "components/buttons/Button"; +import Modal from "components/Modal"; + +const baseClass = "save-changes-modal"; + +export interface IConfirmSaveChangesModalProps { + isUpdating: boolean; + onSaveChanges: (evt: React.MouseEvent) => void; + onClose: () => void; + showChangedSQLCopy?: boolean; +} + +const ConfirmSaveChangesModal = ({ + isUpdating, + onSaveChanges, + onClose, + showChangedSQLCopy = false, +}: IConfirmSaveChangesModalProps) => { + const warningText = showChangedSQLCopy + ? "Changing this query's SQL will delete its previous results, since the existing report does not reflect the updated query." + : "The changes you are making to this query will delete its previous results."; + + return ( + +
+

{warningText}

+

You cannot undo this action.

+
+ + +
+
+
+ ); +}; + +export default ConfirmSaveChangesModal; diff --git a/frontend/pages/queries/edit/components/ConfirmSaveChangesModal/index.ts b/frontend/pages/queries/edit/components/ConfirmSaveChangesModal/index.ts new file mode 100644 index 0000000000..c8c31da396 --- /dev/null +++ b/frontend/pages/queries/edit/components/ConfirmSaveChangesModal/index.ts @@ -0,0 +1 @@ +export { default } from "./ConfirmSaveChangesModal"; diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.stories.tsx b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.stories.tsx new file mode 100644 index 0000000000..7a3b738ff1 --- /dev/null +++ b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.stories.tsx @@ -0,0 +1,14 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import DiscardDataOption from "./DiscardDataOption"; + +const meta: Meta = { + title: "Components/DiscardDataOption", + component: DiscardDataOption, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx new file mode 100644 index 0000000000..011e23d502 --- /dev/null +++ b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx @@ -0,0 +1,86 @@ +import React from "react"; + +import { fireEvent, render, screen } from "@testing-library/react"; + +import DiscardDataOption from "./DiscardDataOption"; + +describe("DiscardDataOption component", () => { + const selectedLoggingType = "snapshot"; + const [discardData, setDiscardData] = [false, jest.fn()]; + + it("Renders normal help text when the global option is not disabled", () => { + render( + + ); + + expect(screen.getByText(/Discard data/)).toBeInTheDocument(); + expect(screen.getByText(/Data will still be sent/)).toBeInTheDocument(); + }); + + it('Renders the "disabled" help text with tooltip when the global option is disabled', async () => { + render( + + ); + + expect(screen.getByText(/Discard data/)).toBeInTheDocument(); + expect(screen.getByText(/This setting is ignored/)).toBeInTheDocument(); + + await fireEvent.mouseOver(screen.getByText(/globally disabled/)); + + expect(screen.getByText(/A Fleet administrator/)).toBeInTheDocument(); + }); + + it('Restores normal help text when disabled and then "Edit anyway" is clicked', async () => { + render( + + ); + + // disabled + expect(screen.getByText(/Discard data/)).toBeInTheDocument(); + expect(screen.getByText(/This setting is ignored/)).toBeInTheDocument(); + + // enable + await fireEvent.click(screen.getByText(/Edit anyway/)); + + // normal text + expect(screen.getByText(/Data will still be sent/)).toBeInTheDocument(); + }); + it('Renders the info banner when "Differential" logging option is selected', () => { + render( + + ); + + expect( + screen.getByText( + /setting is ignored when differential logging is enabled. This/ + ) + ).toBeInTheDocument(); + }); + it('Renders the info banner when "Differential (ignore removals)" logging option is selected', () => { + render( + + ); + expect( + screen.getByText( + /setting is ignored when differential logging is enabled. This/ + ) + ).toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx new file mode 100644 index 0000000000..aef48c79b6 --- /dev/null +++ b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx @@ -0,0 +1,103 @@ +import Checkbox from "components/forms/fields/Checkbox"; +import Icon from "components/Icon"; +import InfoBanner from "components/InfoBanner"; +import TooltipWrapper from "components/TooltipWrapper"; +import { QueryLoggingOption } from "interfaces/schedulable_query"; +import React, { useState } from "react"; +import { Link } from "react-router"; + +const baseClass = "discard-data-option"; + +interface IDiscardDataOptionProps { + queryReportsDisabled: boolean; + selectedLoggingType: QueryLoggingOption; + discardData: boolean; + setDiscardData: (value: boolean) => void; + breakHelpText?: boolean; +} + +const DiscardDataOption = ({ + queryReportsDisabled, + selectedLoggingType, + discardData, + setDiscardData, + breakHelpText = false, +}: IDiscardDataOptionProps) => { + const [forceEditDiscardData, setForceEditDiscardData] = useState(false); + const disable = queryReportsDisabled && !forceEditDiscardData; + + const renderHelpText = () => ( +
+ {disable ? ( + <> + This setting is ignored because query reports in Fleet have been{" "} + \ + Organization settings > Advanced options > Disable query reports." + } + position="bottom" + > + {"globally disabled."} + {" "} + { + e.preventDefault(); + setForceEditDiscardData(true); + }} + className={`${baseClass}__edit-anyway`} + > + <> + Edit anyway + + + + + ) : ( + <> + The most recent results for each host will not be available in Fleet. + {breakHelpText ?
: " "} + Data will still be sent to your log destination if + automations + {" "} + are on. + + )} +
+ ); + return ( +
+ {["differential", "differential_ignore_removals"].includes( + selectedLoggingType + ) && ( + + <> + The Discard data setting is ignored when differential logging + is enabled. This
+ query's results will not be saved in Fleet. + +
+ )} + + Discard data + + {renderHelpText()} +
+ ); +}; + +export default DiscardDataOption; diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/_styles.scss b/frontend/pages/queries/edit/components/DiscardDataOption/_styles.scss new file mode 100644 index 0000000000..c938b58069 --- /dev/null +++ b/frontend/pages/queries/edit/components/DiscardDataOption/_styles.scss @@ -0,0 +1,20 @@ +.discard-data-option { + .info-banner { + margin-bottom: 1.5rem; + &__info { + line-height: 21px; + } + } + + &__disabled-discard-data-checkbox { + @include disabled; + } + + &__edit-anyway { + display: inline-flex; + align-items: center; + cursor: pointer; + font-weight: inherit; + font-size: inherit; + } +} diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/index.ts b/frontend/pages/queries/edit/components/DiscardDataOption/index.ts new file mode 100644 index 0000000000..71d3111a3b --- /dev/null +++ b/frontend/pages/queries/edit/components/DiscardDataOption/index.ts @@ -0,0 +1 @@ +export { default } from "./DiscardDataOption"; diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tests.tsx b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx similarity index 92% rename from frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tests.tsx rename to frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx index 87bc29c8fd..a203afbf96 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tests.tsx +++ b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx @@ -5,7 +5,7 @@ import { createCustomRenderer } from "test/test-utils"; import createMockQuery from "__mocks__/queryMock"; import createMockUser from "__mocks__/userMock"; -import QueryForm from "./QueryForm"; +import EditQueryForm from "./EditQueryForm"; const mockQuery = createMockQuery(); const mockRouter = { @@ -20,7 +20,7 @@ const mockRouter = { createPath: jest.fn(), }; -describe("QueryForm - component", () => { +describe("EditQueryForm - component", () => { it("disables save button for missing query name", async () => { const render = createCustomRenderer({ context: { @@ -56,7 +56,7 @@ describe("QueryForm - component", () => { }); render( - { isQueryUpdating={false} saveQuery={jest.fn()} onOsqueryTableSelect={jest.fn()} - goToSelectTargets={jest.fn()} onUpdate={jest.fn()} onOpenSchemaSidebar={jest.fn()} renderLiveQueryWarning={jest.fn()} backendValidators={{}} + showConfirmSaveChangesModal={false} + setShowConfirmSaveChangesModal={jest.fn()} /> ); diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx similarity index 86% rename from frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx rename to frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx index 8dd8cb0388..b9cea87652 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx @@ -15,7 +15,11 @@ import PATHS from "router/paths"; import { AppContext } from "context/app"; import { QueryContext } from "context/query"; import { NotificationContext } from "context/notification"; -import { addGravatarUrlToResource, secondsToDhms } from "utilities/helpers"; +import { + addGravatarUrlToResource, + secondsToDhms, + TAGGED_TEMPLATES, +} from "utilities/helpers"; import { FREQUENCY_DROPDOWN_OPTIONS, SCHEDULE_PLATFORM_DROPDOWN_OPTIONS, @@ -48,10 +52,12 @@ import Spinner from "components/Spinner"; import Icon from "components/Icon/Icon"; import AutoSizeInputField from "components/forms/fields/AutoSizeInputField"; import SaveQueryModal from "../SaveQueryModal"; +import ConfirmSaveChangesModal from "../ConfirmSaveChangesModal"; +import DiscardDataOption from "../DiscardDataOption"; -const baseClass = "query-form"; +const baseClass = "edit-query-form"; -interface IQueryFormProps { +interface IEditQueryFormProps { router: InjectedRouter; queryIdForEdit: number | null; apiTeamIdForQuery?: number; @@ -63,11 +69,14 @@ interface IQueryFormProps { isQueryUpdating: boolean; saveQuery: (formData: ICreateQueryRequestBody) => void; onOsqueryTableSelect: (tableName: string) => void; - goToSelectTargets: () => void; onUpdate: (formData: ICreateQueryRequestBody) => void; onOpenSchemaSidebar: () => void; renderLiveQueryWarning: () => JSX.Element | null; backendValidators: { [key: string]: string }; + hostId?: number; + queryReportsDisabled?: boolean; + showConfirmSaveChangesModal: boolean; + setShowConfirmSaveChangesModal: (bool: boolean) => void; } const validateQuerySQL = (query: string) => { @@ -98,7 +107,7 @@ const customFrequencyOptions = (frequency: number) => { return FREQUENCY_DROPDOWN_OPTIONS; }; -const QueryForm = ({ +const EditQueryForm = ({ router, queryIdForEdit, apiTeamIdForQuery, @@ -110,12 +119,15 @@ const QueryForm = ({ isQueryUpdating, saveQuery, onOsqueryTableSelect, - goToSelectTargets, onUpdate, onOpenSchemaSidebar, renderLiveQueryWarning, backendValidators, -}: IQueryFormProps): JSX.Element => { + hostId, + queryReportsDisabled, + showConfirmSaveChangesModal, + setShowConfirmSaveChangesModal, +}: IEditQueryFormProps): JSX.Element => { // Note: The QueryContext values should always be used for any mutable query data such as query name // The storedQuery prop should only be used to access immutable metadata such as author id const { @@ -128,6 +140,7 @@ const QueryForm = ({ lastEditedQueryPlatforms, lastEditedQueryMinOsqueryVersion, lastEditedQueryLoggingType, + lastEditedQueryDiscardData, setLastEditedQueryName, setLastEditedQueryDescription, setLastEditedQueryBody, @@ -136,6 +149,7 @@ const QueryForm = ({ setLastEditedQueryPlatforms, setLastEditedQueryMinOsqueryVersion, setLastEditedQueryLoggingType, + setLastEditedQueryDiscardData, } = useContext(QueryContext); const { @@ -169,9 +183,7 @@ const QueryForm = ({ const { setCompatiblePlatforms } = platformCompatibility; const debounceSQL = useDebouncedCallback((sql: string) => { - let valid = true; - const { valid: isValidated, errors: newErrors } = validateQuerySQL(sql); - valid = isValidated; + const { errors: newErrors } = validateQuerySQL(sql); setErrors({ ...newErrors, @@ -194,17 +206,14 @@ const QueryForm = ({ } }, [lastEditedQueryFrequency, isInitialFrequency]); - const hasTeamMaintainerPermissions = savedQueryMode - ? isAnyTeamMaintainerOrTeamAdmin && - storedQuery && - currentUser && - storedQuery.author_id === currentUser.id - : isAnyTeamMaintainerOrTeamAdmin; - const toggleSaveQueryModal = () => { setShowSaveQueryModal(!showSaveQueryModal); }; + const toggleConfirmSaveChangesModal = () => { + setShowConfirmSaveChangesModal(!showConfirmSaveChangesModal); + }; + const onLoad = (editor: IAceEditor) => { editor.setOptions({ enableLinking: true, @@ -399,6 +408,7 @@ const QueryForm = ({ platform: lastEditedQueryPlatforms, min_osquery_version: lastEditedQueryMinOsqueryVersion, logging: lastEditedQueryLoggingType, + discard_data: lastEditedQueryDiscardData, }); } } @@ -576,7 +586,12 @@ const QueryForm = ({ @@ -587,6 +602,28 @@ const QueryForm = ({ const hasSavePermissions = isGlobalAdmin || isGlobalMaintainer; + const currentlySavingQueryResults = + storedQuery && + !storedQuery.discard_data && + !["differential", "differential_ignore_removals"].includes( + storedQuery.logging + ); + const changedSQL = storedQuery && lastEditedQueryBody !== storedQuery.query; + const changedLoggingToDifferential = [ + "differential", + "differential_ignore_removals", + ].includes(lastEditedQueryLoggingType); + + const enabledDiscardData = + storedQuery && lastEditedQueryDiscardData && !storedQuery.discard_data; + + const confirmChanges = + currentlySavingQueryResults && + (changedSQL || changedLoggingToDifferential || enabledDiscardData); + + const showChangedSQLCopy = + changedSQL && !changedLoggingToDifferential && !enabledDiscardData; + // Global admin, any maintainer, any observer+ on new query const renderEditableQueryForm = () => { // Save disabled for team maintainer/admins viewing global queries @@ -618,7 +655,9 @@ const QueryForm = ({ onLoad={onLoad} wrapperClassName={`${baseClass}__text-editor-wrapper`} onChange={onChangeQuery} - handleSubmit={promptSaveQuery} + handleSubmit={ + confirmChanges ? toggleConfirmSaveChangesModal : promptSaveQuery + } wrapEnabled focus={!savedQueryMode} /> @@ -635,10 +674,11 @@ const QueryForm = ({ placeholder={"Every day"} value={lastEditedQueryFrequency} label={"Frequency"} - wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`} + wrapperClassName={`${baseClass}__form-field form-field--frequency`} /> - If automations are on, this is how often your query collects - data. +
+ This is how often your query collects data. +
setLastEditedQueryObserverCanRun(value) } - wrapperClassName={`${baseClass}__query-observer-can-run-wrapper`} + wrapperClassName={"observer-can-run-wrapper"} > Observers can run -

+

Users with the observer role will be able to run this query on hosts where they have access. -

+
+
+ By default, your query collects data on all compatible + platforms. +
+ {queryReportsDisabled !== undefined && ( + + )} )} @@ -711,7 +763,7 @@ const QueryForm = ({ Save as new )} -
+
{ + router.push( + PATHS.LIVE_QUERY(queryIdForEdit) + + TAGGED_TEMPLATES.queryByHostRoute(hostId) + ); + }} > Live query @@ -764,6 +825,15 @@ const QueryForm = ({ toggleSaveQueryModal={toggleSaveQueryModal} backendValidators={backendValidators} isLoading={isQuerySaving} + queryReportsDisabled={queryReportsDisabled} + /> + )} + {showConfirmSaveChangesModal && ( + )} @@ -791,4 +861,4 @@ const QueryForm = ({ return renderEditableQueryForm(); }; -export default QueryForm; +export default EditQueryForm; diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss b/frontend/pages/queries/edit/components/EditQueryForm/_styles.scss similarity index 86% rename from frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss rename to frontend/pages/queries/edit/components/EditQueryForm/_styles.scss index 5a9ab0d49f..f2ee6ab788 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss +++ b/frontend/pages/queries/edit/components/EditQueryForm/_styles.scss @@ -1,13 +1,8 @@ -.query-form { +.edit-query-form { &__wrapper { position: relative; font-size: $x-small; - .query-page__warning { - margin: 0; - margin-top: $pad-large; - } - .form-field--input { margin: 0; } @@ -51,7 +46,7 @@ .query-name-wrapper { display: flex; - &:not(.query-form--editing) { + &:not(.edit-query-form--editing) { textarea:hover { cursor: pointer; color: $core-vibrant-blue; @@ -62,7 +57,7 @@ top: 13px; margin-left: 0; } - .query-form__query-name, + .edit-query-form__query-name, .input-sizer::after { font-size: $large; } @@ -75,7 +70,7 @@ .query-description-wrapper { display: flex; padding-top: $pad-small; - &:not(.query-form--editing) { + &:not(.edit-query-form--editing) { textarea:hover { cursor: pointer; color: $core-vibrant-blue; @@ -166,26 +161,6 @@ } } - &__advanced-options { - margin-top: $pad-medium; - } - - &__query-observer-can-run-wrapper { - margin: 0; - margin-top: $pad-large; - font-weight: $bold !important; // override checkbox default - - & + p { - margin: 0; - margin-top: $pad-small; - } - - .fleet-checkbox { - display: flex; - align-items: center; - } - } - &__button-wrap { margin: 0; margin-top: $pad-large; diff --git a/frontend/pages/queries/edit/components/EditQueryForm/index.ts b/frontend/pages/queries/edit/components/EditQueryForm/index.ts new file mode 100644 index 0000000000..a657cb4f46 --- /dev/null +++ b/frontend/pages/queries/edit/components/EditQueryForm/index.ts @@ -0,0 +1 @@ +export { default } from "./EditQueryForm"; diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx similarity index 100% rename from frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx rename to frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx similarity index 100% rename from frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx rename to frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss b/frontend/pages/queries/edit/components/QueryResults/_styles.scss similarity index 100% rename from frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss rename to frontend/pages/queries/edit/components/QueryResults/_styles.scss diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/index.ts b/frontend/pages/queries/edit/components/QueryResults/index.ts similarity index 100% rename from frontend/pages/queries/QueryPage/components/QueryResults/index.ts rename to frontend/pages/queries/edit/components/QueryResults/index.ts diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx similarity index 51% rename from frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx rename to frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx index f74a45f66c..f7b3a341ee 100644 --- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx +++ b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx @@ -23,6 +23,7 @@ import { ISchedulableQuery, QueryLoggingOption, } from "interfaces/schedulable_query"; +import DiscardDataOption from "../DiscardDataOption"; const baseClass = "save-query-modal"; export interface ISaveQueryModalProps { @@ -33,6 +34,7 @@ export interface ISaveQueryModalProps { toggleSaveQueryModal: () => void; backendValidators: { [key: string]: string }; existingQuery?: ISchedulableQuery; + queryReportsDisabled?: boolean; } const validateQueryName = (name: string) => { @@ -54,6 +56,7 @@ const SaveQueryModal = ({ toggleSaveQueryModal, backendValidators, existingQuery, + queryReportsDisabled, }: ISaveQueryModalProps): JSX.Element => { const [name, setName] = useState(""); const [description, setDescription] = useState(""); @@ -73,6 +76,7 @@ const SaveQueryModal = ({ setSelectedLoggingType, ] = useState(existingQuery?.logging ?? "snapshot"); const [observerCanRun, setObserverCanRun] = useState(false); + const [discardData, setDiscardData] = useState(false); const [errors, setErrors] = useState<{ [key: string]: string }>( backendValidators ); @@ -108,6 +112,7 @@ const SaveQueryModal = ({ description, interval: selectedFrequency, observer_can_run: observerCanRun, + discard_data: discardData, platform: selectedPlatformOptions, min_osquery_version: selectedMinOsqueryVersionOptions, logging: selectedLoggingType, @@ -141,116 +146,122 @@ const SaveQueryModal = ({ return ( - <> -
+ setName(value)} + value={name} + error={errors.name} + inputClassName={`${baseClass}__name`} + label="Name" + placeholder="What is your query called?" + autofocus + ignore1password + /> + setDescription(value)} + value={description} + inputClassName={`${baseClass}__description`} + label="Description" + type="textarea" + placeholder="What information does your query reveal? (optional)" + /> + { + setSelectedFrequency(value); + }} + placeholder={"Every hour"} + value={selectedFrequency} + label="Frequency" + wrapperClassName={`${baseClass}__form-field form-field--frequency`} + /> +
+ This is how often your query collects data. +
+ - setName(value)} - value={name} - error={errors.name} - inputClassName={`${baseClass}__name`} - label="Name" - placeholder="What is your query called?" - autofocus - ignore1password - /> - setDescription(value)} - value={description} - inputClassName={`${baseClass}__description`} - label="Description" - type="textarea" - placeholder="What information does your query reveal? (optional)" - /> - { - setSelectedFrequency(value); - }} - placeholder={"Every hour"} - value={selectedFrequency} - label="Frequency" - wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`} - /> -

- If automations are on, this is how often your query collects data. -

- +
+ Users with the Observer role will be able to run this query as a live + query. +
+ + {showAdvancedOptions && ( + <> + +
+ By default, your query collects data on all compatible platforms. +
+ + + {queryReportsDisabled !== undefined && ( + + )} + + )} +
+ - -
- - + Save + + +
+ ); }; diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts b/frontend/pages/queries/edit/components/SaveQueryModal/index.ts similarity index 100% rename from frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts rename to frontend/pages/queries/edit/components/SaveQueryModal/index.ts diff --git a/frontend/pages/queries/edit/index.ts b/frontend/pages/queries/edit/index.ts new file mode 100644 index 0000000000..29c0d100ac --- /dev/null +++ b/frontend/pages/queries/edit/index.ts @@ -0,0 +1 @@ +export { default } from "./EditQueryPage"; diff --git a/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx new file mode 100644 index 0000000000..127f3b17c2 --- /dev/null +++ b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx @@ -0,0 +1,219 @@ +import React, { useState, useEffect, useContext, useCallback } from "react"; +import { useQuery } from "react-query"; +import { useErrorHandler } from "react-error-boundary"; +import { InjectedRouter, Params } from "react-router/lib/Router"; +import PATHS from "router/paths"; + +import { AppContext } from "context/app"; +import { QueryContext } from "context/query"; +import { LIVE_QUERY_STEPS, DEFAULT_QUERY } from "utilities/constants"; +import queryAPI from "services/entities/queries"; +import hostAPI from "services/entities/hosts"; +import statusAPI from "services/entities/status"; +import { IHost, IHostResponse } from "interfaces/host"; +import { ILabel } from "interfaces/label"; +import { ITeam } from "interfaces/team"; +import { + IGetQueryResponse, + ISchedulableQuery, +} from "interfaces/schedulable_query"; + +import MainContent from "components/MainContent"; +import SelectTargets from "components/LiveQuery/SelectTargets"; + +import RunQuery from "pages/queries/live/screens/RunQuery"; +import useTeamIdParam from "hooks/useTeamIdParam"; + +interface IRunQueryPageProps { + router: InjectedRouter; + params: Params; + location: { + pathname: string; + query: { host_ids: string; team_id?: string }; + search: string; + }; +} + +const baseClass = "run-query-page"; + +const RunQueryPage = ({ + router, + params: { id: paramsQueryId }, + location, +}: IRunQueryPageProps): JSX.Element => { + const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null; + const { + currentTeamName: teamNameForQuery, + teamIdForApi: apiTeamIdForQuery, + } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); + + const handlePageError = useErrorHandler(); + const { + isGlobalAdmin, + isGlobalMaintainer, + isAnyTeamMaintainerOrTeamAdmin, + isObserverPlus, + isAnyTeamObserverPlus, + } = useContext(AppContext); + const { + selectedQueryTargets, + setSelectedQueryTargets, + selectedQueryTargetsByType, + setSelectedQueryTargetsByType, + setLastEditedQueryId, + setLastEditedQueryName, + setLastEditedQueryDescription, + setLastEditedQueryBody, + setLastEditedQueryObserverCanRun, + setLastEditedQueryFrequency, + setLastEditedQueryLoggingType, + setLastEditedQueryMinOsqueryVersion, + setLastEditedQueryPlatforms, + } = useContext(QueryContext); + + const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false); + const [step, setStep] = useState(LIVE_QUERY_STEPS[1]); + const [targetedHosts, setTargetedHosts] = useState( + selectedQueryTargetsByType.hosts + ); + const [targetedLabels, setTargetedLabels] = useState( + selectedQueryTargetsByType.labels + ); + const [targetedTeams, setTargetedTeams] = useState( + selectedQueryTargetsByType.teams + ); + const [targetsTotalCount, setTargetsTotalCount] = useState(0); + const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true); + + // disabled on page load so we can control the number of renders + // else it will re-populate the context on occasion + const { data: storedQuery } = useQuery< + IGetQueryResponse, + Error, + ISchedulableQuery + >(["query", queryId], () => queryAPI.load(queryId as number), { + enabled: !!queryId, + refetchOnWindowFocus: false, + select: (data) => data.query, + onSuccess: (returnedQuery) => { + setLastEditedQueryId(returnedQuery.id); + setLastEditedQueryName(returnedQuery.name); + setLastEditedQueryDescription(returnedQuery.description); + setLastEditedQueryBody(returnedQuery.query); + setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run); + setLastEditedQueryFrequency(returnedQuery.interval); + setLastEditedQueryPlatforms(returnedQuery.platform); + setLastEditedQueryLoggingType(returnedQuery.logging); + setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version); + }, + onError: (error) => handlePageError(error), + }); + + useQuery( + "hostFromURL", + () => + hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)), + { + enabled: !!location.query.host_ids && !queryParamHostsAdded, + select: (data: IHostResponse) => data.host, + onSuccess: (host) => { + setTargetedHosts((prevHosts) => + prevHosts.filter((h) => h.id !== host.id).concat(host) + ); + const targets = selectedQueryTargets; + host.target_type = "hosts"; + targets.push(host); + setSelectedQueryTargets([...targets]); + if (!queryParamHostsAdded) { + setQueryParamHostsAdded(true); + } + router.replace(location.pathname); + }, + } + ); + + const detectIsFleetQueryRunnable = () => { + statusAPI.live_query().catch(() => { + setIsLiveQueryRunnable(false); + }); + }; + + useEffect(() => { + detectIsFleetQueryRunnable(); + }, [queryId]); + + useEffect(() => { + setSelectedQueryTargetsByType({ + hosts: targetedHosts, + labels: targetedLabels, + teams: targetedTeams, + }); + }, [targetedLabels, targetedHosts, targetedTeams]); + + console.log( + "LiveQueryPage.tsx: selectedQueryTargetsByType", + selectedQueryTargetsByType + ); + + // Updates title that shows up on browser tabs + useEffect(() => { + // e.g., Run live query | Discover TLS certificates | Fleet for osquery + document.title = `Run live query | ${storedQuery?.name} | Fleet for osquery`; + }, [location.pathname, storedQuery?.name]); + + const goToQueryEditor = useCallback( + () => + queryId + ? router.push(PATHS.EDIT_QUERY(queryId)) + : router.push(PATHS.NEW_QUERY()), + [] + ); + + const renderScreen = () => { + const step1Props = { + baseClass, + queryId, + selectedTargets: selectedQueryTargets, + targetedHosts, + targetedLabels, + targetedTeams, + targetsTotalCount, + goToQueryEditor, + goToRunQuery: () => setStep(LIVE_QUERY_STEPS[2]), + setSelectedTargets: setSelectedQueryTargets, + setTargetedHosts, + setTargetedLabels, + setTargetedTeams, + setTargetsTotalCount, + }; + + const step2Props = { + queryId, + selectedTargets: selectedQueryTargets, + storedQuery, + setSelectedTargets: setSelectedQueryTargets, + goToQueryEditor, + targetsTotalCount, + }; + + switch (step) { + case LIVE_QUERY_STEPS[2]: + return ; + default: + return ; + } + }; + + return ( + +
{renderScreen()}
+
+ ); +}; + +export default RunQueryPage; diff --git a/frontend/pages/queries/QueryPage/_styles.scss b/frontend/pages/queries/live/LiveQueryPage/_styles.scss similarity index 69% rename from frontend/pages/queries/QueryPage/_styles.scss rename to frontend/pages/queries/live/LiveQueryPage/_styles.scss index 8e1198b7f8..1ec2385d09 100644 --- a/frontend/pages/queries/QueryPage/_styles.scss +++ b/frontend/pages/queries/live/LiveQueryPage/_styles.scss @@ -1,4 +1,4 @@ -.query-page { +.run-query-page { .body-wrap { min-width: 0; } @@ -9,61 +9,6 @@ min-height: 400px; } - &__warning { - padding: $pad-medium; - font-size: $x-small; - color: $core-fleet-black; - background-color: #fff0b9; - border: 1px solid #f2c94c; - border-radius: $border-radius; - - p { - margin: 0; - line-height: 20px; - } - } - - &__observer-query-view { - width: 90%; - max-width: 1060px; - margin: 0 auto; - color: $core-fleet-black; - - h1 { - font-size: $medium; - } - p { - font-size: $x-small; - } - } - - &__observer-query-details { - padding: 0 2rem; - - h1 { - margin: $pad-large 0; - font-size: $large; - } - - p { - margin-bottom: $pad-small; - } - - .sql-button { - color: $core-vibrant-blue; - font-weight: $bold; - font-size: $x-small; - } - } - - &__query-preview { - margin-top: 15px; - - .fleet-ace__label { - display: none; - } - } - .ace_content { min-height: 500px !important; } @@ -172,10 +117,4 @@ font-size: $x-small; } } - - .targets-input { - .input-icon-field__icon { - top: 34px; // Override styling to include label header - } - } } diff --git a/frontend/pages/queries/live/LiveQueryPage/index.ts b/frontend/pages/queries/live/LiveQueryPage/index.ts new file mode 100644 index 0000000000..354c0445d1 --- /dev/null +++ b/frontend/pages/queries/live/LiveQueryPage/index.ts @@ -0,0 +1 @@ +export { default } from "./LiveQueryPage"; diff --git a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx b/frontend/pages/queries/live/screens/RunQuery.tsx similarity index 98% rename from frontend/pages/queries/QueryPage/screens/RunQuery.tsx rename to frontend/pages/queries/live/screens/RunQuery.tsx index dbceec8069..7715899b34 100644 --- a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx +++ b/frontend/pages/queries/live/screens/RunQuery.tsx @@ -15,7 +15,7 @@ import { ICampaign, ICampaignState } from "interfaces/campaign"; import { IQuery } from "interfaces/query"; import { ITarget } from "interfaces/target"; -import QueryResults from "../components/QueryResults"; +import QueryResults from "../../edit/components/QueryResults"; interface IRunQueryProps { storedQuery: IQuery | undefined; diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index 51602d0ef5..b376f7417b 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -36,7 +36,9 @@ import ManagePoliciesPage from "pages/policies/ManagePoliciesPage"; import NoAccessPage from "pages/NoAccessPage"; import PackComposerPage from "pages/packs/PackComposerPage"; import PolicyPage from "pages/policies/PolicyPage"; -import QueryPage from "pages/queries/QueryPage"; +import QueryDetailsPage from "pages/queries/details/QueryDetailsPage"; +import LiveQueryPage from "pages/queries/live/LiveQueryPage"; +import EditQueryPage from "pages/queries/edit/EditQueryPage"; import RegistrationPage from "pages/RegistrationPage"; import ResetPasswordPage from "pages/ResetPasswordPage"; import MDMAppleSSOPage from "pages/MDMAppleSSOPage"; @@ -223,9 +225,16 @@ const routes = ( - + + + + + + + + + - diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index 109001a85c..6babce6bba 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -53,6 +53,16 @@ export default { return `${URL_PREFIX}/labels/${labelId}`; }, EDIT_QUERY: (queryId: number, teamId?: number): string => { + return `${URL_PREFIX}/queries/${queryId}/edit${ + teamId ? `?team_id=${teamId}` : "" + }`; + }, + LIVE_QUERY: (queryId: number | null, teamId?: number): string => { + return `${URL_PREFIX}/queries/${queryId || "new"}/live${ + teamId ? `?team_id=${teamId}` : "" + }`; + }, + QUERY: (queryId: number, teamId?: number): string => { return `${URL_PREFIX}/queries/${queryId}${ teamId ? `?team_id=${teamId}` : "" }`; diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 89765f6481..355429d316 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import sendRequest, { getError } from "services"; import endpoints from "utilities/endpoints"; -import { ISelectedTargets } from "interfaces/target"; +import { ISelectedTargetsForApi } from "interfaces/target"; import { AxiosResponse } from "axios"; import { ICreateQueryRequestBody, @@ -52,12 +52,12 @@ export default { }: { query: string; queryId: number | null; - selected: ISelectedTargets; + selected: ISelectedTargetsForApi; }) => { - const { RUN_QUERY } = endpoints; + const { LIVE_QUERY } = endpoints; try { - const { campaign } = await sendRequest("POST", RUN_QUERY, { + const { campaign } = await sendRequest("POST", LIVE_QUERY, { query, query_id: queryId, selected, diff --git a/frontend/services/entities/query_report.ts b/frontend/services/entities/query_report.ts new file mode 100644 index 0000000000..02855a2644 --- /dev/null +++ b/frontend/services/entities/query_report.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import sendRequest from "services"; +import endpoints from "utilities/endpoints"; + +import { buildQueryStringFromParams } from "utilities/url"; + +export interface ISortOption { + key: string; + direction: string; +} + +export interface ILoadQueryReportOptions { + id: number; + sortBy: ISortOption[]; +} + +const getSortParams = (sortOptions?: ISortOption[]) => { + if (sortOptions === undefined || sortOptions.length === 0) { + return {}; + } + + const sortItem = sortOptions[0]; + return { + order_key: sortItem.key, + order_direction: sortItem.direction, + }; +}; + +export default { + load: ({ id, sortBy }: ILoadQueryReportOptions) => { + const sortParams = getSortParams(sortBy); + + const { QUERIES } = endpoints; + + const queryParams = { + order_key: sortParams.order_key, + order_direction: sortParams.order_direction, + }; + + const queryString = buildQueryStringFromParams(queryParams); + + const endpoint = `${QUERIES}/${id}/report`; + const path = `${endpoint}?${queryString}`; + return sendRequest("GET", path); + }, +}; diff --git a/frontend/services/entities/targets.ts b/frontend/services/entities/targets.ts index 0e00bc286f..400733da22 100644 --- a/frontend/services/entities/targets.ts +++ b/frontend/services/entities/targets.ts @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import sendRequest from "services"; import { IHost } from "interfaces/host"; -import { ISelectedTargets, ITargetsAPIResponse } from "interfaces/target"; +import { ISelectedTargetsForApi, ITargetsAPIResponse } from "interfaces/target"; import endpoints from "utilities/endpoints"; import appendTargetTypeToTargets from "utilities/append_target_type_to_targets"; interface ITargetsProps { query?: string; queryId?: number | null; - selected: ISelectedTargets; + selected: ISelectedTargetsForApi; } const defaultSelected = { @@ -29,7 +29,7 @@ export interface ITargetsSearchResponse { export interface ITargetsCountParams { query_id?: number | null; - selected: ISelectedTargets | null; + selected: ISelectedTargetsForApi | null; } export interface ITargetsCountResponse { diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index 50ebcbf1f9..5fd108c994 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -33,6 +33,8 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { "queries/7": RESPONSES.globalQuery6, "queries/8": RESPONSES.teamQuery2, "queries?team_id=13": RESPONSES.teamQueries, + "queries/113/report?order_key=host_name&order_direction=asc": + RESPONSES.queryReport, }, POST: { // request body is ISelectedTargets diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index d20d275f8f..6533487f5b 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -598,6 +598,9990 @@ const teamQueries = { ], }; +const queryReport = { + query_id: 31, + results: [ + { + host_id: 1, + host_name: "foo", + last_fetched: "2021-01-19T17:08:31Z", + columns: { + model: "Razer Viper", + vendor: "Razer", + model_id: "0078", + }, + }, + { + host_id: 1, + host_name: "foo", + last_fetched: "2021-01-19T17:08:31Z", + columns: { + model: "USB Keyboard", + vendor: "VIA Labs, Inc.", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB Reciever", + vendor: "Logitech", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB Keyboard", + vendor: "Logitech", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "YubiKey OTP+FIDO+CCID", + vendor: "Yubico", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Lenovo USB Optical Mouse", + vendor: "PixArt", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Lenovo Traditional USB Keyboard", + vendor: "Lenovo", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Display Audio", + vendor: "Bose", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB-C Digital AV Multiport Adapter", + vendor: "Apple, Inc.", + model_id: "1460", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB Reciever", + vendor: "Logitech", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB-C Digital AV Multiport Adapter", + vendor: "Apple Inc.", + model_id: "1460", + }, + }, + { + host_id: 2, + host_name: "bar", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "USB Reciever", + vendor: "Logitech", + }, + }, + { + host_id: 3, + host_name: "zoo", + last_fetched: "2022-04-09T17:20:00Z", + columns: { + model: "Logitech Webcam C925e", + model_id: "085b", + }, + }, + { + host_id: 3, + host_name: "zoo", + last_fetched: "2022-04-09T17:20:00Z", + columns: { + model: "Display Audio", + vendor: "Apple Inc.", + }, + }, + { + host_id: 3, + host_name: "zoo", + last_fetched: "2022-04-09T17:20:00Z", + columns: { + model: "Ambient Light Sensor", + vendor: "Apple Inc.", + }, + }, + { + host_id: 3, + host_name: "zoo", + last_fetched: "2022-04-09T17:20:00Z", + columns: { + model: "DELL Laser Mouse", + model_id: "4d51", + }, + }, + { + host_id: 7, + host_name: "Rachel's Magnificent Testing Computer of All Computers", + last_fetched: "2023-09-21T19:03:30Z", + columns: { + model: "AppleUSBVHCIBCE Root Hub Simulation", + vendor: "Apple Inc.", + }, + }, + { + host_id: 7, + host_name: "Rachel's Magnificent Testing Computer of All Computers", + last_fetched: "2023-09-21T19:03:30Z", + columns: { + model: "QuickFire Rapid keyboard", + vendor: "CM Storm", + model_id: "0004", + }, + }, + { + host_id: 7, + host_name: "Rachel's Magnificent Testing Computer of All Computers", + last_fetched: "2023-09-21T19:03:30Z", + columns: { + model: "Lenovo USB Optical Mouse", + vendor: "Lenovo", + }, + }, + { + host_id: 7, + host_name: "Rachel's Magnificent Testing Computer of All Computers", + last_fetched: "2023-09-21T19:03:30Z", + columns: { + model: "YubiKey FIDO+CCID", + vendor: "Yubico", + }, + }, + { + host_id: 4, + host_name: "car", + last_fetched: "2023-01-14T12:40:30Z", + columns: { + model: "USB2.0 Hub", + vendor: "Apple Inc.", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "FaceTime HD Camera (Display)", + vendor: "Apple Inc.", + model_id: "1112", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Apple Internal Keyboard / Trackpad", + model_id: "027e", + vendor: "Apple Inc.", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Apple Thunderbolt Display", + vendor: "Apple Inc.", + model_id: "9227", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "AppleUSBXHCI Root Hub Simulation", + vendor: "Apple Inc.", + model_id: "8007", + }, + }, + { + host_id: 8, + host_name: "apple man", + last_fetched: "2021-01-19T17:20:00Z", + columns: { + model: "Apple T2 Controller", + vendor: "Apple Inc.", + model_id: "8233", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "4-Port USB 2.0 Hub", + vendor: "Generic", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "USB 10_100_1000 LAN", + vendor: "Realtek", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "Display Audio", + vendor: "Apple Inc.", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "USB Mouse", + vendor: "Razor", + }, + }, + { + host_id: 5, + host_name: "choo", + last_fetched: "2023-09-03T03:40:30Z", + columns: { + model: "USB Audio", + vendor: "Apple, Inc.", + }, + }, + { + host_id: 6, + host_name: "moo", + last_fetched: "2023-09-20T07:02:34Z", + columns: { + model: "Display Audio", + vendor: "Apple Inc.", + }, + }, + { + host_id: 6, + host_name: "moo", + last_fetched: "2023-09-20T07:02:34Z", + columns: { + model: "USB Reciever", + vendor: "Logitech", + }, + }, + { + host_id: 6, + host_name: "moo", + last_fetched: "2023-09-20T07:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "Display Audio", + vendor: "Apple Inc.", + version: "5.11", + vender_id: "05ac", + usb_address: "6", + }, + }, + { + host_id: 9, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "USB Reciever", + vendor: "Logitech", + wOAnow: "that's a weird column!", + }, + }, + { + host_id: 9, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 99, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 999, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + { + host_id: 9990, + host_name: "moo moo", + last_fetched: "2023-09-28T02:02:34Z", + columns: { + model: "LG Monitor Controls", + vendor: "LG Electronics Inc.", + model_id: "9a39", + }, + }, + ], +}; + const globalQuery1 = { query: globalQueries.queries[0] }; const globalQuery2 = { query: globalQueries.queries[1] }; const globalQuery3 = { query: globalQueries.queries[2] }; @@ -611,6 +10595,7 @@ export default { count, hosts, labels, + queryReport, globalQueries, globalQuery1, globalQuery2, diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss index a7e8215dfa..ab3e740616 100644 --- a/frontend/styles/var/mixins.scss +++ b/frontend/styles/var/mixins.scss @@ -9,11 +9,11 @@ $max-width: 2560px; @content; } } @else if ($size == ltdesktop) { - @media (max-width: $desktop-width - 1) { + @media (max-width: ($desktop-width - 1)) { @content; } } @else if ($size == desktop) { - @media (min-width: $medium-width + 1) { + @media (min-width: ($medium-width + 1)) { @content; } } @else if ($size == smalldesk) { @@ -108,3 +108,10 @@ $max-width: 2560px; list-style-position: inside; } } + +@mixin disabled { + opacity: 0.5; + filter: grayscale(0.5); + pointer-events: none; + cursor: default; +} diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts index 14530e240b..b911d225a6 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -96,12 +96,17 @@ export const MIN_OSQUERY_VERSION_OPTIONS = [ { label: "1.8.1 +", value: "1.8.1" }, ]; -export const QUERIES_PAGE_STEPS = { +export const LIVE_POLICY_STEPS = { 1: "EDITOR", 2: "TARGETS", 3: "RUN", }; +export const LIVE_QUERY_STEPS = { + 1: "TARGETS", + 2: "RUN", +}; + export const DEFAULT_QUERY: ISchedulableQuery = { description: "", name: "", @@ -109,6 +114,7 @@ export const DEFAULT_QUERY: ISchedulableQuery = { id: 0, interval: 0, observer_can_run: false, + discard_data: false, platform: "", min_osquery_version: "", automations_enabled: false, diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index 7ccd075ce4..e322061b07 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -82,7 +82,7 @@ export default { PERFORM_REQUIRED_PASSWORD_RESET: `/${API_VERSION}/fleet/perform_required_password_reset`, QUERIES: `/${API_VERSION}/fleet/queries`, RESET_PASSWORD: `/${API_VERSION}/fleet/reset_password`, - RUN_QUERY: `/${API_VERSION}/fleet/queries/run`, + LIVE_QUERY: `/${API_VERSION}/fleet/queries/run`, SCHEDULE_QUERY: `/${API_VERSION}/fleet/packs/schedule`, SCHEDULED_QUERIES: (packId: number): string => { return `/${API_VERSION}/fleet/packs/${packId}/scheduled`; diff --git a/frontend/utilities/generate_csv/index.ts b/frontend/utilities/generate_csv/index.ts index 8ee514ef93..501441a887 100644 --- a/frontend/utilities/generate_csv/index.ts +++ b/frontend/utilities/generate_csv/index.ts @@ -14,7 +14,7 @@ export const generateCSVFilename = (descriptor: string) => { return `${descriptor} (${format(new Date(), "MM-dd-yy hh-mm-ss")}).csv`; }; -// Query results and query errors +// Live query results, live query errors, and query report export const generateCSVQueryResults = ( rows: Row[], filename: string, @@ -35,7 +35,7 @@ export const generateCSVQueryResults = ( ); }; -// Policy results only +// Live policy results only export const generateCSVPolicyResults = ( rows: { host: string; status: string }[], filename: string @@ -45,7 +45,7 @@ export const generateCSVPolicyResults = ( }); }; -// Policy errors only +// Live policy errors only export const generateCSVPolicyErrors = ( rows: ICampaignError[], filename: string diff --git a/frontend/utilities/helpers.ts b/frontend/utilities/helpers.ts index 641b5cf734..89b291524b 100644 --- a/frontend/utilities/helpers.ts +++ b/frontend/utilities/helpers.ts @@ -31,7 +31,7 @@ import { } from "interfaces/scheduled_query"; import { ISelectTargetsEntity, - ISelectedTargets, + ISelectedTargetsForApi, IPackTargets, } from "interfaces/target"; import { ITeam, ITeamSummary } from "interfaces/team"; @@ -258,7 +258,7 @@ const formatLabelResponse = (response: any): ILabel[] => { export const formatSelectedTargetsForApi = ( selectedTargets: ISelectTargetsEntity[] -): ISelectedTargets => { +): ISelectedTargetsForApi => { const targets = selectedTargets || []; // TODO: can flatMap be removed? const hostIds = flatMap(targets, filterTarget("hosts")); @@ -910,6 +910,12 @@ export const getSoftwareBundleTooltipMarkup = (bundle: string) => { `; }; +export const TAGGED_TEMPLATES = { + queryByHostRoute: (hostId: number | undefined | null) => { + return `${hostId ? `?host_ids=${hostId}` : ""}`; + }, +}; + export default { addGravatarUrlToResource, formatConfigDataForServer, @@ -945,4 +951,5 @@ export default { syntaxHighlight, normalizeEmptyValues, wrapFleetHelper, + TAGGED_TEMPLATES, }; diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 1a49bfb106..cbdeeba136 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -464,6 +464,7 @@ var hostRefs = []string{ "host_disk_encryption_keys", "host_software_installed_paths", "host_script_results", + "query_results", } // NOTE: The following tables are explicity excluded from hostRefs list and accordingly are not diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index ab05b59f68..1b053e8890 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -5745,6 +5745,11 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) { Query: "select * from time", }) require.NoError(t, err) + + // update policy_results + _, err = ds.writer(context.Background()).Exec(`INSERT INTO query_results (host_id, query_id, last_fetched, data) VALUES (?, ?, ?, ?)`, host.ID, policy.ID, time.Now(), `{"foo": "bar"}`) + require.NoError(t, err) + require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{policy.ID: ptr.Bool(true)}, time.Now(), false)) // Update host_mdm. err = ds.SetOrUpdateMDMData(context.Background(), host.ID, false, true, "foo.mdm.example.com", false, "") diff --git a/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting.go b/server/datastore/mysql/migrations/tables/20231004144339_MoveDiskEncryptionSetting.go similarity index 79% rename from server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting.go rename to server/datastore/mysql/migrations/tables/20231004144339_MoveDiskEncryptionSetting.go index 793432c209..656e68da6f 100644 --- a/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting.go +++ b/server/datastore/mysql/migrations/tables/20231004144339_MoveDiskEncryptionSetting.go @@ -6,10 +6,10 @@ import ( ) func init() { - MigrationClient.AddMigration(Up_20230918221115, Down_20230918221115) + MigrationClient.AddMigration(Up_20231004144339, Down_20231004144339) } -func Up_20230918221115(tx *sql.Tx) error { +func Up_20231004144339(tx *sql.Tx) error { stmt := ` UPDATE teams SET @@ -27,6 +27,6 @@ WHERE return nil } -func Down_20230918221115(tx *sql.Tx) error { +func Down_20231004144339(tx *sql.Tx) error { return nil } diff --git a/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting_test.go b/server/datastore/mysql/migrations/tables/20231004144339_MoveDiskEncryptionSetting_test.go similarity index 98% rename from server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting_test.go rename to server/datastore/mysql/migrations/tables/20231004144339_MoveDiskEncryptionSetting_test.go index 90538968f8..78e7600537 100644 --- a/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting_test.go +++ b/server/datastore/mysql/migrations/tables/20231004144339_MoveDiskEncryptionSetting_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestUp_20230918221115(t *testing.T) { +func TestUp_20231004144339(t *testing.T) { db := applyUpToPrev(t) dataStmts := ` diff --git a/server/datastore/mysql/migrations/tables/20230927111208_AddIndexToHostScriptResults.go b/server/datastore/mysql/migrations/tables/20231009094541_AddIndexToHostScriptResults.go similarity index 69% rename from server/datastore/mysql/migrations/tables/20230927111208_AddIndexToHostScriptResults.go rename to server/datastore/mysql/migrations/tables/20231009094541_AddIndexToHostScriptResults.go index 2ed3458244..4317ef816b 100644 --- a/server/datastore/mysql/migrations/tables/20230927111208_AddIndexToHostScriptResults.go +++ b/server/datastore/mysql/migrations/tables/20231009094541_AddIndexToHostScriptResults.go @@ -6,10 +6,10 @@ import ( ) func init() { - MigrationClient.AddMigration(Up_20230927111208, Down_20230927111208) + MigrationClient.AddMigration(Up_20231009094541, Down_20231009094541) } -func Up_20230927111208(tx *sql.Tx) error { +func Up_20231009094541(tx *sql.Tx) error { sql := ` ALTER TABLE host_script_results @@ -23,6 +23,6 @@ ADD INDEX return nil } -func Down_20230927111208(tx *sql.Tx) error { +func Down_20231009094541(tx *sql.Tx) error { return nil } diff --git a/server/datastore/mysql/migrations/tables/20231009094540_AddIndexesToScriptContents.go b/server/datastore/mysql/migrations/tables/20231009094542_AddIndexesToScriptContents.go similarity index 53% rename from server/datastore/mysql/migrations/tables/20231009094540_AddIndexesToScriptContents.go rename to server/datastore/mysql/migrations/tables/20231009094542_AddIndexesToScriptContents.go index 5523cab77c..a502be16aa 100644 --- a/server/datastore/mysql/migrations/tables/20231009094540_AddIndexesToScriptContents.go +++ b/server/datastore/mysql/migrations/tables/20231009094542_AddIndexesToScriptContents.go @@ -5,14 +5,14 @@ import ( ) func init() { - MigrationClient.AddMigration(Up_20231009094540, Down_20231009094540) + MigrationClient.AddMigration(Up_20231009094542, Down_20231009094542) } -func Up_20231009094540(tx *sql.Tx) error { +func Up_20231009094542(tx *sql.Tx) error { _, err := tx.Exec(`ALTER TABLE scripts ADD UNIQUE KEY idx_scripts_team_name (team_id, name)`) return err } -func Down_20231009094540(tx *sql.Tx) error { +func Down_20231009094542(tx *sql.Tx) error { return nil } diff --git a/server/datastore/mysql/migrations/tables/20231009094540_AddIndexesToScriptContents_test.go b/server/datastore/mysql/migrations/tables/20231009094542_AddIndexesToScriptContents_test.go similarity index 87% rename from server/datastore/mysql/migrations/tables/20231009094540_AddIndexesToScriptContents_test.go rename to server/datastore/mysql/migrations/tables/20231009094542_AddIndexesToScriptContents_test.go index ca37607a72..6e4de0d128 100644 --- a/server/datastore/mysql/migrations/tables/20231009094540_AddIndexesToScriptContents_test.go +++ b/server/datastore/mysql/migrations/tables/20231009094542_AddIndexesToScriptContents_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestUp_20231009094540(t *testing.T) { +func TestUp_20231009094542(t *testing.T) { db := applyUpToPrev(t) idxExists := indexExists(db, "scripts", "idx_scripts_team_name") diff --git a/server/datastore/mysql/migrations/tables/20231009094543_AddDiscardToQueries.go b/server/datastore/mysql/migrations/tables/20231009094543_AddDiscardToQueries.go new file mode 100644 index 0000000000..12a6ee011c --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20231009094543_AddDiscardToQueries.go @@ -0,0 +1,18 @@ +package tables + +import ( + "database/sql" +) + +func init() { + MigrationClient.AddMigration(Up_20231009094543, Down_20231009094543) +} + +func Up_20231009094543(tx *sql.Tx) error { + _, err := tx.Exec(`ALTER TABLE queries ADD COLUMN discard_data TINYINT(1) NOT NULL DEFAULT TRUE;`) + return err +} + +func Down_20231009094543(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20231009094543_AddDiscardToQueries_test.go b/server/datastore/mysql/migrations/tables/20231009094543_AddDiscardToQueries_test.go new file mode 100644 index 0000000000..dc848ea2e0 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20231009094543_AddDiscardToQueries_test.go @@ -0,0 +1,60 @@ +package tables + +import ( + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/stretchr/testify/require" +) + +func TestUp_20231009094543(t *testing.T) { + db := applyUpToPrev(t) + applyNext(t, db) + + // + // Check data, insert new entries, e.g. to verify migration is safe. + // + insertStmt := `INSERT INTO queries ( + name, description, query, discard_data + ) VALUES (?, ?, ?, ?)` + + res, err := db.Exec(insertStmt, "test", "test description", "SELECT 1 from hosts", false) + require.NoError(t, err) + id, _ := res.LastInsertId() + require.NotNil(t, id) + require.Equal(t, int64(1), id) + + var query []fleet.Query + err = db.Select(&query, `SELECT + id, + name, + description, + query, + discard_data + FROM queries WHERE id = ?`, id) + require.NoError(t, err) + require.False(t, query[0].DiscardData) + + // Insert without discard_data, verify that default is correct + + insertStmt = `INSERT INTO queries ( + name, description, query + ) VALUES (?, ?, ?)` + + res, err = db.Exec(insertStmt, "test 2", "test description 2", "SELECT 1 from hosts") + require.NoError(t, err) + id, _ = res.LastInsertId() + require.NotNil(t, id) + require.Equal(t, int64(2), id) + + var queryNoDiscard []fleet.Query + err = db.Select(&queryNoDiscard, `SELECT + id, + name, + description, + query, + discard_data + FROM queries WHERE id = ?`, id) + require.NoError(t, err) + require.True(t, queryNoDiscard[0].DiscardData) +} diff --git a/server/datastore/mysql/migrations/tables/20231009094544_CreateTableQueryReports.go b/server/datastore/mysql/migrations/tables/20231009094544_CreateTableQueryReports.go new file mode 100644 index 0000000000..b20feb6e03 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20231009094544_CreateTableQueryReports.go @@ -0,0 +1,34 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20231009094544, Down_20231009094544) +} + +func Up_20231009094544(tx *sql.Tx) error { + _, err := tx.Exec(` + CREATE TABLE query_results ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + query_id INT(10) UNSIGNED NOT NULL, + host_id INT(10) UNSIGNED NOT NULL, + osquery_version VARCHAR(50), + error TEXT COLLATE utf8mb4_unicode_ci DEFAULT NULL, + last_fetched TIMESTAMP NOT NULL, + data JSON, + FOREIGN KEY (query_id) REFERENCES queries(id) ON DELETE CASCADE + ); + `) + if err != nil { + return fmt.Errorf("failed to create table query_results: %w", err) + } + + return nil +} + +func Down_20231009094544(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20231009094544_CreateTableQueryReports_test.go b/server/datastore/mysql/migrations/tables/20231009094544_CreateTableQueryReports_test.go new file mode 100644 index 0000000000..028af521a8 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20231009094544_CreateTableQueryReports_test.go @@ -0,0 +1,91 @@ +package tables + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestUp_20231009094544(t *testing.T) { + db := applyUpToPrev(t) + + // Apply current migration. + applyNext(t, db) + + // Insert a record into query_results + insertStmt := `INSERT INTO query_results ( + query_id, host_id, osquery_version, error, last_fetched, data + ) VALUES (?, ?, ?, ?, ?, ?)` + + queryID := insertQuery(t, db) + hostID := insertHost(t, db) + osqueryVersion := "5.9.1" + lastFetched := time.Now().UTC() + + // Example JSON data for data field + osqueryData := map[string]string{ + "model": "USB Keyboard", + "vendor": "Apple Inc.", + } + jsonData, err := json.Marshal(osqueryData) + require.NoError(t, err) + + res, err := db.Exec(insertStmt, queryID, hostID, osqueryVersion, "", lastFetched, jsonData) + require.NoError(t, err) + + id, _ := res.LastInsertId() + + // Insert a sample error result containing a NULL data field + errorMessage := "Some error message" + _, err = db.Exec(insertStmt, queryID, hostID, osqueryVersion, errorMessage, lastFetched, nil) + require.NoError(t, err) + + type QueryResult struct { + ID uint `db:"id"` + QueryID uint `db:"query_id"` + HostID uint `db:"host_id"` + OsqueryVersion string `db:"osquery_version"` + Error string `db:"error"` + LastFetched time.Time `db:"last_fetched"` + OsqueryResultData *json.RawMessage `db:"data"` + } + + // Load the 1st result + var queryReport []QueryResult + selectStmt := ` + SELECT id, query_id, host_id, osquery_version, error, last_fetched, data + FROM query_results + WHERE query_id = ? AND host_id = ? + ORDER BY id ASC + ` + err = db.Select(&queryReport, selectStmt, queryID, hostID) + require.NoError(t, err) + + require.Equal(t, queryID, queryReport[0].QueryID) + require.Equal(t, hostID, queryReport[0].HostID) + require.Equal(t, osqueryVersion, queryReport[0].OsqueryVersion) + require.Empty(t, queryReport[0].Error) + require.True(t, lastFetched.Sub(queryReport[0].LastFetched) < time.Second) + require.JSONEq(t, string(jsonData), string(*queryReport[0].OsqueryResultData)) + + // Error results should be loaded as well + require.Equal(t, queryID, queryReport[1].QueryID) + require.Equal(t, hostID, queryReport[1].HostID) + require.Equal(t, osqueryVersion, queryReport[1].OsqueryVersion) + require.Equal(t, errorMessage, queryReport[1].Error) + require.True(t, lastFetched.Sub(queryReport[1].LastFetched) < time.Second) // allow a 1 sec difference to account for time to run the query + require.Empty(t, queryReport[1].OsqueryResultData) + + // Delete the query we just created to test the ON DELETE CASCADE + deleteQueryStmt := `DELETE FROM queries WHERE id = ?` + _, err = db.Exec(deleteQueryStmt, queryID) + require.NoError(t, err) + + // Verify that both query_result records were deleted + var count int + err = db.Get(&count, "SELECT COUNT(*) FROM query_results WHERE id = ?", id) + require.NoError(t, err) + require.Equal(t, 0, count) +} diff --git a/server/datastore/mysql/migrations/tables/migration_test.go b/server/datastore/mysql/migrations/tables/migration_test.go index 8de460812e..402a6d60f0 100644 --- a/server/datastore/mysql/migrations/tables/migration_test.go +++ b/server/datastore/mysql/migrations/tables/migration_test.go @@ -89,3 +89,63 @@ func applyNext(t *testing.T, db *sqlx.DB) { err := MigrationClient.UpByOne(db.DB, gooseNoDir) require.NoError(t, err) } + +func insertQuery(t *testing.T, db *sqlx.DB) uint { + // Insert a record into queries table + insertQueryStmt := ` + INSERT INTO queries ( + name, description, query, observer_can_run, platform, logging_type + ) VALUES (?, ?, ?, ?, ?, ?) + ` + + queryName := "Test Query" + queryDescription := "A test query for the test suite" + queryValue := "SELECT * FROM apps;" + observerCanRun := 0 + platform := "mac" // Just a placeholder, adjust as needed + loggingType := "snapshot" + + res, err := db.Exec(insertQueryStmt, queryName, queryDescription, queryValue, observerCanRun, platform, loggingType) + require.NoError(t, err) + + id, err := res.LastInsertId() + require.NoError(t, err) + + return uint(id) +} + +func insertHost(t *testing.T, db *sqlx.DB) uint { + // Insert a minimal record into hosts table + insertHostStmt := ` + INSERT INTO hosts ( + hostname, uuid, platform, osquery_version, os_version, build, platform_like, code_name, + cpu_type, cpu_subtype, cpu_brand, hardware_vendor, hardware_model, hardware_version, + hardware_serial, computer_name + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ` + + hostName := "Dummy Hostname" + hostUUID := "12345678-1234-1234-1234-123456789012" + hostPlatform := "windows" + osqueryVer := "5.9.1" + osVersion := "Windows 10" + buildVersion := "10.0.19042.1234" + platformLike := "windows" + codeName := "20H2" + cpuType := "x86_64" + cpuSubtype := "x86_64" + cpuBrand := "Intel" + hwVendor := "Dell Inc." + hwModel := "OptiPlex 7090" + hwVersion := "1.0" + hwSerial := "ABCDEFGHIJ" + computerName := "DESKTOP-TEST" + + res, err := db.Exec(insertHostStmt, hostName, hostUUID, hostPlatform, osqueryVer, osVersion, buildVersion, platformLike, codeName, cpuType, cpuSubtype, cpuBrand, hwVendor, hwModel, hwVersion, hwSerial, computerName) + require.NoError(t, err) + + id, err := res.LastInsertId() + require.NoError(t, err) + + return uint(id) +} diff --git a/server/datastore/mysql/packs_test.go b/server/datastore/mysql/packs_test.go index 09a28361c5..21eff1c891 100644 --- a/server/datastore/mysql/packs_test.go +++ b/server/datastore/mysql/packs_test.go @@ -145,7 +145,7 @@ func setupPackSpecsTest(t *testing.T, ds fleet.Datastore) []*fleet.PackSpec { {Name: "bar", Description: "do some bars", Query: "select baz from bar"}, } // Zach creates some queries - err := ds.ApplyQueries(context.Background(), zwass.ID, queries) + err := ds.ApplyQueries(context.Background(), zwass.ID, queries, nil) require.Nil(t, err) labels := []*fleet.LabelSpec{ diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index 1ea22ef2bf..2d5df3d51d 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -10,7 +10,7 @@ import ( "github.com/jmoiron/sqlx" ) -func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries []*fleet.Query) (err error) { +func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]bool) (err error) { tx, err := ds.writer(ctx).BeginTxx(ctx, nil) if err != nil { return ctxerr.Wrap(ctx, err, "begin ApplyQueries transaction") @@ -29,7 +29,7 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] } }() - sql := ` + insertSql := ` INSERT INTO queries ( name, description, @@ -43,8 +43,9 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] min_osquery_version, schedule_interval, automations_enabled, - logging_type - ) VALUES ( ?, ?, ?, ?, true, ?, ?, ?, ?, ?, ?, ?, ? ) + logging_type, + discard_data + ) VALUES ( ?, ?, ?, ?, true, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ON DUPLICATE KEY UPDATE name = VALUES(name), description = VALUES(description), @@ -58,14 +59,26 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] min_osquery_version = VALUES(min_osquery_version), schedule_interval = VALUES(schedule_interval), automations_enabled = VALUES(automations_enabled), - logging_type = VALUES(logging_type) + logging_type = VALUES(logging_type), + discard_data = VALUES(discard_data) ` - stmt, err := tx.PrepareContext(ctx, sql) + stmt, err := tx.PrepareContext(ctx, insertSql) if err != nil { return ctxerr.Wrap(ctx, err, "prepare ApplyQueries insert") } defer stmt.Close() + var resultsStmt *sql.Stmt + if len(queriesToDiscardResults) > 0 { + resultsSql := `DELETE FROM query_results WHERE query_id = ?` + resultsStmt, err = tx.PrepareContext(ctx, resultsSql) + if err != nil { + return ctxerr.Wrap(ctx, err, "prepare ApplyQueries delete query results") + } + defer resultsStmt.Close() + + } + for _, q := range queries { if err := q.Verify(); err != nil { return ctxerr.Wrap(ctx, err) @@ -84,12 +97,23 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] q.Interval, q.AutomationsEnabled, q.Logging, + q.DiscardData, ) if err != nil { return ctxerr.Wrap(ctx, err, "exec ApplyQueries insert") } } + for id := range queriesToDiscardResults { + _, err := resultsStmt.ExecContext( + ctx, + id, + ) + if err != nil { + return ctxerr.Wrap(ctx, err, "exec ApplyQueries delete query results") + } + } + err = tx.Commit() return ctxerr.Wrap(ctx, err, "commit ApplyQueries transaction") } @@ -115,6 +139,7 @@ func (ds *Datastore) QueryByName( min_osquery_version, automations_enabled, logging_type, + discard_data, created_at, updated_at FROM queries @@ -164,8 +189,9 @@ func (ds *Datastore) NewQuery( min_osquery_version, schedule_interval, automations_enabled, - logging_type - ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) + logging_type, + discard_data + ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ` result, err := ds.writer(ctx).ExecContext( ctx, @@ -183,6 +209,7 @@ func (ds *Datastore) NewQuery( query.Interval, query.AutomationsEnabled, query.Logging, + query.DiscardData, ) if err != nil && isDuplicate(err) { @@ -198,8 +225,26 @@ func (ds *Datastore) NewQuery( } // SaveQuery saves changes to a Query. -func (ds *Datastore) SaveQuery(ctx context.Context, q *fleet.Query) error { - sql := ` +func (ds *Datastore) SaveQuery(ctx context.Context, q *fleet.Query, shouldDiscardResults bool) (err error) { + tx, err := ds.writer(ctx).BeginTxx(ctx, nil) + if err != nil { + return ctxerr.Wrap(ctx, err, "begin SaveQuery transaction") + } + + defer func() { + if err != nil { + rbErr := tx.Rollback() + // It seems possible that there might be a case in + // which the error we are dealing with here was thrown + // by the call to tx.Commit(), and the docs suggest + // this call would then result in sql.ErrTxDone. + if rbErr != nil && rbErr != sql.ErrTxDone { + panic(fmt.Sprintf("got err '%s' rolling back after err '%s'", rbErr, err)) + } + } + }() + + updateSql := ` UPDATE queries SET name = ?, description = ?, @@ -213,12 +258,30 @@ func (ds *Datastore) SaveQuery(ctx context.Context, q *fleet.Query) error { min_osquery_version = ?, schedule_interval = ?, automations_enabled = ?, - logging_type = ? + logging_type = ?, + discard_data = ? WHERE id = ? ` - result, err := ds.writer(ctx).ExecContext( + + stmt, err := tx.PrepareContext(ctx, updateSql) + if err != nil { + return ctxerr.Wrap(ctx, err, "prepare SaveQuery update") + } + defer stmt.Close() + + var resultsStmt *sql.Stmt + if shouldDiscardResults { + resultsSql := `DELETE FROM query_results WHERE query_id = ?` + resultsStmt, err = tx.PrepareContext(ctx, resultsSql) + if err != nil { + return ctxerr.Wrap(ctx, err, "prepare SaveQuery delete query results") + } + defer resultsStmt.Close() + + } + + _, err = stmt.ExecContext( ctx, - sql, q.Name, q.Description, q.Query, @@ -232,19 +295,25 @@ func (ds *Datastore) SaveQuery(ctx context.Context, q *fleet.Query) error { q.Interval, q.AutomationsEnabled, q.Logging, - q.ID) + q.DiscardData, + q.ID, + ) if err != nil { - return ctxerr.Wrap(ctx, err, "updating query") - } - rows, err := result.RowsAffected() - if err != nil { - return ctxerr.Wrap(ctx, err, "rows affected updating query") - } - if rows == 0 { - return ctxerr.Wrap(ctx, notFound("Query").WithID(q.ID)) + return ctxerr.Wrap(ctx, err, "exec SaveQuery update") } - return nil + if resultsStmt != nil { + _, err := resultsStmt.ExecContext( + ctx, + q.ID, + ) + if err != nil { + return ctxerr.Wrap(ctx, err, "exec SaveQuery delete query results") + } + } + + err = tx.Commit() + return ctxerr.Wrap(ctx, err, "commit SaveQuery transaction") } func (ds *Datastore) DeleteQuery( @@ -299,8 +368,10 @@ func (ds *Datastore) Query(ctx context.Context, id uint) (*fleet.Query, error) { q.min_osquery_version, q.automations_enabled, q.logging_type, + q.discard_data, q.created_at, q.updated_at, + q.discard_data, COALESCE(NULLIF(u.name, ''), u.email, '') AS author_name, COALESCE(u.email, '') AS author_email, JSON_EXTRACT(json_value, '$.user_time_p50') as user_time_p50, @@ -348,8 +419,10 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions q.min_osquery_version, q.automations_enabled, q.logging_type, + q.discard_data, q.created_at, q.updated_at, + q.discard_data, COALESCE(u.name, '') AS author_name, COALESCE(u.email, '') AS author_email, JSON_EXTRACT(json_value, '$.user_time_p50') as user_time_p50, @@ -462,8 +535,8 @@ func (ds *Datastore) ObserverCanRunQuery(ctx context.Context, queryID uint) (boo return observerCanRun, nil } -func (ds *Datastore) ListScheduledQueriesForAgents(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { - sql := ` +func (ds *Datastore) ListScheduledQueriesForAgents(ctx context.Context, teamID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) { + sqlStmt := ` SELECT q.name, q.query, @@ -472,24 +545,36 @@ func (ds *Datastore) ListScheduledQueriesForAgents(ctx context.Context, teamID * q.platform, q.min_osquery_version, q.automations_enabled, - q.logging_type + q.logging_type, + q.discard_data FROM queries q WHERE q.saved = true - AND (q.schedule_interval > 0 AND q.automations_enabled = 1) + AND (q.schedule_interval > 0 AND %s AND (q.automations_enabled OR (NOT q.discard_data AND NOT ?))) ` args := []interface{}{} + teamSQL := " team_id IS NULL" if teamID != nil { args = append(args, *teamID) - sql += " AND team_id = ?" - } else { - sql += " AND team_id IS NULL" + teamSQL = " team_id = ?" } + sqlStmt = fmt.Sprintf(sqlStmt, teamSQL) + args = append(args, queryReportsDisabled) results := []*fleet.Query{} - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, args...); err != nil { + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sqlStmt, args...); err != nil { return nil, ctxerr.Wrap(ctx, err, "list scheduled queries for agents") } return results, nil } + +func (ds *Datastore) CleanupGlobalDiscardQueryResults(ctx context.Context) error { + deleteStmt := "DELETE FROM query_results" + _, err := ds.writer(ctx).ExecContext(ctx, deleteStmt) + if err != nil { + return ctxerr.Wrapf(ctx, err, "delete all from query_result") + } + + return nil +} diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index 29bd76c113..78a0e4eba0 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -59,16 +59,18 @@ func testQueriesApply(t *testing.T, ds *Datastore) { MinOsqueryVersion: "5.2.1", AutomationsEnabled: true, Logging: "differential", + DiscardData: true, }, { Name: "bar", Description: "do some bars", Query: "select baz from bar", + DiscardData: true, }, } // Zach creates some queries - err := ds.ApplyQueries(context.Background(), zwass.ID, expectedQueries) + err := ds.ApplyQueries(context.Background(), zwass.ID, expectedQueries, nil) require.NoError(t, err) queries, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) @@ -88,7 +90,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { // Victor modifies a query (but also pushes the same version of the // first query) expectedQueries[1].Query = "not really a valid query ;)" - err = ds.ApplyQueries(context.Background(), groob.ID, expectedQueries) + err = ds.ApplyQueries(context.Background(), groob.ID, expectedQueries, nil) require.NoError(t, err) queries, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) @@ -111,9 +113,10 @@ func testQueriesApply(t *testing.T, ds *Datastore) { Name: "trouble", Description: "Look out!", Query: "select * from time", + DiscardData: true, }, ) - err = ds.ApplyQueries(context.Background(), zwass.ID, []*fleet.Query{expectedQueries[2]}) + err = ds.ApplyQueries(context.Background(), zwass.ID, []*fleet.Query{expectedQueries[2]}, nil) require.NoError(t, err) queries, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) @@ -150,7 +153,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { Logging: "differential", }, } - err = ds.ApplyQueries(context.Background(), zwass.ID, invalidQueries) + err = ds.ApplyQueries(context.Background(), zwass.ID, invalidQueries, nil) require.ErrorIs(t, err, fleet.ErrQueryInvalidPlatform) } @@ -276,8 +279,9 @@ func testQueriesSave(t *testing.T, ds *Datastore) { query.MinOsqueryVersion = "5.2.1" query.AutomationsEnabled = true query.Logging = "differential" + query.DiscardData = true - err = ds.SaveQuery(context.Background(), query) + err = ds.SaveQuery(context.Background(), query, true) require.NoError(t, err) actual, err := ds.Query(context.Background(), query.ID) @@ -296,10 +300,11 @@ func testQueriesList(t *testing.T, ds *Datastore) { for i := 0; i < 10; i++ { _, err := ds.NewQuery(context.Background(), &fleet.Query{ - Name: fmt.Sprintf("name%02d", i), - Query: fmt.Sprintf("query%02d", i), - Saved: true, - AuthorID: &user.ID, + Name: fmt.Sprintf("name%02d", i), + Query: fmt.Sprintf("query%02d", i), + Saved: true, + AuthorID: &user.ID, + DiscardData: true, }) require.Nil(t, err) } @@ -319,6 +324,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { require.Equal(t, 10, len(results)) require.Equal(t, "Zach", results[0].AuthorName) require.Equal(t, "zwass@fleet.co", results[0].AuthorEmail) + require.True(t, results[0].DiscardData) idWithAgg := results[0].ID @@ -351,7 +357,7 @@ func testQueriesLoadPacksForQueries(t *testing.T, ds *Datastore) { {Name: "q1", Query: "select * from time"}, {Name: "q2", Query: "select * from osquery_info"}, } - err := ds.ApplyQueries(context.Background(), zwass.ID, queries) + err := ds.ApplyQueries(context.Background(), zwass.ID, queries, nil) require.NoError(t, err) specs := []*fleet.PackSpec{ @@ -702,32 +708,44 @@ func testListScheduledQueriesForAgents(t *testing.T, ds *Datastore) { if teamID != nil { teamIDStr = fmt.Sprintf("%d", *teamID) } - _, err := ds.NewQuery(context.Background(), &fleet.Query{ - Name: fmt.Sprintf("%s query1", teamIDStr), - Query: "select 1;", - Saved: true, - Interval: 0, - TeamID: teamID, - }) - require.NoError(t, err) + + // Non saved queries should not be returned here. _, err = ds.NewQuery(context.Background(), &fleet.Query{ - Name: fmt.Sprintf("%s query2", teamIDStr), + Name: fmt.Sprintf("%s query1", teamIDStr), Query: "select 1;", Saved: false, Interval: 10, AutomationsEnabled: false, TeamID: teamID, + DiscardData: true, }) require.NoError(t, err) - q3, err := ds.NewQuery(context.Background(), &fleet.Query{ + + // Interval=0, AutomationsEnabled=0, DiscardData=0 + _, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: fmt.Sprintf("%s query2", teamIDStr), + Query: "select 1;", + Saved: true, + Interval: 0, + TeamID: teamID, + AutomationsEnabled: false, + DiscardData: false, + }) + require.NoError(t, err) + + // Interval=0, AutomationsEnabled=0, DiscardData=1 + _, err = ds.NewQuery(context.Background(), &fleet.Query{ Name: fmt.Sprintf("%s query3", teamIDStr), Query: "select 1;", Saved: true, - Interval: 20, - AutomationsEnabled: true, + Interval: 0, + AutomationsEnabled: false, TeamID: teamID, + DiscardData: true, }) require.NoError(t, err) + + // Interval=0, AutomationsEnabled=1, DiscardData=0 _, err = ds.NewQuery(context.Background(), &fleet.Query{ Name: fmt.Sprintf("%s query4", teamIDStr), Query: "select 1;", @@ -735,11 +753,84 @@ func testListScheduledQueriesForAgents(t *testing.T, ds *Datastore) { Interval: 0, AutomationsEnabled: true, TeamID: teamID, + DiscardData: false, }) require.NoError(t, err) - result, err := ds.ListScheduledQueriesForAgents(ctx, teamID) + // Interval=0, AutomationsEnabled=1, DiscardData=1 + _, err = ds.NewQuery(context.Background(), &fleet.Query{ + Name: fmt.Sprintf("%s query5", teamIDStr), + Query: "select 1;", + Saved: true, + Interval: 0, + AutomationsEnabled: true, + TeamID: teamID, + DiscardData: true, + }) require.NoError(t, err) - test.QueryElementsMatch(t, result, []*fleet.Query{q3}, i) + + // Interval=1, AutomationsEnabled=0, DiscardData=0 + q6, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: fmt.Sprintf("%s query6", teamIDStr), + Query: "select 1;", + Saved: true, + Interval: 10, + AutomationsEnabled: false, + TeamID: teamID, + DiscardData: false, + }) + require.NoError(t, err) + + // Interval=1, AutomationsEnabled=0, DiscardData=1 + _, err = ds.NewQuery(context.Background(), &fleet.Query{ + Name: fmt.Sprintf("%s query7", teamIDStr), + Query: "select 1;", + Saved: true, + Interval: 10, + AutomationsEnabled: false, + TeamID: teamID, + DiscardData: true, + }) + require.NoError(t, err) + + // Interval=1, AutomationsEnabled=1, DiscardData=0 + q8, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: fmt.Sprintf("%s query8", teamIDStr), + Query: "select 1;", + Saved: true, + Interval: 10, + AutomationsEnabled: true, + TeamID: teamID, + DiscardData: false, + }) + require.NoError(t, err) + + // Interval=1, AutomationsEnabled=1, DiscardData=1 + q9, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: fmt.Sprintf("%s query9", teamIDStr), + Query: "select 1;", + Saved: true, + Interval: 10, + AutomationsEnabled: true, + TeamID: teamID, + DiscardData: true, + }) + require.NoError(t, err) + + queryReportsDisabled := false + result, err := ds.ListScheduledQueriesForAgents(ctx, teamID, queryReportsDisabled) + require.NoError(t, err) + sort.Slice(result, func(i, j int) bool { + return result[i].ID < result[j].ID + }) + test.QueryElementsMatch(t, result, []*fleet.Query{q6, q8, q9}, i) + + queryReportsDisabled = true + result, err = ds.ListScheduledQueriesForAgents(ctx, teamID, queryReportsDisabled) + require.NoError(t, err) + sort.Slice(result, func(i, j int) bool { + return result[i].ID < result[j].ID + }) + test.QueryElementsMatch(t, result, []*fleet.Query{q8, q9}, i) } } diff --git a/server/datastore/mysql/query_results.go b/server/datastore/mysql/query_results.go new file mode 100644 index 0000000000..59387786d2 --- /dev/null +++ b/server/datastore/mysql/query_results.go @@ -0,0 +1,126 @@ +package mysql + +import ( + "context" + "strings" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/jmoiron/sqlx" +) + +// OverwriteQueryResultRows overwrites the query result rows for a given query and host +// in a single transaction, ensuring that the number of rows for the given query +// does not exceed the maximum allowed +func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) (err error) { + if len(rows) == 0 { + return nil + } + + err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + // Since we assume all rows have the same queryID, take it from the first row + queryID := rows[0].QueryID + hostID := rows[0].HostID + + // Count how many rows are already in the database for the given queryID + var countExisting int + countStmt := ` + SELECT COUNT(*) FROM query_results WHERE query_id = ? + ` + err = sqlx.GetContext(ctx, tx, &countExisting, countStmt, queryID) + if err != nil { + return ctxerr.Wrap(ctx, err, "counting existing query results") + } + + if countExisting >= fleet.MaxQueryReportRows { + // do not delete any rows if we are already at the limit + return nil + } + + // Delete rows based on the specific queryID and hostID + deleteStmt := ` + DELETE FROM query_results WHERE host_id = ? AND query_id = ? + ` + result, err := tx.ExecContext(ctx, deleteStmt, hostID, queryID) + if err != nil { + return ctxerr.Wrap(ctx, err, "deleting query results for host") + } + + // Count how many rows we deleted + countDeleted, err := result.RowsAffected() + if err != nil { + return ctxerr.Wrap(ctx, err, "fetching deleted row count") + } + + // Calculate how many new rows can be added given the maximum limit + netRowsAfterDeletion := countExisting - int(countDeleted) + allowedNewRows := fleet.MaxQueryReportRows - netRowsAfterDeletion + if allowedNewRows == 0 { + return nil + } + + if len(rows) > allowedNewRows { + rows = rows[:allowedNewRows] + } + + // Insert the new rows + valueStrings := make([]string, 0, len(rows)) + valueArgs := make([]interface{}, 0, len(rows)*4) + for _, row := range rows { + valueStrings = append(valueStrings, "(?, ?, ?, ?)") + valueArgs = append(valueArgs, queryID, hostID, row.LastFetched, row.Data) + } + + //nolint:gosec // SQL query is constructed using constant strings + insertStmt := ` + INSERT INTO query_results (query_id, host_id, last_fetched, data) VALUES + ` + strings.Join(valueStrings, ",") + + _, err = tx.ExecContext(ctx, insertStmt, valueArgs...) + if err != nil { + return ctxerr.Wrap(ctx, err, "inserting new rows") + } + + return nil + }) + + return ctxerr.Wrap(ctx, err, "overwriting query result rows") +} + +// TODO(lucas): Any chance we can store hostname in the query_results table? +// (to avoid having to left join hosts). +func (ds *Datastore) QueryResultRows(ctx context.Context, queryID uint) ([]*fleet.ScheduledQueryResultRow, error) { + selectStmt := ` + SELECT qr.query_id, qr.host_id, h.hostname, qr.last_fetched, qr.data + FROM query_results qr + LEFT JOIN hosts h ON (qr.host_id=h.id) + WHERE query_id = ? + ` + results := []*fleet.ScheduledQueryResultRow{} + err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, selectStmt, queryID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "selecting query result rows") + } + + return results, nil +} + +func (ds *Datastore) ResultCountForQuery(ctx context.Context, queryID uint) (int, error) { + var count int + err := sqlx.GetContext(ctx, ds.reader(ctx), &count, `select count(*) from query_results where query_id = ?`, queryID) + if err != nil { + return 0, ctxerr.Wrap(ctx, err, "counting query results for query") + } + + return count, nil +} + +func (ds *Datastore) ResultCountForQueryAndHost(ctx context.Context, queryID, hostID uint) (int, error) { + var count int + err := sqlx.GetContext(ctx, ds.reader(ctx), &count, `select count(*) from query_results where query_id = ? AND host_id = ?`, queryID, hostID) + if err != nil { + return 0, ctxerr.Wrap(ctx, err, "counting query results for query and host") + } + + return count, nil +} diff --git a/server/datastore/mysql/query_results_test.go b/server/datastore/mysql/query_results_test.go new file mode 100644 index 0000000000..2e259ce924 --- /dev/null +++ b/server/datastore/mysql/query_results_test.go @@ -0,0 +1,402 @@ +package mysql + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "testing" + "time" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/test" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" +) + +func TestQueryResults(t *testing.T) { + ds := CreateMySQLDS(t) + + cases := []struct { + name string + fn func(t *testing.T, ds *Datastore) + }{ + {"Save", saveQueryResultRows}, + {"Get", getQueryResultRows}, + {"CountForQuery", testCountResultsForQuery}, + {"CountForQueryAndHost", testCountResultsForQueryAndHost}, + {"Overwrite", testOverwriteQueryResultRows}, + {"MaxRows", testQueryResultRowsDoNotExceedMaxRows}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + defer TruncateTables(t, ds) + c.fn(t, ds) + }) + } +} + +func saveQueryResultRows(t *testing.T, ds *Datastore) { + user := test.NewUser(t, ds, "Test User", "test@example.com", true) + query := test.NewQuery(t, ds, nil, "New Query", "SELECT 1", user.ID, true) + host := test.NewHost(t, ds, "hostname123", "192.168.1.100", "1234", "UI8XB1223", time.Now()) + + mockTime := time.Now().UTC().Truncate(time.Second) + + resultRows := []*fleet.ScheduledQueryResultRow{ + { + QueryID: query.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage( + `{"model": "USB Keyboard", "vendor": "Apple Inc."}`, + ), + }, + { + QueryID: query.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage( + `{"model": "USB Mouse", "vendor": "Logitech"}`, + ), + }, + } + + err := ds.SaveQueryResultRows(context.Background(), resultRows) + require.NoError(t, err) +} + +func getQueryResultRows(t *testing.T, ds *Datastore) { + user := test.NewUser(t, ds, "Test User", "test@example.com", true) + query := test.NewQuery(t, ds, nil, "New Query", "SELECT 1", user.ID, true) + host := test.NewHost(t, ds, "hostname123", "192.168.1.100", "1234", "UI8XB1223", time.Now()) + + mockTime := time.Now().UTC().Truncate(time.Second) + + // Insert 2 Result Rows for Query1 + resultRows := []*fleet.ScheduledQueryResultRow{ + { + QueryID: query.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage( + `{"model": "USB Keyboard", "vendor": "Apple Inc."}`, + ), + }, + { + QueryID: query.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage( + `{"model": "USB Mouse", "vendor": "Logitech"}`, + ), + }, + } + + err := ds.SaveQueryResultRows(context.Background(), resultRows) + require.NoError(t, err) + + // Insert Result Row for different Scheduled Query + query2 := test.NewQuery(t, ds, nil, "New Query 2", "SELECT 1", user.ID, true) + resultRow3 := []*fleet.ScheduledQueryResultRow{ + { + QueryID: query2.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage( + `{"model": "USB Hub","vendor": "Logitech"}`, + ), + }, + } + + err = ds.SaveQueryResultRows(context.Background(), resultRow3) + require.NoError(t, err) + + // Assert that Query1 returns 2 results + results, err := ds.QueryResultRowsForHost(context.Background(), resultRows[0].QueryID, resultRows[0].HostID) + require.NoError(t, err) + require.Len(t, results, 2) + require.Equal(t, resultRows[0].QueryID, results[0].QueryID) + require.Equal(t, resultRows[0].HostID, results[0].HostID) + require.Equal(t, resultRows[0].LastFetched.Unix(), results[0].LastFetched.Unix()) + require.JSONEq(t, string(resultRows[0].Data), string(results[0].Data)) + require.Equal(t, resultRows[1].QueryID, results[1].QueryID) + require.Equal(t, resultRows[1].HostID, results[1].HostID) + require.Equal(t, resultRows[1].LastFetched.Unix(), results[1].LastFetched.Unix()) + require.JSONEq(t, string(resultRows[1].Data), string(results[1].Data)) + + // Assert that Query2 returns 1 result + results, err = ds.QueryResultRowsForHost(context.Background(), resultRow3[0].QueryID, resultRow3[0].HostID) + require.NoError(t, err) + require.Len(t, results, 1) + require.Equal(t, resultRow3[0].QueryID, results[0].QueryID) + require.Equal(t, resultRow3[0].HostID, results[0].HostID) + require.Equal(t, resultRow3[0].LastFetched.Unix(), results[0].LastFetched.Unix()) + require.JSONEq(t, string(resultRow3[0].Data), string(results[0].Data)) + + // Assert that QueryResultRows returns empty slice when no results are found + results, err = ds.QueryResultRowsForHost(context.Background(), 999, 999) + require.NoError(t, err) + require.Len(t, results, 0) +} + +func testCountResultsForQuery(t *testing.T, ds *Datastore) { + user := test.NewUser(t, ds, "Test User", "test@example.com", true) + query1 := test.NewQuery(t, ds, nil, "New Query", "SELECT 1", user.ID, true) + query2 := test.NewQuery(t, ds, nil, "New Query 2", "SELECT 1", user.ID, true) + host := test.NewHost(t, ds, "hostname123", "192.168.1.100", "1234", "UI8XB1223", time.Now()) + + mockTime := time.Now().UTC().Truncate(time.Second) + + // Insert 1 Result Row for Query1 + resultRow := []*fleet.ScheduledQueryResultRow{ + { + QueryID: query1.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage(`{ + "model": "USB Keyboard", + "vendor": "Apple Inc." + }`), + }, + } + + err := ds.SaveQueryResultRows(context.Background(), resultRow) + require.NoError(t, err) + + // Insert 5 Result Rows for Query2 + resultRow2 := []*fleet.ScheduledQueryResultRow{ + { + QueryID: query2.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage(`{ + "model": "USB Mouse", + "vendor": "Apple Inc." + }`), + }, + } + for i := 0; i < 5; i++ { + err = ds.SaveQueryResultRows(context.Background(), resultRow2) + require.NoError(t, err) + } + + // Assert that ResultCountForQuery returns 1 + count, err := ds.ResultCountForQuery(context.Background(), query1.ID) + require.NoError(t, err) + require.Equal(t, 1, count) + + // Assert that ResultCountForQuery returns 5 + count, err = ds.ResultCountForQuery(context.Background(), query2.ID) + require.NoError(t, err) + require.Equal(t, 5, count) + + // Returns empty result when no results are found + count, err = ds.ResultCountForQuery(context.Background(), 999) + require.NoError(t, err) + require.Equal(t, 0, count) +} + +func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) { + user := test.NewUser(t, ds, "Test User", "test@example.com", true) + query1 := test.NewQuery(t, ds, nil, "New Query", "SELECT 1", user.ID, true) + query2 := test.NewQuery(t, ds, nil, "New Query 2", "SELECT 1", user.ID, true) + host := test.NewHost(t, ds, "host1", "192.168.1.100", "1234", "UI8XB1223", time.Now()) + host2 := test.NewHost(t, ds, "host2", "192.168.1.101", "4567", "UI8XB1224", time.Now()) + + mockTime := time.Now().UTC().Truncate(time.Second) + + resultRows := []*fleet.ScheduledQueryResultRow{ + { + QueryID: query1.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage(`{ + "model": "USB Keyboard", + "vendor": "Apple Inc." + }`), + }, + { + QueryID: query1.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage(`{ + "model": "USB Mouse", + "vendor": "Logitech" + }`), + }, + { + QueryID: query1.ID, + HostID: host2.ID, + LastFetched: mockTime, + Data: json.RawMessage(`{ + "model": "USB Mouse", + "vendor": "Logitech" + }`), + }, + { + QueryID: query2.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage(`{ + "foo": "bar" + }`), + }, + } + + err := ds.SaveQueryResultRows(context.Background(), resultRows) + require.NoError(t, err) + + // Assert that Query1 returns 2 + count, err := ds.ResultCountForQueryAndHost(context.Background(), query1.ID, host.ID) + require.NoError(t, err) + require.Equal(t, 2, count) + + // Assert that ResultCountForQuery returns 1 + count, err = ds.ResultCountForQueryAndHost(context.Background(), query2.ID, host.ID) + require.NoError(t, err) + require.Equal(t, 1, count) + + // Returns empty result when no results are found + count, err = ds.ResultCountForQueryAndHost(context.Background(), 999, host.ID) + require.NoError(t, err) + require.Equal(t, 0, count) +} + +func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) { + user := test.NewUser(t, ds, "Test User", "test@example.com", true) + query := test.NewQuery(t, ds, nil, "Overwrite Test Query", "SELECT 1", user.ID, true) + host := test.NewHost(t, ds, "hostname1234", "192.168.1.101", "12345", "UI8XB1224", time.Now()) + + mockTime := time.Now().UTC().Truncate(time.Second) + + // Insert initial Result Rows + initialRows := []*fleet.ScheduledQueryResultRow{ + { + QueryID: query.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage( + `{"model": "USB Keyboard", "vendor": "Apple Inc."}`, + ), + }, + } + + err := ds.SaveQueryResultRows(context.Background(), initialRows) + require.NoError(t, err) + + // Overwrite Result Rows with new data + newMockTime := mockTime.Add(2 * time.Minute) + overwriteRows := []*fleet.ScheduledQueryResultRow{ + { + QueryID: query.ID, + HostID: host.ID, + LastFetched: newMockTime, + Data: json.RawMessage( + `{"model": "USB Mouse", "vendor": "Logitech"}`, + ), + }, + } + + err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows) + require.NoError(t, err) + + // Assert that we get the overwritten data (1 result with USB Mouse data) + results, err := ds.QueryResultRowsForHost(context.Background(), overwriteRows[0].QueryID, overwriteRows[0].HostID) + require.NoError(t, err) + require.Len(t, results, 1) + require.Equal(t, overwriteRows[0].QueryID, results[0].QueryID) + require.Equal(t, overwriteRows[0].HostID, results[0].HostID) + require.Equal(t, overwriteRows[0].LastFetched.Unix(), results[0].LastFetched.Unix()) + require.JSONEq(t, string(overwriteRows[0].Data), string(results[0].Data)) +} + +func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) { + user := test.NewUser(t, ds, "Test User", "test@example.com", true) + query := test.NewQuery(t, ds, nil, "Overwrite Test Query", "SELECT 1", user.ID, true) + host := test.NewHost(t, ds, "hostname1", "192.168.1.101", "12345", "UI8XB1224", time.Now()) + + mockTime := time.Now().UTC().Truncate(time.Second) + + // Generate more than max rows + rows := fleet.MaxQueryReportRows + 50 + largeBatchRows := make([]*fleet.ScheduledQueryResultRow, rows) + for i := 0; i < rows; i++ { + largeBatchRows[i] = &fleet.ScheduledQueryResultRow{ + QueryID: query.ID, + HostID: host.ID, + LastFetched: mockTime, + Data: json.RawMessage(`{"model": "Bulk Mouse", "vendor": "BulkTech"}`), + } + } + + err := ds.OverwriteQueryResultRows(context.Background(), largeBatchRows) + require.NoError(t, err) + + // Confirm only max rows are stored for the queryID + allResults, err := ds.QueryResultRowsForHost(context.Background(), query.ID, host.ID) + require.NoError(t, err) + require.Len(t, allResults, fleet.MaxQueryReportRows) + + // Confirm that new rows are not added when the max is reached + host2 := test.NewHost(t, ds, "hostname2", "192.168.1.102", "678910", "UI8XB1225", time.Now()) + newMockTime := mockTime.Add(2 * time.Minute) + overwriteRows := []*fleet.ScheduledQueryResultRow{ + { + QueryID: query.ID, + HostID: host2.ID, + LastFetched: newMockTime, + Data: json.RawMessage( + `{"model": "USB Mouse", "vendor": "Logitech"}`, + ), + }, + } + + err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows) + require.NoError(t, err) + + host2Results, err := ds.QueryResultRowsForHost(context.Background(), query.ID, host2.ID) + require.NoError(t, err) + require.Len(t, host2Results, 0) +} + +func (ds *Datastore) SaveQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error { + if len(rows) == 0 { + return nil // Nothing to insert + } + + valueStrings := make([]string, 0, len(rows)) + valueArgs := make([]interface{}, 0, len(rows)*4) + + for _, row := range rows { + valueStrings = append(valueStrings, "(?, ?, ?, ?)") + valueArgs = append(valueArgs, row.QueryID, row.HostID, row.LastFetched, row.Data) + } + + insertStmt := fmt.Sprintf(` + INSERT INTO query_results (query_id, host_id, last_fetched, data) + VALUES %s + `, strings.Join(valueStrings, ",")) + + _, err := ds.writer(ctx).ExecContext(ctx, insertStmt, valueArgs...) + if err != nil { + return err + } + + return nil +} + +func (ds *Datastore) QueryResultRowsForHost(ctx context.Context, queryID, hostID uint) ([]*fleet.ScheduledQueryResultRow, error) { + selectStmt := ` + SELECT query_id, host_id, last_fetched, data FROM query_results + WHERE query_id = ? AND host_id = ? + ` + results := []*fleet.ScheduledQueryResultRow{} + err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, selectStmt, queryID, hostID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "selecting query result rows for host") + } + + return results, nil +} diff --git a/server/datastore/mysql/scheduled_queries_test.go b/server/datastore/mysql/scheduled_queries_test.go index 9231587f99..04568fea60 100644 --- a/server/datastore/mysql/scheduled_queries_test.go +++ b/server/datastore/mysql/scheduled_queries_test.go @@ -44,7 +44,7 @@ func testScheduledQueriesListInPackWithStats(t *testing.T, ds *Datastore) { {Name: "foo", Description: "get the foos", Query: "select * from foo"}, {Name: "bar", Description: "do some bars", Query: "select baz from bar"}, } - err := ds.ApplyQueries(context.Background(), zwass.ID, queries) + err := ds.ApplyQueries(context.Background(), zwass.ID, queries, nil) require.NoError(t, err) specs := []*fleet.PackSpec{ @@ -134,7 +134,7 @@ func testScheduledQueriesListInPack(t *testing.T, ds *Datastore) { {Name: "foo", Description: "get the foos", Query: "select * from foo"}, {Name: "bar", Description: "do some bars", Query: "select baz from bar"}, } - err := ds.ApplyQueries(context.Background(), zwass.ID, queries) + err := ds.ApplyQueries(context.Background(), zwass.ID, queries, nil) require.NoError(t, err) specs := []*fleet.PackSpec{ @@ -327,7 +327,7 @@ func testScheduledQueriesCascadingDelete(t *testing.T, ds *Datastore) { {Name: "foo", Description: "get the foos", Query: "select * from foo"}, {Name: "bar", Description: "do some bars", Query: "select baz from bar"}, } - err := ds.ApplyQueries(context.Background(), zwass.ID, queries) + err := ds.ApplyQueries(context.Background(), zwass.ID, queries, nil) require.Nil(t, err) specs := []*fleet.PackSpec{ @@ -379,7 +379,7 @@ func testScheduledQueriesIDsByName(t *testing.T, ds *Datastore) { {Name: "foo2", Description: "get the foos", Query: "select * from foo2"}, {Name: "bar2", Description: "do some bars", Query: "select * from bar2"}, } - err := ds.ApplyQueries(ctx, user.ID, queries) + err := ds.ApplyQueries(ctx, user.ID, queries, nil) require.NoError(t, err) specs := []*fleet.PackSpec{ diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 10814e0900..b8c43ee5e9 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -40,7 +40,7 @@ CREATE TABLE `app_config_json` ( UNIQUE KEY `id` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `carve_blocks` ( @@ -686,9 +686,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=212 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=214 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20230918221115,1,'2020-01-01 01:01:01'),(210,20230927111208,1,'2020-01-01 01:01:01'),(211,20231009094540,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( @@ -1050,6 +1050,7 @@ CREATE TABLE `queries` ( `schedule_interval` int(10) unsigned NOT NULL DEFAULT '0', `automations_enabled` tinyint(1) unsigned NOT NULL DEFAULT '0', `logging_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'snapshot', + `discard_data` tinyint(1) NOT NULL DEFAULT '1', PRIMARY KEY (`id`), UNIQUE KEY `idx_team_id_name_unq` (`team_id_char`,`name`), UNIQUE KEY `idx_name_team_id_unq` (`name`,`team_id_char`), @@ -1061,6 +1062,21 @@ CREATE TABLE `queries` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; +CREATE TABLE `query_results` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `query_id` int(10) unsigned NOT NULL, + `host_id` int(10) unsigned NOT NULL, + `osquery_version` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `error` text COLLATE utf8mb4_unicode_ci, + `last_fetched` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `data` json DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `query_id` (`query_id`), + CONSTRAINT `query_results_ibfk_1` FOREIGN KEY (`query_id`) REFERENCES `queries` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; CREATE TABLE `scep_certificates` ( `serial` bigint(20) NOT NULL, `name` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL, diff --git a/server/fleet/app.go b/server/fleet/app.go index 4819d954e7..e12e04892f 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -755,11 +755,12 @@ const DefaultOrgInfoContactURL = "https://fleetdm.com/company/contact" // ServerSettings contains general settings about the Fleet application. type ServerSettings struct { - ServerURL string `json:"server_url"` - LiveQueryDisabled bool `json:"live_query_disabled"` - EnableAnalytics bool `json:"enable_analytics"` - DebugHostIDs []uint `json:"debug_host_ids,omitempty"` - DeferredSaveHost bool `json:"deferred_save_host"` + ServerURL string `json:"server_url"` + LiveQueryDisabled bool `json:"live_query_disabled"` + EnableAnalytics bool `json:"enable_analytics"` + DebugHostIDs []uint `json:"debug_host_ids,omitempty"` + DeferredSaveHost bool `json:"deferred_save_host"` + QueryReportsDisabled bool `json:"query_reports_disabled"` } // HostExpirySettings contains settings pertaining to automatic host expiry. diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index d5f63e8203..dd4d0257d1 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -66,11 +66,11 @@ type Datastore interface { // ApplyQueries applies a list of queries (likely from a yaml file) to the datastore. Existing queries are updated, // and new queries are created. - ApplyQueries(ctx context.Context, authorID uint, queries []*Query) error + ApplyQueries(ctx context.Context, authorID uint, queries []*Query, queriesToDiscardResults map[uint]bool) error // NewQuery creates a new query object in thie datastore. The returned query should have the ID updated. NewQuery(ctx context.Context, query *Query, opts ...OptionalArg) (*Query, error) // SaveQuery saves changes to an existing query object. - SaveQuery(ctx context.Context, query *Query) error + SaveQuery(ctx context.Context, query *Query, shouldDiscardResults bool) error // DeleteQuery deletes an existing query object on a team. If teamID is nil, then the query is // looked up in the 'global' team. DeleteQuery(ctx context.Context, teamID *uint, name string) error @@ -84,13 +84,15 @@ type Datastore interface { ListQueries(ctx context.Context, opt ListQueryOptions) ([]*Query, error) // ListScheduledQueriesForAgents returns a list of scheduled queries (without stats) for the // given teamID. If teamID is nil, then all scheduled queries for the 'global' team are returned. - ListScheduledQueriesForAgents(ctx context.Context, teamID *uint) ([]*Query, error) + ListScheduledQueriesForAgents(ctx context.Context, teamID *uint, queryReportsDisabled bool) ([]*Query, error) // QueryByName looks up a query by name on a team. If teamID is nil, then the query is looked up in // the 'global' team. QueryByName(ctx context.Context, teamID *uint, name string, opts ...OptionalArg) (*Query, error) // ObserverCanRunQuery returns whether a user with an observer role is permitted to run the // identified query ObserverCanRunQuery(ctx context.Context, queryID uint) (bool, error) + // CleanupGlobalDiscardQueryResults deletes all cached query results. Used in cleanups_then_aggregation cron. + CleanupGlobalDiscardQueryResults(ctx context.Context) error /////////////////////////////////////////////////////////////////////////////// // CampaignStore defines the distributed query campaign related datastore methods @@ -391,6 +393,15 @@ type Datastore interface { // scheduled query did not exist. ScheduledQueryIDsByName(ctx context.Context, batchSize int, packAndSchedQueryNames ...[2]string) ([]uint, error) + /////////////////////////////////////////////////////////////////////////////// + // QueryResultsStore + + // QueryResultRows returns all the stored results of a query (from all hosts). + QueryResultRows(ctx context.Context, queryID uint) ([]*ScheduledQueryResultRow, error) + ResultCountForQuery(ctx context.Context, queryID uint) (int, error) + ResultCountForQueryAndHost(ctx context.Context, queryID, hostID uint) (int, error) + OverwriteQueryResultRows(ctx context.Context, rows []*ScheduledQueryResultRow) error + /////////////////////////////////////////////////////////////////////////////// // TeamStore diff --git a/server/fleet/osquery.go b/server/fleet/osquery.go index 737e4aed4d..8445977635 100644 --- a/server/fleet/osquery.go +++ b/server/fleet/osquery.go @@ -10,7 +10,8 @@ type OsqueryStatus int const ( // StatusOK is the success code returned by osquery - StatusOK OsqueryStatus = 0 + StatusOK OsqueryStatus = 0 + MaxQueryReportRows int = 1000 ) // QueryContent is the format of a query stanza in an osquery configuration. diff --git a/server/fleet/queries.go b/server/fleet/queries.go index 867680573a..6c0b86d0fd 100644 --- a/server/fleet/queries.go +++ b/server/fleet/queries.go @@ -1,6 +1,7 @@ package fleet import ( + "encoding/json" "errors" "fmt" "strings" @@ -36,6 +37,11 @@ type QueryPayload struct { AutomationsEnabled *bool `json:"automations_enabled"` // Logging is set to "snapshot" if not set when creating a query. Logging *string `json:"logging"` + // DiscardData indicates if the scheduled query results should be discarded (true) + // or kept (false) in a query report. + // + // If not set during creation of a query, then the default value is false. + DiscardData *bool `json:"discard_data"` } // Query represents a osquery query to run on devices. @@ -91,6 +97,9 @@ type Query struct { // // This field has null values if the query did not run as a schedule on any host. AggregatedStats `json:"stats"` + // DiscardData indicates if the scheduled query results should be discarded (true) + // or kept (false) in a query report. + DiscardData bool `json:"discard_data" db:"discard_data"` } var ( @@ -286,6 +295,11 @@ type QuerySpec struct { AutomationsEnabled bool `json:"automations_enabled"` // Logging is set to "snapshot" if not set. Logging string `json:"logging"` + // DiscardData indicates if the scheduled query results should be discarded (true) + // or kept (false) in a query report. + // + // If not set, then the default value is false. + DiscardData bool `json:"discard_data"` } func LoadQueriesFromYaml(yml string) ([]*Query, error) { @@ -355,3 +369,64 @@ type QueryStats struct { UserTime int `json:"user_time" db:"user_time"` WallTime int `json:"wall_time" db:"wall_time"` } + +// MapQueryReportsResultsToRows converts the scheduled query results as stored in Fleet's database +// to HostQueryResultRows to be exposed to the API. +func MapQueryReportResultsToRows(rows []*ScheduledQueryResultRow) ([]HostQueryResultRow, error) { + var results []HostQueryResultRow + for _, row := range rows { + var columns map[string]string + if err := json.Unmarshal(row.Data, &columns); err != nil { + return nil, err + } + results = append(results, HostQueryResultRow{ + HostID: row.HostID, + Hostname: row.Hostname, + LastFetched: row.LastFetched, + Columns: columns, + }) + } + return results, nil +} + +// HostQueryResultRow contains a single scheduled query result row from a host. +// This type is used to expose the results on the API. +type HostQueryResultRow struct { + // HostID is the unique ID of the host. + HostID uint `json:"host_id"` + // Hostname is the host's hostname. + Hostname string `json:"host_name"` + // LastFetched is the time this result row was received. + LastFetched time.Time `json:"last_fetched"` + // Columns contains the key-value pairs of a result row. + // The map key is the name of the column, and the map value is the value. + Columns map[string]string `json:"columns"` +} + +// ScheduledQueryResult holds results of a scheduled query received from a osquery agent. +type ScheduledQueryResult struct { + // QueryName is the name of the query. + QueryName string `json:"name,omitempty"` + // OsqueryHostID is the identifier of the host. + OsqueryHostID string `json:"hostIdentifier"` + // Snapshot holds the result rows. It's an array of maps, where the map keys + // are column names and map values are the values. + Snapshot []json.RawMessage `json:"snapshot"` + // LastFetched is the time this result was received. + UnixTime uint `json:"unixTime"` +} + +// ScheduledQueryResultRow is a scheduled query result row. +type ScheduledQueryResultRow struct { + // QueryID is the unique identifier of the query. + QueryID uint `db:"query_id"` + // HostID is the unique identifier of the host. + HostID uint `db:"host_id"` + // Hostname is the host's hostname. + Hostname string `db:"hostname"` + // Data holds a single result row. It holds a map where the map keys + // are column names and map values are the values. + Data json.RawMessage `db:"data"` + // LastFetched is the time this result was received. + LastFetched time.Time `db:"last_fetched"` +} diff --git a/server/fleet/queries_test.go b/server/fleet/queries_test.go index ca0341d348..32eb9e10c2 100644 --- a/server/fleet/queries_test.go +++ b/server/fleet/queries_test.go @@ -1,7 +1,9 @@ package fleet import ( + "encoding/json" "testing" + "time" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/stretchr/testify/assert" @@ -210,3 +212,216 @@ func TestVerifyQueryPlatforms(t *testing.T) { }) } } + +func TestMapQueryReportResultRows(t *testing.T) { + macOSUSBDevicesLastFetched := time.Now() + ubuntuUSBDevicesLastFetched := time.Now().Add(-1 * time.Hour) + macOSOsqueryInfoLastFetched := time.Now().Add(-2 * time.Hour) + for _, tc := range []struct { + name string + rows []*ScheduledQueryResultRow + expected []HostQueryResultRow + shouldFail bool + }{ + { + name: "USB devices query with results from a macOS and Linux host", + rows: []*ScheduledQueryResultRow{ + { + HostID: 1, + Hostname: "macOS host", + LastFetched: macOSUSBDevicesLastFetched, + Data: json.RawMessage(`{ + "class": "9", + "model": "AppleUSBVHCIBCE Root Hub Simulation", + "model_id": "8000", + "protocol": "", + "removable": "0", + "serial": "0", + "subclass": "255", + "usb_address": "", + "usb_port": "", + "vendor": "Apple Inc.", + "vendor_id": "05bc", + "version": "0.0" + }`), + }, + { + HostID: 1, + Hostname: "macOS host", + LastFetched: macOSUSBDevicesLastFetched, + Data: json.RawMessage(`{ + "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" + }`), + }, + { + HostID: 2, + Hostname: "ubuntu host", + LastFetched: ubuntuUSBDevicesLastFetched, + Data: json.RawMessage(`{ + "class": "9", + "model": "1.1 root hub", + "model_id": "0001", + "protocol": "0", + "removable": "-1", + "serial": "0000:02:00.0", + "subclass": "0", + "usb_address": "1", + "usb_port": "1", + "vendor": "Linux Foundation", + "vendor_id": "1d6b", + "version": "0602" + }`), + }, + }, + expected: []HostQueryResultRow{ + { + HostID: 1, + Hostname: "macOS host", + LastFetched: macOSUSBDevicesLastFetched, + Columns: map[string]string{ + "class": "9", + "model": "AppleUSBVHCIBCE Root Hub Simulation", + "model_id": "8000", + "protocol": "", + "removable": "0", + "serial": "0", + "subclass": "255", + "usb_address": "", + "usb_port": "", + "vendor": "Apple Inc.", + "vendor_id": "05bc", + "version": "0.0", + }, + }, + { + HostID: 1, + Hostname: "macOS host", + LastFetched: macOSUSBDevicesLastFetched, + Columns: map[string]string{ + "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", + }, + }, + { + HostID: 2, + Hostname: "ubuntu host", + LastFetched: ubuntuUSBDevicesLastFetched, + Columns: map[string]string{ + "class": "9", + "model": "1.1 root hub", + "model_id": "0001", + "protocol": "0", + "removable": "-1", + "serial": "0000:02:00.0", + "subclass": "0", + "usb_address": "1", + "usb_port": "1", + "vendor": "Linux Foundation", + "vendor_id": "1d6b", + "version": "0602", + }, + }, + }, + shouldFail: false, + }, + { + name: "macOS osquery_info result", + rows: []*ScheduledQueryResultRow{ + { + HostID: 1, + Hostname: "macOS host", + LastFetched: macOSOsqueryInfoLastFetched, + Data: json.RawMessage(`{ + "build_distro": "10.14", + "build_platform": "darwin", + "config_hash": "eed0d8296e5f90b790a23814a9db7a127b13498d", + "config_valid": "1", + "extensions": "active", + "instance_id": "7f02ff0f-f8a7-4ba9-a1d2-66836b154f4a", + "pid": "96730", + "platform_mask": "21", + "start_time": "1696421866", + "uuid": "589966AE-074A-503B-B17B-54B05684A120", + "version": "5.9.1", + "watcher": "96729" + }`), + }, + }, + expected: []HostQueryResultRow{ + { + HostID: 1, + Hostname: "macOS host", + LastFetched: macOSOsqueryInfoLastFetched, + Columns: map[string]string{ + "build_distro": "10.14", + "build_platform": "darwin", + "config_hash": "eed0d8296e5f90b790a23814a9db7a127b13498d", + "config_valid": "1", + "extensions": "active", + "instance_id": "7f02ff0f-f8a7-4ba9-a1d2-66836b154f4a", + "pid": "96730", + "platform_mask": "21", + "start_time": "1696421866", + "uuid": "589966AE-074A-503B-B17B-54B05684A120", + "version": "5.9.1", + "watcher": "96729", + }, + }, + }, + shouldFail: false, + }, + { + name: "invalid JSON result", + rows: []*ScheduledQueryResultRow{ + { + HostID: 3, + Hostname: "bar", + LastFetched: time.Now(), + Data: json.RawMessage(`invalid JSON`), + }, + }, + shouldFail: true, + }, + { + name: "invalid item value type", + rows: []*ScheduledQueryResultRow{ + { + HostID: 3, + Hostname: "bar", + LastFetched: time.Now(), + Data: json.RawMessage(`{"foobar": 1}`), + }, + }, + shouldFail: true, + }, + } { + results, err := MapQueryReportResultsToRows(tc.rows) + if !tc.shouldFail { + require.NoError(t, err) + require.Equal(t, tc.expected, results) + } else { + require.Error(t, err) + } + } +} diff --git a/server/fleet/service.go b/server/fleet/service.go index 4ebde994b9..86420cc843 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -268,6 +268,8 @@ type Service interface { // and only non-scheduled queries will be returned if `*scheduled == false`. ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool) ([]*Query, error) GetQuery(ctx context.Context, id uint) (*Query, error) + // GetQueryReportResults returns all the stored results of a query. + GetQueryReportResults(ctx context.Context, id uint) ([]HostQueryResultRow, error) NewQuery(ctx context.Context, p QueryPayload) (*Query, error) ModifyQuery(ctx context.Context, id uint, p QueryPayload) (*Query, error) DeleteQuery(ctx context.Context, teamID *uint, name string) error diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index b1a1ea0b19..2c85a98eb4 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -56,11 +56,11 @@ type PendingEmailChangeFunc func(ctx context.Context, userID uint, newEmail stri type ConfirmPendingEmailChangeFunc func(ctx context.Context, userID uint, token string) (string, error) -type ApplyQueriesFunc func(ctx context.Context, authorID uint, queries []*fleet.Query) error +type ApplyQueriesFunc func(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]bool) error type NewQueryFunc func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) -type SaveQueryFunc func(ctx context.Context, query *fleet.Query) error +type SaveQueryFunc func(ctx context.Context, query *fleet.Query, shouldDiscardResults bool) error type DeleteQueryFunc func(ctx context.Context, teamID *uint, name string) error @@ -70,12 +70,14 @@ type QueryFunc func(ctx context.Context, id uint) (*fleet.Query, error) type ListQueriesFunc func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) -type ListScheduledQueriesForAgentsFunc func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) +type ListScheduledQueriesForAgentsFunc func(ctx context.Context, teamID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) type QueryByNameFunc func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) type ObserverCanRunQueryFunc func(ctx context.Context, queryID uint) (bool, error) +type CleanupGlobalDiscardQueryResultsFunc func(ctx context.Context) error + type NewDistributedQueryCampaignFunc func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) type DistributedQueryCampaignFunc func(ctx context.Context, id uint) (*fleet.DistributedQueryCampaign, error) @@ -294,6 +296,14 @@ type CleanupExpiredHostsFunc func(ctx context.Context) ([]uint, error) type ScheduledQueryIDsByNameFunc func(ctx context.Context, batchSize int, packAndSchedQueryNames ...[2]string) ([]uint, error) +type QueryResultRowsFunc func(ctx context.Context, queryID uint) ([]*fleet.ScheduledQueryResultRow, error) + +type ResultCountForQueryFunc func(ctx context.Context, queryID uint) (int, error) + +type ResultCountForQueryAndHostFunc func(ctx context.Context, queryID uint, hostID uint) (int, error) + +type OverwriteQueryResultRowsFunc func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error + type NewTeamFunc func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) type SaveTeamFunc func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) @@ -792,6 +802,9 @@ type DataStore struct { ObserverCanRunQueryFunc ObserverCanRunQueryFunc ObserverCanRunQueryFuncInvoked bool + CleanupGlobalDiscardQueryResultsFunc CleanupGlobalDiscardQueryResultsFunc + CleanupGlobalDiscardQueryResultsFuncInvoked bool + NewDistributedQueryCampaignFunc NewDistributedQueryCampaignFunc NewDistributedQueryCampaignFuncInvoked bool @@ -1119,6 +1132,18 @@ type DataStore struct { ScheduledQueryIDsByNameFunc ScheduledQueryIDsByNameFunc ScheduledQueryIDsByNameFuncInvoked bool + QueryResultRowsFunc QueryResultRowsFunc + QueryResultRowsFuncInvoked bool + + ResultCountForQueryFunc ResultCountForQueryFunc + ResultCountForQueryFuncInvoked bool + + ResultCountForQueryAndHostFunc ResultCountForQueryAndHostFunc + ResultCountForQueryAndHostFuncInvoked bool + + OverwriteQueryResultRowsFunc OverwriteQueryResultRowsFunc + OverwriteQueryResultRowsFuncInvoked bool + NewTeamFunc NewTeamFunc NewTeamFuncInvoked bool @@ -1870,11 +1895,11 @@ func (s *DataStore) ConfirmPendingEmailChange(ctx context.Context, userID uint, return s.ConfirmPendingEmailChangeFunc(ctx, userID, token) } -func (s *DataStore) ApplyQueries(ctx context.Context, authorID uint, queries []*fleet.Query) error { +func (s *DataStore) ApplyQueries(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]bool) error { s.mu.Lock() s.ApplyQueriesFuncInvoked = true s.mu.Unlock() - return s.ApplyQueriesFunc(ctx, authorID, queries) + return s.ApplyQueriesFunc(ctx, authorID, queries, queriesToDiscardResults) } func (s *DataStore) NewQuery(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) { @@ -1884,11 +1909,11 @@ func (s *DataStore) NewQuery(ctx context.Context, query *fleet.Query, opts ...fl return s.NewQueryFunc(ctx, query, opts...) } -func (s *DataStore) SaveQuery(ctx context.Context, query *fleet.Query) error { +func (s *DataStore) SaveQuery(ctx context.Context, query *fleet.Query, shouldDiscardResults bool) error { s.mu.Lock() s.SaveQueryFuncInvoked = true s.mu.Unlock() - return s.SaveQueryFunc(ctx, query) + return s.SaveQueryFunc(ctx, query, shouldDiscardResults) } func (s *DataStore) DeleteQuery(ctx context.Context, teamID *uint, name string) error { @@ -1919,11 +1944,11 @@ func (s *DataStore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) return s.ListQueriesFunc(ctx, opt) } -func (s *DataStore) ListScheduledQueriesForAgents(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { +func (s *DataStore) ListScheduledQueriesForAgents(ctx context.Context, teamID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) { s.mu.Lock() s.ListScheduledQueriesForAgentsFuncInvoked = true s.mu.Unlock() - return s.ListScheduledQueriesForAgentsFunc(ctx, teamID) + return s.ListScheduledQueriesForAgentsFunc(ctx, teamID, queryReportsDisabled) } func (s *DataStore) QueryByName(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { @@ -1940,6 +1965,13 @@ func (s *DataStore) ObserverCanRunQuery(ctx context.Context, queryID uint) (bool return s.ObserverCanRunQueryFunc(ctx, queryID) } +func (s *DataStore) CleanupGlobalDiscardQueryResults(ctx context.Context) error { + s.mu.Lock() + s.CleanupGlobalDiscardQueryResultsFuncInvoked = true + s.mu.Unlock() + return s.CleanupGlobalDiscardQueryResultsFunc(ctx) +} + func (s *DataStore) NewDistributedQueryCampaign(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) { s.mu.Lock() s.NewDistributedQueryCampaignFuncInvoked = true @@ -2703,6 +2735,34 @@ func (s *DataStore) ScheduledQueryIDsByName(ctx context.Context, batchSize int, return s.ScheduledQueryIDsByNameFunc(ctx, batchSize, packAndSchedQueryNames...) } +func (s *DataStore) QueryResultRows(ctx context.Context, queryID uint) ([]*fleet.ScheduledQueryResultRow, error) { + s.mu.Lock() + s.QueryResultRowsFuncInvoked = true + s.mu.Unlock() + return s.QueryResultRowsFunc(ctx, queryID) +} + +func (s *DataStore) ResultCountForQuery(ctx context.Context, queryID uint) (int, error) { + s.mu.Lock() + s.ResultCountForQueryFuncInvoked = true + s.mu.Unlock() + return s.ResultCountForQueryFunc(ctx, queryID) +} + +func (s *DataStore) ResultCountForQueryAndHost(ctx context.Context, queryID uint, hostID uint) (int, error) { + s.mu.Lock() + s.ResultCountForQueryAndHostFuncInvoked = true + s.mu.Unlock() + return s.ResultCountForQueryAndHostFunc(ctx, queryID, hostID) +} + +func (s *DataStore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error { + s.mu.Lock() + s.OverwriteQueryResultRowsFuncInvoked = true + s.mu.Unlock() + return s.OverwriteQueryResultRowsFunc(ctx, rows) +} + func (s *DataStore) NewTeam(ctx context.Context, team *fleet.Team) (*fleet.Team, error) { s.mu.Lock() s.NewTeamFuncInvoked = true diff --git a/server/service/global_schedule_test.go b/server/service/global_schedule_test.go index 25f25608c6..d29e0f3f40 100644 --- a/server/service/global_schedule_test.go +++ b/server/service/global_schedule_test.go @@ -24,7 +24,7 @@ func TestGlobalScheduleAuth(t *testing.T) { Query: "SELECT 1;", }, nil } - ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query) error { + ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query, shouldDiscardResults bool) error { return nil } ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { diff --git a/server/service/handler.go b/server/service/handler.go index c13808415d..f1c50486bd 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -338,6 +338,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC ue.GET("/api/_version_/fleet/queries/{id:[0-9]+}", getQueryEndpoint, getQueryRequest{}) ue.GET("/api/_version_/fleet/queries", listQueriesEndpoint, listQueriesRequest{}) + ue.GET("/api/_version_/fleet/queries/{id:[0-9]+}/report", getQueryReportEndpoint, getQueryReportRequest{}) ue.POST("/api/_version_/fleet/queries", createQueryEndpoint, createQueryRequest{}) ue.PATCH("/api/_version_/fleet/queries/{id:[0-9]+}", modifyQueryEndpoint, modifyQueryRequest{}) ue.DELETE("/api/_version_/fleet/queries/{name}", deleteQueryEndpoint, deleteQueryRequest{}) diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 6bbb60c2d1..8af8976a82 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -2548,6 +2548,10 @@ func (s *integrationTestSuite) TestScheduledQueries() { s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", query.ID), fleet.QueryPayload{Description: ptr.String("updated")}, http.StatusOK, &modQryResp) assert.Equal(t, "updated", modQryResp.Query.Description) + // TODO(jahziel): check that the query results were deleted + + // TODO(jahziel): check that the query results were deleted after setting `discard_data` + // modify a non-existing query s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", query.ID+1), fleet.QueryPayload{Description: ptr.String("updated")}, http.StatusNotFound, &modQryResp) @@ -7941,3 +7945,415 @@ func (s *integrationTestSuite) TestHostsReportWithPolicyResults() { }) } } + +func (s *integrationTestSuite) TestQueryReports() { + t := s.T() + ctx := context.Background() + + team1, err := s.ds.NewTeam(ctx, &fleet.Team{ + ID: 42, + Name: "team1", + Description: "desc team1", + }) + require.NoError(t, err) + + host1Global, err := s.ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("1"), + UUID: "1", + Hostname: "foo.local1", + OsqueryHostID: ptr.String("1"), + PrimaryIP: "192.168.1.1", + PrimaryMac: "30-65-EC-6F-C4-58", + Platform: "ubuntu", + }) + require.NoError(t, err) + + host2Team1, err := s.ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("2"), + UUID: "2", + Hostname: "foo.local2", + OsqueryHostID: ptr.String("2"), + PrimaryIP: "192.168.1.2", + PrimaryMac: "30-65-EC-6F-C4-59", + Platform: "darwin", + }) + require.NoError(t, err) + + err = s.ds.AddHostsToTeam(ctx, &team1.ID, []uint{host2Team1.ID}) + require.NoError(t, err) + + osqueryInfoQuery, err := s.ds.NewQuery(ctx, &fleet.Query{ + Name: "Osquery info", + Description: "osquery_info table", + Query: "select * from osquery_info;", + Saved: true, + Interval: 30, + AutomationsEnabled: true, + DiscardData: false, + TeamID: nil, + Logging: fleet.LoggingSnapshot, + }) + require.NoError(t, err) + + usbDevicesQuery, err := s.ds.NewQuery(ctx, &fleet.Query{ + Name: "USB devices", + Description: "usb_devices table", + Query: "select * from usb_devices;", + Saved: true, + Interval: 60, + AutomationsEnabled: true, + DiscardData: false, + TeamID: ptr.Uint(team1.ID), + Logging: fleet.LoggingSnapshot, + }) + require.NoError(t, err) + + // Should return no results. + var gqrr getQueryReportResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", usbDevicesQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.NoError(t, gqrr.Err) + require.Equal(t, usbDevicesQuery.ID, gqrr.QueryID) + require.NotNil(t, gqrr.Results) + require.Len(t, gqrr.Results, 0) + + slreq := submitLogsRequest{ + NodeKey: *host2Team1.NodeKey, + LogType: "result", + Data: json.RawMessage(`[{ + "snapshot": [ + { + "class": "239", + "model": "HD Pro Webcam C920", + "model_id": "0892", + "protocol": "", + "removable": "1", + "serial": "zoobar", + "subclass": "2", + "usb_address": "3", + "usb_port": "1", + "vendor": "", + "vendor_id": "046d", + "version": "0.19" + }, + { + "class": "0", + "model": "Apple Internal Keyboard / Trackpad", + "model_id": "027e", + "protocol": "", + "removable": "0", + "serial": "foobar", + "subclass": "0", + "usb_address": "8", + "usb_port": "5", + "vendor": "Apple Inc.", + "vendor_id": "05ac", + "version": "9.33" + } + ], + "action": "snapshot", + "name": "pack/team-` + usbDevicesQuery.TeamIDStr() + `/` + usbDevicesQuery.Name + `", + "hostIdentifier": "` + *host2Team1.OsqueryHostID + `", + "calendarTime": "Fri Oct 6 17:32:08 2023 UTC", + "unixTime": 1696613528, + "epoch": 0, + "counter": 0, + "numerics": false, + "decorations": { + "host_uuid": "` + host2Team1.UUID + `", + "hostname": "` + host2Team1.Hostname + `" + } +}, +{ + "snapshot": [ + { + "build_distro": "10.14", + "build_platform": "darwin", + "config_hash": "eed0d8296e5f90b790a23814a9db7a127b13498d", + "config_valid": "1", + "extensions": "active", + "instance_id": "7f02ff0f-f8a7-4ba9-a1d2-66836b154f4a", + "pid": "95637", + "platform_mask": "21", + "start_time": "1696611201", + "uuid": "` + host2Team1.UUID + `", + "version": "5.9.1", + "watcher": "95636" + } + ], + "action": "snapshot", + "name": "pack/Global/` + osqueryInfoQuery.Name + `", + "hostIdentifier": "` + *host2Team1.OsqueryHostID + `", + "calendarTime": "Fri Oct 6 18:08:18 2023 UTC", + "unixTime": 1696615698, + "epoch": 0, + "counter": 0, + "numerics": false, + "decorations": { + "host_uuid": "` + host2Team1.UUID + `", + "hostname": "` + host2Team1.Hostname + `" + } +} +]`), + } + slres := submitLogsResponse{} + s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres) + require.NoError(t, slres.Err) + + slreq = submitLogsRequest{ + NodeKey: *host1Global.NodeKey, + LogType: "result", + Data: json.RawMessage(`[{ + "snapshot": [ + { + "build_distro": "centos7", + "build_platform": "linux", + "config_hash": "eed0d8296e5f90b790a23814a9db7a127b13498d", + "config_valid": "1", + "extensions": "active", + "instance_id": "e5799132-85ab-4cfa-89f3-03e0dd3c509a", + "pid": "3574", + "platform_mask": "9", + "start_time": "1696502961", + "uuid": "` + host1Global.UUID + `", + "version": "5.9.2", + "watcher": "3570" + } + ], + "action": "snapshot", + "name": "pack/Global/` + osqueryInfoQuery.Name + `", + "hostIdentifier": "` + *host1Global.OsqueryHostID + `", + "calendarTime": "Fri Oct 6 18:13:04 2023 UTC", + "unixTime": 1696615984, + "epoch": 0, + "counter": 0, + "numerics": false, + "decorations": { + "host_uuid": "187c4d56-8e45-1a9d-8513-ac17efd2f0fd", + "hostname": "` + host1Global.Hostname + `" + } +}]`), + } + slres = submitLogsResponse{} + s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres) + require.NoError(t, slres.Err) + + gqrr = getQueryReportResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", usbDevicesQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.NoError(t, gqrr.Err) + require.Equal(t, usbDevicesQuery.ID, gqrr.QueryID) + require.Len(t, gqrr.Results, 2) + sort.Slice(gqrr.Results, func(i, j int) bool { + // Let's just pick a known column of the query to sort. + return gqrr.Results[i].Columns["usb_port"] < gqrr.Results[j].Columns["usb_port"] + }) + require.Equal(t, host2Team1.ID, gqrr.Results[0].HostID) + require.Equal(t, host2Team1.Hostname, gqrr.Results[0].Hostname) + require.NotZero(t, gqrr.Results[0].LastFetched) + require.Equal(t, map[string]string{ + "class": "239", + "model": "HD Pro Webcam C920", + "model_id": "0892", + "protocol": "", + "removable": "1", + "serial": "zoobar", + "subclass": "2", + "usb_address": "3", + "usb_port": "1", + "vendor": "", + "vendor_id": "046d", + "version": "0.19", + }, gqrr.Results[0].Columns) + require.Equal(t, host2Team1.ID, gqrr.Results[1].HostID) + require.Equal(t, host2Team1.Hostname, gqrr.Results[1].Hostname) + require.NotZero(t, gqrr.Results[1].LastFetched) + require.Equal(t, map[string]string{ + "class": "0", + "model": "Apple Internal Keyboard / Trackpad", + "model_id": "027e", + "protocol": "", + "removable": "0", + "serial": "foobar", + "subclass": "0", + "usb_address": "8", + "usb_port": "5", + "vendor": "Apple Inc.", + "vendor_id": "05ac", + "version": "9.33", + }, gqrr.Results[1].Columns) + + gqrr = getQueryReportResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.NoError(t, gqrr.Err) + require.Equal(t, osqueryInfoQuery.ID, gqrr.QueryID) + require.Len(t, gqrr.Results, 2) + sort.Slice(gqrr.Results, func(i, j int) bool { + // Let's just pick a known column of the query to sort. + return gqrr.Results[i].Columns["version"] > gqrr.Results[j].Columns["version"] + }) + require.Equal(t, host1Global.ID, gqrr.Results[0].HostID) + require.Equal(t, host1Global.Hostname, gqrr.Results[0].Hostname) + require.NotZero(t, gqrr.Results[0].LastFetched) + require.Equal(t, map[string]string{ + "build_distro": "centos7", + "build_platform": "linux", + "config_hash": "eed0d8296e5f90b790a23814a9db7a127b13498d", + "config_valid": "1", + "extensions": "active", + "instance_id": "e5799132-85ab-4cfa-89f3-03e0dd3c509a", + "pid": "3574", + "platform_mask": "9", + "start_time": "1696502961", + "uuid": host1Global.UUID, + "version": "5.9.2", + "watcher": "3570", + }, gqrr.Results[0].Columns) + require.Equal(t, host2Team1.ID, gqrr.Results[1].HostID) + require.Equal(t, host2Team1.Hostname, gqrr.Results[1].Hostname) + require.NotZero(t, gqrr.Results[1].LastFetched) + require.Equal(t, map[string]string{ + "build_distro": "10.14", + "build_platform": "darwin", + "config_hash": "eed0d8296e5f90b790a23814a9db7a127b13498d", + "config_valid": "1", + "extensions": "active", + "instance_id": "7f02ff0f-f8a7-4ba9-a1d2-66836b154f4a", + "pid": "95637", + "platform_mask": "21", + "start_time": "1696611201", + "uuid": host2Team1.UUID, + "version": "5.9.1", + "watcher": "95636", + }, gqrr.Results[1].Columns) + + // verify that certain modifications to queries don't cause result deletion + modifyQueryResp := modifyQueryResponse{} + updatedDesc := "Updated description" + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{ID: osqueryInfoQuery.ID, QueryPayload: fleet.QueryPayload{Description: &updatedDesc}}, http.StatusOK, &modifyQueryResp) + require.Equal(t, updatedDesc, modifyQueryResp.Query.Description) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.Len(t, gqrr.Results, 2) + + // now cause deletions and verify that results are deleted + updatedQuery := "SELECT * FROM some_new_table;" + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{ID: osqueryInfoQuery.ID, QueryPayload: fleet.QueryPayload{Query: &updatedQuery}}, http.StatusOK, &modifyQueryResp) + require.Equal(t, updatedQuery, modifyQueryResp.Query.Query) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.Len(t, gqrr.Results, 0) + + // Update logging type, which should cause results deletion + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", usbDevicesQuery.ID), modifyQueryRequest{ID: usbDevicesQuery.ID, QueryPayload: fleet.QueryPayload{Logging: &fleet.LoggingDifferential}}, http.StatusOK, &modifyQueryResp) + require.Equal(t, fleet.LoggingDifferential, modifyQueryResp.Query.Logging) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", usbDevicesQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.Len(t, gqrr.Results, 0) + + // Re-add results to our query and check that they're actually there + s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres) + require.NoError(t, slres.Err) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.Len(t, gqrr.Results, 1) + + discardData := true + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{ID: osqueryInfoQuery.ID, QueryPayload: fleet.QueryPayload{DiscardData: &discardData}}, http.StatusOK, &modifyQueryResp) + require.True(t, modifyQueryResp.Query.DiscardData) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.Len(t, gqrr.Results, 0) + + // check that now that discardData is set, we don't add new results + s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres) + require.NoError(t, slres.Err) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.Len(t, gqrr.Results, 0) + + // Verify that we can't have more than 1k results + + discardData = false + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{ID: osqueryInfoQuery.ID, QueryPayload: fleet.QueryPayload{DiscardData: &discardData}}, http.StatusOK, &modifyQueryResp) + require.False(t, modifyQueryResp.Query.DiscardData) + + slreq = submitLogsRequest{ + NodeKey: *host1Global.NodeKey, + LogType: "result", + Data: json.RawMessage(`[{ + "snapshot": [` + results(1000, host1Global.UUID) + ` + ], + "action": "snapshot", + "name": "pack/Global/` + osqueryInfoQuery.Name + `", + "hostIdentifier": "` + *host1Global.OsqueryHostID + `", + "calendarTime": "Fri Oct 6 18:13:04 2023 UTC", + "unixTime": 1696615984, + "epoch": 0, + "counter": 0, + "numerics": false, + "decorations": { + "host_uuid": "187c4d56-8e45-1a9d-8513-ac17efd2f0fd", + "hostname": "` + host1Global.Hostname + `" + } +}]`), + } + slres = submitLogsResponse{} + s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres) + require.NoError(t, slres.Err) + + s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres) + require.NoError(t, slres.Err) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.Len(t, gqrr.Results, 1000) + + slreq.Data = json.RawMessage(`[{ + "snapshot": [` + results(1, host1Global.UUID) + ` + ], + "action": "snapshot", + "name": "pack/Global/` + osqueryInfoQuery.Name + `", + "hostIdentifier": "` + *host1Global.OsqueryHostID + `", + "calendarTime": "Fri Oct 6 18:13:04 2023 UTC", + "unixTime": 1696615984, + "epoch": 0, + "counter": 0, + "numerics": false, + "decorations": { + "host_uuid": "187c4d56-8e45-1a9d-8513-ac17efd2f0fd", + "hostname": "` + host1Global.Hostname + `" + } +}]`) + + s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres) + require.NoError(t, slres.Err) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr) + require.Len(t, gqrr.Results, 1000) + + // TODO: Set global discard flag and verify that all data is gone. +} + +// Creates a set of results for use in tests for Query Results. +func results(num int, hostID string) string { + b := strings.Builder{} + for i := 0; i < num; i++ { + b.WriteString(` { + "build_distro": "centos7", + "build_platform": "linux", + "config_hash": "eed0d8296e5f90b790a23814a9db7a127b13498d", + "config_valid": "1", + "extensions": "active", + "instance_id": "e5799132-85ab-4cfa-89f3-03e0dd3c509a", + "pid": "3574", + "platform_mask": "9", + "start_time": "1696502961", + "uuid": "` + hostID + `", + "version": "5.9.2", + "watcher": "3570" + }`) + if i != num-1 { + b.WriteString(",") + } + } + + return b.String() +} diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index fbd13bf825..750e3e7ca2 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -1900,21 +1900,26 @@ func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() { // try with invalid token res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/policies", nil, http.StatusUnauthorized) - res.Body.Close() + err = res.Body.Close() + require.NoError(t, err) // GET `/api/_version_/fleet/device/{token}/policies` listDevicePoliciesResp := listDevicePoliciesResponse{} res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/policies", nil, http.StatusOK) - json.NewDecoder(res.Body).Decode(&listDevicePoliciesResp) //nolint:errcheck - res.Body.Close() //nolint:errcheck + err = json.NewDecoder(res.Body).Decode(&listDevicePoliciesResp) + require.NoError(t, err) + err = res.Body.Close() + require.NoError(t, err) require.Len(t, listDevicePoliciesResp.Policies, 2) require.NoError(t, listDevicePoliciesResp.Err) // GET `/api/_version_/fleet/device/{token}` getDeviceHostResp := getDeviceHostResponse{} res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK) - json.NewDecoder(res.Body).Decode(&getDeviceHostResp) //nolint:errcheck - res.Body.Close() //nolint:errcheck + err = json.NewDecoder(res.Body).Decode(&getDeviceHostResp) + require.NoError(t, err) + err = res.Body.Close() + require.NoError(t, err) require.NoError(t, getDeviceHostResp.Err) require.Equal(t, host.ID, getDeviceHostResp.Host.ID) require.False(t, getDeviceHostResp.Host.RefetchRequested) @@ -1924,8 +1929,10 @@ func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() { // GET `/api/_version_/fleet/device/{token}/desktop` getDesktopResp := fleetDesktopResponse{} res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK) - require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp)) - require.NoError(t, res.Body.Close()) + err = json.NewDecoder(res.Body).Decode(&getDesktopResp) + require.NoError(t, err) + err = res.Body.Close() + require.NoError(t, err) require.NoError(t, getDesktopResp.Err) require.Equal(t, *getDesktopResp.FailingPolicies, uint(1)) require.False(t, getDesktopResp.Notifications.NeedsMDMMigration) diff --git a/server/service/osquery.go b/server/service/osquery.go index 47852be0d0..32a4de296b 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -20,8 +20,8 @@ import ( "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/pubsub" "github.com/fleetdm/fleet/v4/server/service/osquery_utils" - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" + "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/spf13/cast" ) @@ -355,7 +355,12 @@ func getClientConfigEndpoint(ctx context.Context, request interface{}, svc fleet } func (svc *Service) getScheduledQueries(ctx context.Context, teamID *uint) (fleet.Queries, error) { - queries, err := svc.ds.ListScheduledQueriesForAgents(ctx, teamID) + appConfig, err := svc.ds.AppConfig(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "load app config") + } + + queries, err := svc.ds.ListScheduledQueriesForAgents(ctx, teamID, appConfig.ServerSettings.QueryReportsDisabled) if err != nil { return nil, err } @@ -536,7 +541,7 @@ func (svc *Service) AgentOptionsForHost(ctx context.Context, hostTeamID *uint, h // Otherwise return the appropriate override for global options. appConfig, err := svc.ds.AppConfig(ctx) if err != nil { - return nil, ctxerr.Wrap(ctx, err, "load global agent options") + return nil, ctxerr.Wrap(ctx, err, "load app config") } var options fleet.AgentOptions if appConfig.AgentOptions != nil { @@ -1358,6 +1363,9 @@ func submitLogsEndpoint(ctx context.Context, request interface{}, svc fleet.Serv switch req.LogType { case "status": var statuses []json.RawMessage + // NOTE(lucas): This unmarshal error is not being sent back to osquery (`if err :=` vs. `if err =`) + // Maybe there's a reason for it, we need to test such a change before fixing what appears + // to be a bug because the `err` is lost. if err := json.Unmarshal(req.Data, &statuses); err != nil { err = newOsqueryError("unmarshalling status logs: " + err.Error()) break @@ -1370,12 +1378,17 @@ func submitLogsEndpoint(ctx context.Context, request interface{}, svc fleet.Serv case "result": var results []json.RawMessage + // NOTE(lucas): This unmarshal error is not being sent back to osquery (`if err :=` vs. `if err =`) + // Maybe there's a reason for it, we need to test such a change before fixing what appears + // to be a bug because the `err` is lost. if err := json.Unmarshal(req.Data, &results); err != nil { err = newOsqueryError("unmarshalling result logs: " + err.Error()) break } - err = svc.SubmitResultLogs(ctx, results) - if err != nil { + + // We currently return errors to osqueryd if there are any issues submitting results + // to the configured external destinations. + if err = svc.SubmitResultLogs(ctx, results); err != nil { break } @@ -1386,6 +1399,44 @@ func submitLogsEndpoint(ctx context.Context, request interface{}, svc fleet.Serv return submitLogsResponse{Err: err}, nil } +func (svc *Service) preProcessOsqueryResults(ctx context.Context, osqueryResults []json.RawMessage) (unmarshaledResults []*fleet.ScheduledQueryResult, queriesDBData map[string]*fleet.Query) { + // skipauth: Authorization is currently for user endpoints only. + svc.authz.SkipAuthorization(ctx) + + for _, raw := range osqueryResults { + var result *fleet.ScheduledQueryResult + if err := json.Unmarshal(raw, &result); err != nil { + level.Error(svc.logger).Log("msg", "unmarshalling result", "err", err) + // Note we store a nil item if the result could not be unmarshaled. + } + unmarshaledResults = append(unmarshaledResults, result) + } + + queriesDBData = make(map[string]*fleet.Query) + for _, queryResult := range unmarshaledResults { + if queryResult == nil { + // These are results that could not be unmarshaled. + continue + } + teamID, queryName, err := getQueryNameAndTeamIDFromResult(queryResult.QueryName) + if err != nil { + level.Error(svc.logger).Log("msg", "querying name and team ID from result", "err", err) + continue + } + if _, ok := queriesDBData[queryResult.QueryName]; ok { + // Already loaded. + continue + } + query, err := svc.ds.QueryByName(ctx, teamID, queryName) + if err != nil { + level.Debug(svc.logger).Log("msg", "loading query by name", "err", err, "team", teamID, "name", queryName) + continue + } + queriesDBData[queryResult.QueryName] = query + } + return unmarshaledResults, queriesDBData +} + func (svc *Service) SubmitStatusLogs(ctx context.Context, logs []json.RawMessage) error { // skipauth: Authorization is currently for user endpoints only. svc.authz.SkipAuthorization(ctx) @@ -1400,8 +1451,182 @@ func (svc *Service) SubmitResultLogs(ctx context.Context, logs []json.RawMessage // skipauth: Authorization is currently for user endpoints only. svc.authz.SkipAuthorization(ctx) - if err := svc.osqueryLogWriter.Result.Write(ctx, logs); err != nil { + // + // We do not return errors to osqueryd when processing results because + // otherwise the results will never clear from its local DB and + // will keep retrying forever. + // + unmarshaledResults, queriesDBData := svc.preProcessOsqueryResults(ctx, logs) + svc.saveResultLogsToQueryReports(ctx, unmarshaledResults, queriesDBData) + + var filteredLogs []json.RawMessage + for i, unmarshaledResult := range unmarshaledResults { + if unmarshaledResult == nil { + // Ignore results that could not be unmarshaled. + continue + } + dbQuery, ok := queriesDBData[unmarshaledResult.QueryName] + if !ok { + // Ignore results for unknown queries. + continue + } + if !dbQuery.AutomationsEnabled { + // Ignore results for queries that have automations disabled. + continue + } + filteredLogs = append(filteredLogs, logs[i]) + } + if err := svc.osqueryLogWriter.Result.Write(ctx, filteredLogs); err != nil { return newOsqueryError("error writing result logs: " + err.Error()) } return nil } + +//////////////////////////////////////////////////////////////////////////////// +// Query Reports +//////////////////////////////////////////////////////////////////////////////// + +func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshaledResults []*fleet.ScheduledQueryResult, queriesDBData map[string]*fleet.Query) { + // skipauth: Authorization is currently for user endpoints only. + svc.authz.SkipAuthorization(ctx) + + host, ok := hostctx.FromContext(ctx) + if !ok { + level.Error(svc.logger).Log("err", "getting host from context") + return + } + + // Do not insert results if query reports are disabled globally + appConfig, err := svc.ds.AppConfig(ctx) + if err != nil { + level.Error(svc.logger).Log("msg", "getting app config", "err", err) + return + } + if appConfig.ServerSettings.QueryReportsDisabled { + return + } + + // Filter results to only the most recent for each query. + filtered := getMostRecentResults(unmarshaledResults) + + for _, result := range filtered { + // Discard result if there is no snapshot + if len(result.Snapshot) == 0 { + continue + } + + dbQuery, ok := queriesDBData[result.QueryName] + if !ok { + // Means the query does not exist with such name anymore. Thus we ignore its result. + continue + } + + if dbQuery.DiscardData || dbQuery.Logging != fleet.LoggingSnapshot { + // Ignore result if query is marked as discard data or if logging is not snapshot + continue + } + + if err := svc.overwriteResultRows(ctx, result, dbQuery.ID, host.ID); err != nil { + level.Error(svc.logger).Log("msg", "overwrite results", "err", err, "query_id", dbQuery.ID, "host_id", host.ID) + continue + } + } +} + +// The "snapshot" array in a ScheduledQueryResult can contain multiple rows. Each +// row is saved as a separate ScheduledQueryResultRow. ie. a result could contain +// many USB Devices or a result could contain all User Accounts on a host. +func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.ScheduledQueryResult, queryID, hostID uint) error { + fetchTime := time.Now() + + rows := make([]*fleet.ScheduledQueryResultRow, 0, len(result.Snapshot)) + + for _, snapshotItem := range result.Snapshot { + + row := &fleet.ScheduledQueryResultRow{ + QueryID: queryID, + HostID: hostID, + Data: snapshotItem, + LastFetched: fetchTime, + } + + rows = append(rows, row) + + } + + if err := svc.ds.OverwriteQueryResultRows(ctx, rows); err != nil { + return ctxerr.Wrap(ctx, err, "overwriting query result rows") + } + + return nil +} + +// getMostRecentResults returns only the most recent result per query. +// Osquery can send multiple results for the same query (ie. if an agent loses +// network connectivity it will cache multiple results). Query Reports only +// save the most recent result for a given query. +func getMostRecentResults(results []*fleet.ScheduledQueryResult) []*fleet.ScheduledQueryResult { + // Use a map to track the most recent entry for each unique QueryName + latestResults := make(map[string]*fleet.ScheduledQueryResult) + + for _, result := range results { + if result == nil { + // This is a result that failed to unmarshal. + continue + } + if existing, ok := latestResults[result.QueryName]; ok { + // Compare the UnixTime time and update the map if the current result is more recent + if result.UnixTime > existing.UnixTime { + latestResults[result.QueryName] = result + } + } else { + latestResults[result.QueryName] = result + } + } + + // Convert the map back to a slice + var filteredResults []*fleet.ScheduledQueryResult + for _, v := range latestResults { + filteredResults = append(filteredResults, v) + } + + return filteredResults +} + +// Query names recieved from osqueryd are prefixed by teamID so we need +// to pull them out to match the query name and team ID in the database +func getQueryNameAndTeamIDFromResult(path string) (*uint, string, error) { + // For pattern: pack/Global/Name + if strings.HasPrefix(path, "pack/Global/") { + return nil, strings.TrimPrefix(path, "pack/Global/"), nil + } + + // For pattern: pack/team-/Name + if strings.HasPrefix(path, "pack/team-") { + parts := strings.SplitN(path, "/", 3) + if len(parts) != 3 { + return nil, "", fmt.Errorf("unknown format: %s", path) + } + + teamNumberStr := strings.TrimPrefix(parts[1], "team-") + teamNumberUint, err := strconv.ParseUint(teamNumberStr, 10, 32) + if err != nil { + return nil, "", fmt.Errorf("parsing team number: %w", err) + } + + teamNumber := uint(teamNumberUint) + return &teamNumber, parts[2], nil + } + + // For pattern: pack/PackName/Query (legacy pack) + if strings.HasPrefix(path, "pack/") { + parts := strings.SplitN(path, "/", 3) + if len(parts) != 3 { + return nil, "", fmt.Errorf("unknown format: %s", path) + } + return nil, parts[2], nil + } + + // If none of the above patterns match, return error + return nil, "", fmt.Errorf("unknown format: %s", path) +} diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index a0da69b815..66c5832ef0 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -65,7 +65,7 @@ func TestGetClientConfig(t *testing.T) { return []*fleet.ScheduledQuery{}, nil } } - ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { + ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) { if teamID == nil { return nil, nil } @@ -535,34 +535,294 @@ func TestSubmitResultLogs(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{}, nil + } + ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { + switch { + case teamID == nil && (name == "time" || name == "system_info" || name == "encrypted" || name == "hosts"): + return &fleet.Query{ + Name: name, + AutomationsEnabled: true, + }, nil + case teamID != nil && *teamID == 1 && name == "hosts": + return &fleet.Query{ + Name: name, + AutomationsEnabled: true, + TeamID: teamID, + }, nil + case teamID == nil && name == "query_not_automated": + return &fleet.Query{ + Name: name, + AutomationsEnabled: false, + }, nil + case teamID == nil && name == "query_should_be_saved_and_submitted": + return &fleet.Query{ + ID: 123, + Name: name, + AutomationsEnabled: true, + Logging: fleet.LoggingSnapshot, + }, nil + case teamID == nil && name == "query_should_be_saved_but_not_submitted": + return &fleet.Query{ + ID: 444, + Name: name, + AutomationsEnabled: false, + Logging: fleet.LoggingSnapshot, + }, nil + case teamID == nil && name == "query_no_rows": + return &fleet.Query{ + ID: 555, + Name: name, + AutomationsEnabled: true, + Logging: fleet.LoggingSnapshot, + }, nil + default: + return nil, newNotFoundError() + } + } + ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error { + if len(rows) == 0 { + return nil + } + switch { + case rows[0].QueryID == 123: + require.Len(t, rows, 1) + require.Equal(t, uint(999), rows[0].HostID) + require.NotZero(t, rows[0].LastFetched) + require.JSONEq(t, `{"hour":"20","minutes":"8"}`, string(rows[0].Data)) + case rows[0].QueryID == 444: + require.Len(t, rows, 2) + require.Equal(t, uint(999), rows[0].HostID) + require.NotZero(t, rows[0].LastFetched) + require.JSONEq(t, `{"hour":"20","minutes":"8"}`, string(rows[0].Data)) + require.Equal(t, uint(999), rows[1].HostID) + require.Equal(t, uint(444), rows[1].QueryID) + require.NotZero(t, rows[1].LastFetched) + require.JSONEq(t, `{"hour":"21","minutes":"9"}`, string(rows[1].Data)) + } + return nil + } + // Hack to get at the service internals and modify the writer serv := ((svc.(validationMiddleware)).Service).(*Service) testLogger := &testJSONLogger{} serv.osqueryLogWriter = &OsqueryLogger{Result: testLogger} - logs := []string{ - `{"name":"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"}`, - `{"name":"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"}`, - `{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"time","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":"1484078931","decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`, - `{"diffResults":{"removed":[{"address":"127.0.0.1","hostnames":"kl.groob.io"}],"added":""},"name":"pack\/test\/hosts","hostIdentifier":"FA01680E-98CA-5557-8F59-7716ECFEE964","calendarTime":"Sun Nov 19 00:02:08 2017 UTC","unixTime":"1511049728","epoch":"0","counter":"10","decorations":{"host_uuid":"FA01680E-98CA-5557-8F59-7716ECFEE964","hostname":"kl.groob.io"}}`, - // fleet will accept anything in the "data" field of a log request. - `{"unknown":{"foo": [] }}`, + validLogResults := []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"}`, + + `{"name":"pack/SomePack/encrypted","hostIdentifier":"some_uuid","calendarTime":"Fri Sep 30 21:19:15 2016 UTC","unixTime":"1475270355","decorations":{"host_uuid":"4740D59F-699E-5B29-960B-979AAF9BBEEB","username":"zwass"},"columns":{"encrypted":"1","name":"\/dev\/disk1","type":"AES-XTS","uid":"","user_uuid":"","uuid":"some_uuid"},"action":"added"}`, + `{"name":"pack/SomePack/encrypted","hostIdentifier":"some_uuid","calendarTime":"Fri Sep 30 21:19:14 2016 UTC","unixTime":"1475270354","decorations":{"host_uuid":"4740D59F-699E-5B29-960B-979AAF9BBEEB","username":"zwass"},"columns":{"encrypted":"1","name":"\/dev\/disk1","type":"AES-XTS","uid":"","user_uuid":"","uuid":"some_uuid"},"action":"added"}`, + + // These results belong to the same query but have 1 second difference. + `{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/Global/time","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/Global/time","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:50 2017 UTC","unixTime":1484078930,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`, + `{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/Global/time","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:52 2017 UTC","unixTime":1484078932,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`, + + `{"diffResults":{"removed":[{"address":"127.0.0.1","hostnames":"kl.groob.io"}],"added":""},"name":"pack\/team-1/hosts","hostIdentifier":"FA01680E-98CA-5557-8F59-7716ECFEE964","calendarTime":"Sun Nov 19 00:02:08 2017 UTC","unixTime":"1511049728","epoch":"0","counter":"10","decorations":{"host_uuid":"FA01680E-98CA-5557-8F59-7716ECFEE964","hostname":"kl.groob.io"}}`, + + `{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/Global/query_should_be_saved_and_submitted","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`, + + //`{"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, ",")) + logJSON := fmt.Sprintf("[%s]", strings.Join(validLogResults, ",")) + + resultWithInvalidJSON := []byte("foobar:\n\t123") + // The "name" field will be empty, so this result will be ignored. + resultWithoutName := []byte(`{"unknown":{"foo": [] }}`) + // The "name" field has invalid format, so this result will be ignored. + resultWithInvalidNameFmt1 := []byte(`{"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"}}`) + resultWithInvalidNameFmt2 := []byte(`{"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"}}`) + resultWithInvalidNameFmt3 := []byte(`{"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"}}`) + // The query doesn't exist, so this result will be ignored. + resultWithQueryDoesNotExist := []byte(`{"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"}}`) + // The query was configured with automations disabled, so this result will be ignored. + resultWithQueryNotAutomated := []byte(`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/Global/query_not_automated","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":"1484078931","decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`) + // The query is supposed to be saved but with automations disabled (and has two columns). + resultWithQuerySavedNotAutomated := []byte(`{"snapshot":[{"hour":"20","minutes":"8"},{"hour":"21","minutes":"9"}],"action":"snapshot","name":"pack/Global/query_should_be_saved_but_not_submitted","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`) var results []json.RawMessage err := json.Unmarshal([]byte(logJSON), &results) require.NoError(t, err) - host := fleet.Host{} + host := fleet.Host{ + ID: 999, + } ctx = hostctx.NewContext(ctx, &host) - err = serv.SubmitResultLogs(ctx, results) + // Submit valid and invalid log results mixed. + err = serv.SubmitResultLogs(ctx, append(append(results[:3], + resultWithInvalidJSON, + resultWithoutName, + resultWithInvalidNameFmt1, + resultWithInvalidNameFmt2, + resultWithInvalidNameFmt3, + resultWithQueryDoesNotExist, + resultWithQueryNotAutomated, + resultWithQuerySavedNotAutomated, + ), results[3:]...)) require.NoError(t, err) assert.Equal(t, results, testLogger.logs) } +func TestSaveResultLogsToQueryReports(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil) + + // Hack to get at the private methods + serv := ((svc.(validationMiddleware)).Service).(*Service) + + host := fleet.Host{} + ctx = hostctx.NewContext(ctx, &host) + + results := []*fleet.ScheduledQueryResult{ + { + QueryName: "pack/Global/Uptime", + OsqueryHostID: "1379f59d98f4", + Snapshot: []json.RawMessage{ + json.RawMessage(`{"hour":"20","minutes":"8"}`), + }, + UnixTime: 1484078931, + }, + } + + queriesDBData := map[string]*fleet.Query{ + "pack/Global/Uptime": { + ID: 1, + DiscardData: false, + Logging: fleet.LoggingSnapshot, + }, + } + + // Results not saved if query reports disabled globally + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{ServerSettings: fleet.ServerSettings{QueryReportsDisabled: true}}, nil + } + serv.saveResultLogsToQueryReports(ctx, results, queriesDBData) + assert.False(t, ds.OverwriteQueryResultRowsFuncInvoked) + + // Result not saved if result is not a snapshot + notSnapshotResult := []*fleet.ScheduledQueryResult{ + { + QueryName: "pack/Global/Uptime", + OsqueryHostID: "1379f59d98f4", + Snapshot: []json.RawMessage{}, + UnixTime: 1484078931, + }, + } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{ServerSettings: fleet.ServerSettings{QueryReportsDisabled: false}}, nil + } + serv.saveResultLogsToQueryReports(ctx, notSnapshotResult, queriesDBData) + assert.False(t, ds.OverwriteQueryResultRowsFuncInvoked) + + // Results not saved if DiscardData is true in Query + discardDataFalse := map[string]*fleet.Query{ + "pack/Global/Uptime": { + ID: 1, + DiscardData: true, + Logging: fleet.LoggingSnapshot, + }, + } + serv.saveResultLogsToQueryReports(ctx, results, discardDataFalse) + assert.False(t, ds.OverwriteQueryResultRowsFuncInvoked) + + // Happy Path: Results saved + discardDataTrue := map[string]*fleet.Query{ + "pack/Global/Uptime": { + ID: 1, + DiscardData: false, + Logging: fleet.LoggingSnapshot, + }, + } + ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error { + return nil + } + serv.saveResultLogsToQueryReports(ctx, results, discardDataTrue) + require.True(t, ds.OverwriteQueryResultRowsFuncInvoked) +} + +func TestGetQueryNameAndTeamIDFromResult(t *testing.T) { + tests := []struct { + input string + expectedID *uint + expectedName string + hasErr bool + }{ + {"pack/Global/Query Name", nil, "Query Name", false}, + {"pack/team-1/Query Name", ptr.Uint(1), "Query Name", false}, + {"pack/team-12345/Another Query", ptr.Uint(12345), "Another Query", false}, + {"pack/PackName/Query", nil, "Query", false}, // Legacy Pack support + {"pack/team-foo/Query", nil, "", true}, + {"pack/Global/QueryWith/Slash", nil, "QueryWith/Slash", false}, + {"pack/team-1/QueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false}, + {"pack/PackName/QueryWith/Slash", nil, "QueryWith/Slash", false}, // Legacy Pack support + {"InvalidString", nil, "", true}, + {"Invalid/Query", nil, "", true}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + id, str, err := getQueryNameAndTeamIDFromResult(tt.input) + assert.Equal(t, tt.expectedID, id) + assert.Equal(t, tt.expectedName, str) + if tt.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetMostRecentResults(t *testing.T) { + tests := []struct { + name string + input []*fleet.ScheduledQueryResult + expected []*fleet.ScheduledQueryResult + }{ + { + name: "basic test", + input: []*fleet.ScheduledQueryResult{ + {QueryName: "test1", UnixTime: 1}, + {QueryName: "test1", UnixTime: 2}, + {QueryName: "test1", UnixTime: 3}, + {QueryName: "test2", UnixTime: 1}, + {QueryName: "test2", UnixTime: 2}, + {QueryName: "test2", UnixTime: 3}, + }, + expected: []*fleet.ScheduledQueryResult{ + {QueryName: "test1", UnixTime: 3}, + {QueryName: "test2", UnixTime: 3}, + }, + }, + { + name: "out of order test", + input: []*fleet.ScheduledQueryResult{ + {QueryName: "test1", UnixTime: 2}, + {QueryName: "test1", UnixTime: 3}, + {QueryName: "test1", UnixTime: 1}, + {QueryName: "test2", UnixTime: 3}, + {QueryName: "test2", UnixTime: 2}, + {QueryName: "test2", UnixTime: 1}, + }, + expected: []*fleet.ScheduledQueryResult{ + {QueryName: "test1", UnixTime: 3}, + {QueryName: "test2", UnixTime: 3}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results := getMostRecentResults(tt.input) + assert.Equal(t, tt.expected, results) + }) + } +} + func verifyDiscovery(t *testing.T, queries, discovery map[string]string) { assert.Equal(t, len(queries), len(discovery)) // discoveryUsed holds the queries where we know use the distributed discovery feature. @@ -2003,7 +2263,7 @@ func TestUpdateHostIntervals(t *testing.T) { svc, ctx := newTestService(t, ds, nil, nil) - ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { + ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) { return nil, nil } diff --git a/server/service/queries.go b/server/service/queries.go index a911790e90..6304df1ee5 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -122,6 +122,61 @@ func onlyShowObserverCanRunQueries(user *fleet.User, teamID *uint) bool { })[*teamID] } +//////////////////////////////////////////////////////////////////////////////// +// Get query report +//////////////////////////////////////////////////////////////////////////////// + +type getQueryReportRequest struct { + ID uint `url:"id"` +} + +type getQueryReportResponse struct { + QueryID uint `json:"query_id"` + Results []fleet.HostQueryResultRow `json:"results"` + Err error `json:"error,omitempty"` +} + +func (r getQueryReportResponse) error() error { return r.Err } + +func getQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*getQueryReportRequest) + queryReportResults, err := svc.GetQueryReportResults(ctx, req.ID) + if err != nil { + return listQueriesResponse{Err: err}, nil + } + // Return an empty array if there are no results stored. + results := []fleet.HostQueryResultRow{} + if len(queryReportResults) > 0 { + results = queryReportResults + } + return getQueryReportResponse{ + QueryID: req.ID, + Results: results, + }, nil +} + +func (svc *Service) GetQueryReportResults(ctx context.Context, id uint) ([]fleet.HostQueryResultRow, error) { + // Load query first to get its teamID. + query, err := svc.ds.Query(ctx, id) + if err != nil { + setAuthCheckedOnPreAuthErr(ctx) + return nil, ctxerr.Wrap(ctx, err, "get query from datastore") + } + if err := svc.authz.Authorize(ctx, query, fleet.ActionRead); err != nil { + return nil, err + } + + queryReportResultRows, err := svc.ds.QueryResultRows(ctx, id) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get query report results") + } + queryReportResults, err := fleet.MapQueryReportResultsToRows(queryReportResultRows) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "map db rows to results") + } + return queryReportResults, nil +} + //////////////////////////////////////////////////////////////////////////////// // Create Query //////////////////////////////////////////////////////////////////////////////// @@ -193,6 +248,9 @@ func (svc *Service) NewQuery(ctx context.Context, p fleet.QueryPayload) (*fleet. if p.ObserverCanRun != nil { query.ObserverCanRun = *p.ObserverCanRun } + if p.DiscardData != nil { + query.DiscardData = *p.DiscardData + } logging.WithExtras(ctx, "name", query.Name, "sql", query.Query) @@ -250,6 +308,7 @@ func modifyQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Ser func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPayload) (*fleet.Query, error) { // Load query first to determine if the user can modify it. query, err := svc.ds.Query(ctx, id) + shouldDiscardQueryResults := false if err != nil { setAuthCheckedOnPreAuthErr(ctx) return nil, err @@ -271,6 +330,9 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo query.Description = *p.Description } if p.Query != nil { + if query.Query != *p.Query { + shouldDiscardQueryResults = true + } query.Query = *p.Query } if p.Interval != nil { @@ -286,15 +348,24 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo query.AutomationsEnabled = *p.AutomationsEnabled } if p.Logging != nil { + if query.Logging != *p.Logging && *p.Logging != fleet.LoggingSnapshot { + shouldDiscardQueryResults = true + } query.Logging = *p.Logging } if p.ObserverCanRun != nil { query.ObserverCanRun = *p.ObserverCanRun } + if p.DiscardData != nil { + if *p.DiscardData && *p.DiscardData != query.DiscardData { + shouldDiscardQueryResults = true + } + query.DiscardData = *p.DiscardData + } logging.WithExtras(ctx, "name", query.Name, "sql", query.Query) - if err := svc.ds.SaveQuery(ctx, query); err != nil { + if err := svc.ds.SaveQuery(ctx, query, shouldDiscardQueryResults); err != nil { return nil, err } @@ -518,11 +589,32 @@ func (svc *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpe } } // 3. Apply the queries. + + // first, find out if we should delete query results + queriesToDiscardResults := make(map[uint]bool) + for _, query := range queries { + dbQuery, err := svc.ds.QueryByName(ctx, query.TeamID, query.Name) + if err != nil && !fleet.IsNotFound(err) { + return ctxerr.Wrap(ctx, err, "fetching saved query") + } + + if dbQuery == nil { + // then we're creating a new query, so move on. + continue + } + + if (query.DiscardData && query.DiscardData != dbQuery.DiscardData) || + (query.Logging != dbQuery.Logging && query.Logging != fleet.LoggingSnapshot) || + query.Query != dbQuery.Query { + queriesToDiscardResults[dbQuery.ID] = true + } + } + vc, ok := viewer.FromContext(ctx) if !ok { return ctxerr.New(ctx, "user must be authenticated to apply queries") } - err := svc.ds.ApplyQueries(ctx, vc.UserID(), queries) + err := svc.ds.ApplyQueries(ctx, vc.UserID(), queries, queriesToDiscardResults) if err != nil { return ctxerr.Wrap(ctx, err, "applying queries") } @@ -560,6 +652,7 @@ func (svc *Service) queryFromSpec(ctx context.Context, spec *fleet.QuerySpec) (* MinOsqueryVersion: spec.MinOsqueryVersion, AutomationsEnabled: spec.AutomationsEnabled, Logging: spec.Logging, + DiscardData: spec.DiscardData, }, nil } @@ -630,6 +723,7 @@ func (svc *Service) specFromQuery(ctx context.Context, query *fleet.Query) (*fle MinOsqueryVersion: query.MinOsqueryVersion, AutomationsEnabled: query.AutomationsEnabled, Logging: query.Logging, + DiscardData: query.DiscardData, }, nil } diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 95b4ab764c..02e1d6c447 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -239,7 +239,7 @@ func TestQueryPayloadValidationModify(t *testing.T) { ObserverCanRun: false, }, nil } - ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query) error { + ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query, shouldDiscardResults bool) error { assert.NotEmpty(t, query) return nil } @@ -250,6 +250,7 @@ func TestQueryPayloadValidationModify(t *testing.T) { assert.NotEmpty(t, act.Name) return nil } + svc, ctx := newTestService(t, ds, nil, nil) testCases := []struct { @@ -446,7 +447,7 @@ func TestQueryAuth(t *testing.T) { } return nil, newNotFoundError() } - ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query) error { + ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query, shouldDiscardResults bool) error { return nil } ds.DeleteQueryFunc = func(ctx context.Context, teamID *uint, name string) error { @@ -458,7 +459,7 @@ func TestQueryAuth(t *testing.T) { ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } - ds.ApplyQueriesFunc = func(ctx context.Context, authID uint, queries []*fleet.Query) error { + ds.ApplyQueriesFunc = func(ctx context.Context, authID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]bool) error { return nil } diff --git a/server/service/team_schedule_test.go b/server/service/team_schedule_test.go index ab966e5be6..41fa628ae4 100644 --- a/server/service/team_schedule_test.go +++ b/server/service/team_schedule_test.go @@ -33,7 +33,7 @@ func TestTeamScheduleAuth(t *testing.T) { TeamID: nil, }, nil } - ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query) error { + ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query, shouldDiscardResults bool) error { return nil } ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {