diff --git a/changes/12636-merge-schedule-into-queries b/changes/12636-merge-schedule-into-queries new file mode 100644 index 0000000000..bea20bbdda --- /dev/null +++ b/changes/12636-merge-schedule-into-queries @@ -0,0 +1 @@ +- Merged all functionality of the Schedule page into the Queries page diff --git a/changes/12644-include-scheduled-queries-in-getclientconfig b/changes/12644-include-scheduled-queries-in-getclientconfig new file mode 100644 index 0000000000..f572537098 --- /dev/null +++ b/changes/12644-include-scheduled-queries-in-getclientconfig @@ -0,0 +1,2 @@ +- The `osquery/config` endpoint should include scheduled queries for the host's team stored in the + `queries` table. diff --git a/changes/12645-manage-query-automations b/changes/12645-manage-query-automations new file mode 100644 index 0000000000..0df3de75f3 --- /dev/null +++ b/changes/12645-manage-query-automations @@ -0,0 +1 @@ +- Users able to manage schedulable queries (new feature) with automations modal diff --git a/changes/12646-new-query-editor b/changes/12646-new-query-editor new file mode 100644 index 0000000000..45a8b4427c --- /dev/null +++ b/changes/12646-new-query-editor @@ -0,0 +1 @@ +- Query editor includes frequency and other advanced options diff --git a/changes/12646-update-save-query-modal b/changes/12646-update-save-query-modal new file mode 100644 index 0000000000..8c136872ea --- /dev/null +++ b/changes/12646-update-save-query-modal @@ -0,0 +1 @@ +- Update the save query modal to include scheduling-related fields. diff --git a/changes/12999-platforms-column b/changes/12999-platforms-column new file mode 100644 index 0000000000..3affe1d53a --- /dev/null +++ b/changes/12999-platforms-column @@ -0,0 +1 @@ +* Update the "Platforms" column to the more explicit "Compatible with" diff --git a/changes/7765-combine-schedules-and-queries b/changes/7765-combine-schedules-and-queries new file mode 100644 index 0000000000..59b8a029ba --- /dev/null +++ b/changes/7765-combine-schedules-and-queries @@ -0,0 +1 @@ +- Combine the query and schedule features to provide a single interface for creating, scheduling, and tweaking queries at the global and team level. diff --git a/changes/7765-queries-schedules-schema-updates b/changes/7765-queries-schedules-schema-updates new file mode 100644 index 0000000000..9bc0290432 --- /dev/null +++ b/changes/7765-queries-schedules-schema-updates @@ -0,0 +1 @@ +- Updated 'queries' table schema to allow storing scheduling information and configuration in the 'queries' table. diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 363ea3b17d..c8beee8293 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -779,12 +779,6 @@ func newCleanupsAndAggregationSchedule( return ds.UpdateQueryAggregatedStats(ctx) }, ), - schedule.WithJob( - "scheduled_query_aggregated_stats", - func(ctx context.Context) error { - return ds.UpdateScheduledQueryAggregatedStats(ctx) - }, - ), schedule.WithJob( "aggregated_munki_and_mdm", func(ctx context.Context) error { diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 7ee7e3ad33..37222f9cfa 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -62,8 +62,8 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" - _ "go.elastic.co/apm/module/apmsql" - _ "go.elastic.co/apm/module/apmsql/mysql" + _ "go.elastic.co/apm/module/apmsql/v2" + _ "go.elastic.co/apm/module/apmsql/v2/mysql" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 0fa64c0c00..c2175f87d6 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -1122,7 +1122,7 @@ spec: // Apply queries. var appliedQueries []*fleet.Query - ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { + ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { return nil, sql.ErrNoRows } ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error { @@ -1223,7 +1223,7 @@ func TestApplyQueries(t *testing.T) { _, ds := runServerWithMockedDS(t) var appliedQueries []*fleet.Query - ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { + ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { return nil, sql.ErrNoRows } ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error { diff --git a/cmd/fleetctl/convert_test.go b/cmd/fleetctl/convert_test.go index 655911e7c7..6bfd0a4d2c 100644 --- a/cmd/fleetctl/convert_test.go +++ b/cmd/fleetctl/convert_test.go @@ -64,5 +64,5 @@ func TestConvertFileStdout(t *testing.T) { os.Stdout = oldStdout w.Close() out, _ := ioutil.ReadAll(r) - require.Equal(t, expected, out) + require.Equal(t, string(expected), string(out)) } diff --git a/cmd/fleetctl/delete_test.go b/cmd/fleetctl/delete_test.go index ec0963ef6e..301b05d6c1 100644 --- a/cmd/fleetctl/delete_test.go +++ b/cmd/fleetctl/delete_test.go @@ -78,11 +78,11 @@ func TestDeleteQuery(t *testing.T) { _, ds := runServerWithMockedDS(t) var deletedQuery string - ds.DeleteQueryFunc = func(ctx context.Context, name string) error { + ds.DeleteQueryFunc = func(ctx context.Context, teamID *uint, name string) error { deletedQuery = name return nil } - ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { + ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { if name != "query1" { return nil, nil } diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index af6f9aed59..4700715349 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -102,7 +102,7 @@ func printLabel(c *cli.Context, label *fleet.LabelSpec) error { return printSpec(c, spec) } -func printQuery(c *cli.Context, query *fleet.QuerySpec) error { +func printQuerySpec(c *cli.Context, query *fleet.QuerySpec) error { spec := specGeneric{ Kind: fleet.QueryKind, Version: fleet.ApiVersion, @@ -298,12 +298,74 @@ func getCommand() *cli.Command { } } +func queryToTableRow(query fleet.Query, teamName string) []string { + platform := "all" + if query.Platform != "" { + platform = query.Platform + } + + minOsqueryVersion := "all" + if query.MinOsqueryVersion != "" { + minOsqueryVersion = query.MinOsqueryVersion + } + + scheduleInfo := fmt.Sprintf("interval: %d\nplatform: %s\nmin_osquery_version: %s\nautomations_enabled: %t\nlogging: %s", + query.Interval, + platform, + minOsqueryVersion, + query.AutomationsEnabled, + query.Logging, + ) + + teamNameOut := teamName + if teamName == "" { + teamNameOut = "All teams" + } + + return []string{ + query.Name, + query.Description, + query.Query, + teamNameOut, + scheduleInfo, + } +} + +func printInheritedQueriesMsg(client *service.Client, teamID *uint) error { + if teamID != nil { + globalQueries, err := client.GetQueries(nil) + if err != nil { + return fmt.Errorf("could not list global queries: %w", err) + } + + if len(globalQueries) > 0 { + fmt.Printf("Not showing %d inherited queries. To see global queries, run this command without the `--team` flag.\n", len(globalQueries)) + } + return nil + } + + return nil +} + +func printNoQueriesFoundMsg(teamID *uint) { + if teamID != nil { + fmt.Println("No team queries found.") + return + } + fmt.Println("No global queries found.") + fmt.Println("To see team queries, run this command with the --team flag.") +} + func getQueriesCommand() *cli.Command { return &cli.Command{ Name: "queries", Aliases: []string{"query", "q"}, Usage: "List information about one or more queries", Flags: []cli.Flag{ + &cli.UintFlag{ + Name: teamFlagName, + Usage: "filter queries by team_id (0 means global)", + }, jsonFlag(), yamlFlag(), configFlag(), @@ -318,9 +380,27 @@ func getQueriesCommand() *cli.Command { name := c.Args().First() - // if name wasn't provided, list all queries + var teamID *uint + var teamName string + + if tid := c.Uint(teamFlagName); tid != 0 { + teamID = &tid + team, err := client.GetTeam(*teamID) + if err != nil { + var notFoundErr service.NotFoundErr + if errors.As(err, ¬FoundErr) { + // Do not error out, just inform the user and 'gracefully' exit. + fmt.Println("Team not found.") + return nil + } + return fmt.Errorf("get team: %w", err) + } + teamName = team.Name + } + + // if name wasn't provided, list either all global queries or all team queries... if name == "" { - queries, err := client.GetQueries() + queries, err := client.GetQueries(teamID) if err != nil { return fmt.Errorf("could not list queries: %w", err) } @@ -350,44 +430,54 @@ func getQueriesCommand() *cli.Command { } if len(queries) == 0 { - fmt.Println("No queries found") + printNoQueriesFoundMsg(teamID) + if err := printInheritedQueriesMsg(client, teamID); err != nil { + return err + } return nil } if c.Bool(yamlFlagName) || c.Bool(jsonFlagName) { for _, query := range queries { - if err := printQuery(c, &fleet.QuerySpec{ + if err := printQuerySpec(c, &fleet.QuerySpec{ Name: query.Name, Description: query.Description, Query: query.Query, + + TeamName: teamName, + Interval: query.Interval, + ObserverCanRun: query.ObserverCanRun, + Platform: query.Platform, + MinOsqueryVersion: query.MinOsqueryVersion, + AutomationsEnabled: query.AutomationsEnabled, + Logging: query.Logging, }); err != nil { return fmt.Errorf("unable to print query: %w", err) } } } else { // Default to printing as a table - data := [][]string{} + rows := [][]string{} + columns := []string{"name", "description", "query", "team", "schedule"} for _, query := range queries { - data = append(data, []string{ - query.Name, - query.Description, - query.Query, - }) + rows = append(rows, queryToTableRow(query, teamName)) } - columns := []string{"name", "description", "query"} - printTable(c, columns, data) + printQueryTable(c, columns, rows) + if err := printInheritedQueriesMsg(client, teamID); err != nil { + return err + } } return nil } - query, err := client.GetQuery(name) + query, err := client.GetQuerySpec(teamID, name) if err != nil { return err } - if err := printQuery(c, query); err != nil { + if err := printQuerySpec(c, query); err != nil { return fmt.Errorf("unable to print query: %w", err) } @@ -426,7 +516,7 @@ func getPacksCommand() *cli.Command { Flags: []cli.Flag{ &cli.BoolFlag{ Name: withQueriesFlagName, - Usage: "Output queries included in pack(s) too", + Usage: "Output queries included in pack(s) too, when used alongside --yaml or --json", }, jsonFlag(), yamlFlag(), @@ -457,7 +547,8 @@ func getPacksCommand() *cli.Command { return nil } - queries, err := client.GetQueries() + // Get global queries (teamID==nil), because 2017 packs reference global queries. + queries, err := client.GetQueries(nil) if err != nil { return fmt.Errorf("could not list queries: %w", err) } @@ -469,7 +560,7 @@ func getPacksCommand() *cli.Command { continue } - if err := printQuery(c, &fleet.QuerySpec{ + if err := printQuerySpec(c, &fleet.QuerySpec{ Name: query.Name, Description: query.Description, Query: query.Query, @@ -493,10 +584,8 @@ func getPacksCommand() *cli.Command { if err := printPack(c, pack); err != nil { return fmt.Errorf("unable to print pack: %w", err) } - addQueries(pack) } - return printQueries() } @@ -994,6 +1083,14 @@ func getUserRolesCommand() *cli.Command { } } +func printQueryTable(c *cli.Context, columns []string, data [][]string) { + table := defaultTable(c.App.Writer) + table.SetHeader(columns) + table.SetReflowDuringAutoWrap(false) + table.AppendBulk(data) + table.Render() +} + func printTable(c *cli.Context, columns []string, data [][]string) { table := defaultTable(c.App.Writer) table.SetHeader(columns) diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index fc8d4fa76c..5f0f7a3a68 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -972,92 +973,313 @@ spec: } func TestGetQueries(t *testing.T) { - _, ds := runServerWithMockedDS(t) + _, ds := runServerWithMockedDS(t, &service.TestServerOpts{ + License: &fleet.LicenseInfo{ + Tier: fleet.TierPremium, + Expiration: time.Now().Add(24 * time.Hour), + }, + }) - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { - return []*fleet.Query{ + ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) { + return []*fleet.TeamSummary{ { - ID: 33, - Name: "query1", - Description: "some desc", - Query: "select 1;", - Saved: false, - ObserverCanRun: false, - }, - { - ID: 12, - Name: "query2", - Description: "some desc 2", - Query: "select 2;", - Saved: true, - ObserverCanRun: false, + ID: 1, + Name: "Foobar", }, }, nil } + ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { + if tid == 1 { + return &fleet.Team{ + ID: tid, + Name: "Foobar", + }, nil + } + return nil, ¬FoundError{} + } + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { + if opt.TeamID == nil { + return []*fleet.Query{ + { + ID: 33, + Name: "query1", + Description: "some desc", + Query: "select 1;", + Saved: true, // ListQueries always returns the saved ones. + ObserverCanRun: false, + }, + { + ID: 12, + Name: "query2", + Description: "some desc 2", + Query: "select 2;", + Saved: true, // ListQueries always returns the saved ones. + ObserverCanRun: false, + }, + { + ID: 14, + Name: "query4", + Description: "some desc 4", + Query: "select 4;", + Interval: 60, + AutomationsEnabled: true, + MinOsqueryVersion: "5.3.0", + Platform: "darwin,windows", + Logging: "differential_ignore_removals", + Saved: true, // ListQueries always returns the saved ones. + ObserverCanRun: true, + }, + }, nil + } else if *opt.TeamID == 1 { + return []*fleet.Query{ + { + ID: 13, + Name: "query3", + Description: "some desc 3", + Query: "select 3;", + Interval: 3600, + AutomationsEnabled: false, + MinOsqueryVersion: "5.4.0", + Platform: "darwin", + Logging: "snapshot", + Saved: true, // ListQueries always returns the saved ones. + TeamID: ptr.Uint(1), + ObserverCanRun: true, + }, + }, nil + } else if *opt.TeamID == 2 { + return []*fleet.Query{}, nil + } + return nil, errors.New("invalid team ID") + } - expected := `+--------+-------------+-----------+ -| NAME | DESCRIPTION | QUERY | -+--------+-------------+-----------+ -| query1 | some desc | select 1; | -+--------+-------------+-----------+ -| query2 | some desc 2 | select 2; | -+--------+-------------+-----------+ + expectedGlobal := `+--------+-------------+-----------+-----------+--------------------------------+ +| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE | ++--------+-------------+-----------+-----------+--------------------------------+ +| query1 | some desc | select 1; | All teams | interval: 0 | +| | | | | | +| | | | | platform: all | +| | | | | | +| | | | | min_osquery_version: all | +| | | | | | +| | | | | automations_enabled: false | +| | | | | | +| | | | | logging: | ++--------+-------------+-----------+-----------+--------------------------------+ +| query2 | some desc 2 | select 2; | All teams | interval: 0 | +| | | | | | +| | | | | platform: all | +| | | | | | +| | | | | min_osquery_version: all | +| | | | | | +| | | | | automations_enabled: false | +| | | | | | +| | | | | logging: | ++--------+-------------+-----------+-----------+--------------------------------+ +| query4 | some desc 4 | select 4; | All teams | interval: 60 | +| | | | | | +| | | | | platform: darwin,windows | +| | | | | | +| | | | | min_osquery_version: 5.3.0 | +| | | | | | +| | | | | automations_enabled: true | +| | | | | | +| | | | | logging: | +| | | | | differential_ignore_removals | ++--------+-------------+-----------+-----------+--------------------------------+ ` - expectedYaml := `--- + + expectedYAMLGlobal := `--- apiVersion: v1 kind: query spec: + automations_enabled: false description: some desc + interval: 0 + logging: "" + min_osquery_version: "" name: query1 + observer_can_run: false + platform: "" query: select 1; + team: "" --- apiVersion: v1 kind: query spec: + automations_enabled: false description: some desc 2 + interval: 0 + logging: "" + min_osquery_version: "" name: query2 + observer_can_run: false + platform: "" query: select 2; + team: "" +--- +apiVersion: v1 +kind: query +spec: + automations_enabled: true + description: some desc 4 + interval: 60 + logging: differential_ignore_removals + min_osquery_version: 5.3.0 + name: query4 + observer_can_run: true + platform: darwin,windows + query: select 4; + team: "" ` - expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;"}} -{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;"}} + 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"}} ` - assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"})) - assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "queries", "--yaml"})) - assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "queries", "--json"})) + expectedTeam := `+--------+-------------+-----------+--------+----------------------------+ +| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE | ++--------+-------------+-----------+--------+----------------------------+ +| query3 | some desc 3 | select 3; | Foobar | interval: 3600 | +| | | | | | +| | | | | platform: darwin | +| | | | | | +| | | | | min_osquery_version: 5.4.0 | +| | | | | | +| | | | | automations_enabled: false | +| | | | | | +| | | | | logging: snapshot | ++--------+-------------+-----------+--------+----------------------------+ +` + + expectedYAMLTeam := `--- +apiVersion: v1 +kind: query +spec: + automations_enabled: false + description: some desc 3 + interval: 3600 + logging: snapshot + min_osquery_version: 5.4.0 + name: query3 + observer_can_run: true + platform: darwin + 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"}} +` + + assert.Equal(t, expectedGlobal, runAppForTest(t, []string{"get", "queries"})) + assert.Equal(t, expectedYAMLGlobal, runAppForTest(t, []string{"get", "queries", "--yaml"})) + assert.Equal(t, expectedJSONGlobal, runAppForTest(t, []string{"get", "queries", "--json"})) + + assert.Equal(t, expectedTeam, runAppForTest(t, []string{"get", "queries", "--team", "1"})) + assert.Equal(t, expectedYAMLTeam, runAppForTest(t, []string{"get", "queries", "--yaml", "--team", "1"})) + assert.Equal(t, expectedJSONTeam, runAppForTest(t, []string{"get", "queries", "--json", "--team", "1"})) + + assert.Equal(t, "", runAppForTest(t, []string{"get", "queries", "--team", "2"})) + assert.Equal(t, "", runAppForTest(t, []string{"get", "queries", "--yaml", "--team", "2"})) + assert.Equal(t, "", runAppForTest(t, []string{"get", "queries", "--json", "--team", "2"})) } func TestGetQuery(t *testing.T) { - _, ds := runServerWithMockedDS(t) + _, ds := runServerWithMockedDS(t, &service.TestServerOpts{ + License: &fleet.LicenseInfo{ + Tier: fleet.TierPremium, + Expiration: time.Now().Add(24 * time.Hour), + }, + }) - ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { - if name != "query1" { - return nil, nil + ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { + if tid == 1 { + return &fleet.Team{ + ID: tid, + Name: "Foobar", + }, nil + } + return nil, ¬FoundError{} + } + ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { + if teamID == nil { + if name != "globalQuery1" { + return nil, ¬FoundError{} + } + return &fleet.Query{ + ID: 33, + Name: "globalQuery1", + Description: "some desc", + Query: "select 1;", + Saved: true, + ObserverCanRun: false, + }, nil + } else if *teamID == 1 { + if name != "teamQuery1" { + return nil, ¬FoundError{} + } + return &fleet.Query{ + ID: 34, + Name: "teamQuery1", + Description: "some team desc", + Query: "select 2;", + Saved: true, + ObserverCanRun: true, + TeamID: teamID, + + Interval: 3600, + AutomationsEnabled: true, + MinOsqueryVersion: "5.2.0", + Platform: "linux", + Logging: "differential", + }, nil + } else { + return nil, ¬FoundError{} } - return &fleet.Query{ - ID: 33, - Name: "query1", - Description: "some desc", - Query: "select 1;", - Saved: false, - ObserverCanRun: false, - }, nil } expectedYaml := `--- apiVersion: v1 kind: query spec: + automations_enabled: false description: some desc - name: query1 + interval: 0 + logging: "" + min_osquery_version: "" + name: globalQuery1 + observer_can_run: false + platform: "" query: select 1; + team: "" ` - expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;"}} + 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":""}} ` - assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "query1"})) - assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--yaml", "query1"})) - assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "query", "--json", "query1"})) + assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "globalQuery1"})) + assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--yaml", "globalQuery1"})) + assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "query", "--json", "globalQuery1"})) + + expectedYaml = `--- +apiVersion: v1 +kind: query +spec: + automations_enabled: true + description: some team desc + interval: 3600 + logging: differential + min_osquery_version: 5.2.0 + name: teamQuery1 + observer_can_run: true + platform: linux + 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"}} +` + + assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--team", "1", "teamQuery1"})) + assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--yaml", "--team", "1", "teamQuery1"})) + assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "query", "--json", "--team", "1", "teamQuery1"})) } // TestGetQueriesAsObservers tests that when observers run `fleectl get queries` they @@ -1157,21 +1379,36 @@ func TestGetQueriesAsObserver(t *testing.T) { t.Run(tc.name, func(t *testing.T) { setCurrentUserSession(tc.user) - expected := `+--------+-------------+-----------+ -| NAME | DESCRIPTION | QUERY | -+--------+-------------+-----------+ -| query2 | some desc 2 | select 2; | -+--------+-------------+-----------+ + expected := `+--------+-------------+-----------+-----------+----------------------------+ +| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE | ++--------+-------------+-----------+-----------+----------------------------+ +| query2 | some desc 2 | select 2; | All teams | interval: 0 | +| | | | | | +| | | | | platform: all | +| | | | | | +| | | | | min_osquery_version: all | +| | | | | | +| | | | | automations_enabled: false | +| | | | | | +| | | | | logging: | ++--------+-------------+-----------+-----------+----------------------------+ ` expectedYaml := `--- apiVersion: v1 kind: query spec: + automations_enabled: false description: some desc 2 + interval: 0 + logging: "" + min_osquery_version: "" name: query2 + observer_can_run: true + platform: "" query: select 2; + team: "" ` - expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;"}} + 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":""}} ` assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"})) @@ -1199,41 +1436,86 @@ spec: }, }) - expected := `+--------+-------------+-----------+ -| NAME | DESCRIPTION | QUERY | -+--------+-------------+-----------+ -| query1 | some desc | select 1; | -+--------+-------------+-----------+ -| query2 | some desc 2 | select 2; | -+--------+-------------+-----------+ -| query3 | some desc 3 | select 3; | -+--------+-------------+-----------+ + expected := `+--------+-------------+-----------+-----------+----------------------------+ +| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE | ++--------+-------------+-----------+-----------+----------------------------+ +| query1 | some desc | select 1; | All teams | interval: 0 | +| | | | | | +| | | | | platform: all | +| | | | | | +| | | | | min_osquery_version: all | +| | | | | | +| | | | | automations_enabled: false | +| | | | | | +| | | | | logging: | ++--------+-------------+-----------+-----------+----------------------------+ +| query2 | some desc 2 | select 2; | All teams | interval: 0 | +| | | | | | +| | | | | platform: all | +| | | | | | +| | | | | min_osquery_version: all | +| | | | | | +| | | | | automations_enabled: false | +| | | | | | +| | | | | logging: | ++--------+-------------+-----------+-----------+----------------------------+ +| query3 | some desc 3 | select 3; | All teams | interval: 0 | +| | | | | | +| | | | | platform: all | +| | | | | | +| | | | | min_osquery_version: all | +| | | | | | +| | | | | automations_enabled: false | +| | | | | | +| | | | | logging: | ++--------+-------------+-----------+-----------+----------------------------+ ` expectedYaml := `--- apiVersion: v1 kind: query spec: + automations_enabled: false description: some desc + interval: 0 + logging: "" + min_osquery_version: "" name: query1 + observer_can_run: false + platform: "" query: select 1; + team: "" --- apiVersion: v1 kind: query spec: + automations_enabled: false description: some desc 2 + interval: 0 + logging: "" + min_osquery_version: "" name: query2 + observer_can_run: true + platform: "" query: select 2; + team: "" --- apiVersion: v1 kind: query spec: + automations_enabled: false description: some desc 3 + interval: 0 + logging: "" + min_osquery_version: "" name: query3 + observer_can_run: false + platform: "" query: select 3; + team: "" ` - expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;"}} -{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;"}} -{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;"}} + 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":""}} ` assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"})) @@ -1288,13 +1570,29 @@ spec: }, }, nil } - expected = `+--------+-------------+-----------+ -| NAME | DESCRIPTION | QUERY | -+--------+-------------+-----------+ -| query1 | some desc | select 1; | -+--------+-------------+-----------+ -| query2 | some desc 2 | select 2; | -+--------+-------------+-----------+ + expected = `+--------+-------------+-----------+-----------+----------------------------+ +| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE | ++--------+-------------+-----------+-----------+----------------------------+ +| query1 | some desc | select 1; | All teams | interval: 0 | +| | | | | | +| | | | | platform: all | +| | | | | | +| | | | | min_osquery_version: all | +| | | | | | +| | | | | automations_enabled: false | +| | | | | | +| | | | | logging: | ++--------+-------------+-----------+-----------+----------------------------+ +| query2 | some desc 2 | select 2; | All teams | interval: 0 | +| | | | | | +| | | | | platform: all | +| | | | | | +| | | | | min_osquery_version: all | +| | | | | | +| | | | | automations_enabled: false | +| | | | | | +| | | | | logging: | ++--------+-------------+-----------+-----------+----------------------------+ ` assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"})) } diff --git a/cmd/fleetctl/query.go b/cmd/fleetctl/query.go index 368728834a..56cbe9ebaa 100644 --- a/cmd/fleetctl/query.go +++ b/cmd/fleetctl/query.go @@ -75,6 +75,10 @@ func queryCommand() *cli.Command { Destination: &flTimeout, Usage: "How long to run query before exiting (10s, 1h, etc.)", }, + &cli.UintFlag{ + Name: teamFlagName, + Usage: "ID of the team where the named query belongs to (0 means global)", + }, configFlag(), contextFlag(), debugFlag(), @@ -94,7 +98,11 @@ func queryCommand() *cli.Command { } if flQueryName != "" { - q, err := fleet.GetQuery(flQueryName) + var teamID *uint + if tid := c.Uint(teamFlagName); tid != 0 { + teamID = &tid + } + q, err := fleet.GetQuerySpec(teamID, flQueryName) if err != nil { return fmt.Errorf("Query '%s' not found", flQueryName) } diff --git a/cmd/fleetctl/testdata/convert_output.yml b/cmd/fleetctl/testdata/convert_output.yml index 233584961a..a360959840 100644 --- a/cmd/fleetctl/testdata/convert_output.yml +++ b/cmd/fleetctl/testdata/convert_output.yml @@ -44,35 +44,70 @@ spec: apiVersion: v1 kind: query spec: + automations_enabled: false description: Retrieves the list of application scheme/protocol-based IPC handlers. + interval: 0 + logging: "" + min_osquery_version: "" name: app_schemes + observer_can_run: false + platform: "" query: select * from app_schemes; + team: "" --- apiVersion: v1 kind: query spec: + automations_enabled: false description: Retrieves the current disk encryption status for the target system. + interval: 0 + logging: "" + min_osquery_version: "" name: disk_encryption + observer_can_run: false + platform: "" query: select * from disk_encryption; + team: "" --- apiVersion: v1 kind: query spec: + automations_enabled: false description: Retrieves the current filters and chains per filter in the target system. + interval: 0 + logging: "" + min_osquery_version: "" name: iptables + observer_can_run: false + platform: "" query: select * from iptables; + team: "" --- apiVersion: v1 kind: query spec: + automations_enabled: false description: Retrieves all the daemons that will run in the start of the target OSX system. + interval: 0 + logging: "" + min_osquery_version: "" name: launchd + observer_can_run: false + platform: "" query: select * from launchd; + team: "" --- apiVersion: v1 kind: query spec: + automations_enabled: false description: Lists the application bundle that owns a sandbox label. + interval: 0 + logging: "" + min_osquery_version: "" name: sandboxes + observer_can_run: false + platform: "" query: select * from sandboxes; + team: "" diff --git a/frontend/__mocks__/scheduleableQueryMock.ts b/frontend/__mocks__/scheduleableQueryMock.ts new file mode 100644 index 0000000000..edc82a61da --- /dev/null +++ b/frontend/__mocks__/scheduleableQueryMock.ts @@ -0,0 +1,39 @@ +// "SchedulableQuery" to be used in developing frontend for #7765 + +import { ISchedulableQuery } from "interfaces/schedulable_query"; + +const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 1, + name: "Test Query", + description: "A test query", + query: "SELECT * FROM users", + team_id: null, + interval: 43200, // Every 12 hours + platform: "darwin,windows,linux", + min_osquery_version: "", + automations_enabled: true, + logging: "snapshot", + saved: true, + author_id: 1, + author_name: "Test User", + author_email: "test@example.com", + observer_can_run: false, + packs: [], + stats: { + system_time_p50: 28.1053, + system_time_p95: 397.6667, + user_time_p50: 29.9412, + user_time_p95: 251.4615, + total_executions: 5746, + }, +}; + +const createMockSchedulableQuery = ( + overrides?: Partial +): ISchedulableQuery => { + return { ...DEFAULT_SCHEDULABLE_QUERY_MOCK, ...overrides }; +}; + +export default createMockSchedulableQuery; diff --git a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx new file mode 100644 index 0000000000..a39903f806 --- /dev/null +++ b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import LogDestinationIndicator from "./LogDestinationIndicator"; + +const meta: Meta = { + title: "Components/LogDestinationIndicator", + component: LogDestinationIndicator, + args: { + logDestination: "filesystem", + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx new file mode 100644 index 0000000000..82de7b5d0b --- /dev/null +++ b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import classnames from "classnames"; +import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper"; +import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; + +interface ILogDestinationIndicatorProps { + logDestination: string; +} + +const generateClassTag = (rawValue: string): string => { + if (rawValue === DEFAULT_EMPTY_CELL_VALUE) { + return "indeterminate"; + } + return rawValue.replace(" ", "-").toLowerCase(); +}; + +const LogDestinationIndicator = ({ + logDestination, +}: ILogDestinationIndicatorProps): JSX.Element => { + const classTag = generateClassTag(logDestination); + const statusClassName = classnames( + "log-destination-indicator", + `log-destination-indicator--${classTag}`, + `log-destination--${classTag}` + ); + const readableLogDestination = () => { + switch (logDestination) { + case "filesystem": + return "Filesystem"; + case "firehose": + return "Amazon Kinesis Data Firehose"; + case "kinesis": + return "Amazon Kinesis Data Streams"; + case "lambda": + return "AWS Lambda"; + case "pubsub": + return "Google Cloud Pub/Sub"; + case "kafta": + return "Apache Kafka"; + case "stdout": + return "Standard output (stdout)"; + case "": + return "Not configured"; + default: + return logDestination; + } + }; + + const tooltipText = () => { + switch (logDestination) { + case "filesystem": + return `Each time a query runs, the data is sent to
+ /var/log/osquery/osqueryd.snapshots.log
+ in each host's filesystem.`; + case "firehose": + return `Each time a query runs, the data is sent to
+ Amazon Kinesis Data Firehose.`; + case "kinesis": + return `Each time a query runs, the data is sent to
+ Amazon Kinesis Data Streams.`; + case "lambda": + return ` + Each time a query runs, the data
is sent to AWS Lambda. + `; + case "pubsub": + return `Each time a query runs, the data is
sent to Google Cloud Pub/Sub.`; + case "kafta": + return `Each time a query runs, the data
is sent to Apache Kafka.`; + case "stdout": + return `Each time a query runs, the data is sent to
+ standard output (stdout) on the Fleet server.`; + case "": + return "Please configure a log destination."; + default: + return "No additional information is available about this log destination."; + } + }; + + return ( + + {readableLogDestination()} + + ); +}; + +export default LogDestinationIndicator; diff --git a/frontend/components/LogDestinationIndicator/index.ts b/frontend/components/LogDestinationIndicator/index.ts new file mode 100644 index 0000000000..1d2d5a12d4 --- /dev/null +++ b/frontend/components/LogDestinationIndicator/index.ts @@ -0,0 +1 @@ +export { default } from "./LogDestinationIndicator"; diff --git a/frontend/components/Modal/Modal.tsx b/frontend/components/Modal/Modal.tsx index 4044852d82..0ec2c2149f 100644 --- a/frontend/components/Modal/Modal.tsx +++ b/frontend/components/Modal/Modal.tsx @@ -11,6 +11,7 @@ export interface IModalProps { children: JSX.Element; onExit: () => void; onEnter?: () => void; + /** default 650px, large 800px, xlarge 850px, auto auto-width */ width?: ModalWidth; className?: string; } diff --git a/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx b/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx index 2d4ad7516a..f278db6f08 100644 --- a/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx +++ b/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx @@ -1,13 +1,13 @@ import React from "react"; -import { IOsqueryPlatform } from "interfaces/platform"; +import { OsqueryPlatform } from "interfaces/platform"; import { PLATFORM_DISPLAY_NAMES } from "utilities/constants"; import TooltipWrapper from "components/TooltipWrapper"; import Icon from "components/Icon"; interface IPlatformCompatibilityProps { - compatiblePlatforms: IOsqueryPlatform[] | null; + compatiblePlatforms: OsqueryPlatform[] | null; error: Error | null; } @@ -18,13 +18,13 @@ const DISPLAY_ORDER = [ "Windows", "Linux", "ChromeOS", -] as IOsqueryPlatform[]; +] as OsqueryPlatform[]; const ERROR_NO_COMPATIBLE_TABLES = Error("no tables in query"); const formatPlatformsForDisplay = ( - compatiblePlatforms: IOsqueryPlatform[] -): IOsqueryPlatform[] => { + compatiblePlatforms: OsqueryPlatform[] +): OsqueryPlatform[] => { return compatiblePlatforms.map((str) => PLATFORM_DISPLAY_NAMES[str] || str); }; diff --git a/frontend/components/PlatformCompatibility/_styles.scss b/frontend/components/PlatformCompatibility/_styles.scss index 5a461d45e3..38b20a68a1 100644 --- a/frontend/components/PlatformCompatibility/_styles.scss +++ b/frontend/components/PlatformCompatibility/_styles.scss @@ -3,6 +3,7 @@ font-size: $x-small; align-items: center; padding-top: $pad-medium; + padding-bottom: $pad-large; b, svg, diff --git a/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx b/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx new file mode 100644 index 0000000000..df2a8fc374 --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx @@ -0,0 +1,18 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import QueryFrequencyIndicator from "./QueryFrequencyIndicator"; + +const meta: Meta = { + title: "Components/QueryFrequencyIndicator", + component: QueryFrequencyIndicator, + args: { + frequency: 300, + checked: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx b/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx new file mode 100644 index 0000000000..94dfecbdda --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import classnames from "classnames"; +import Icon from "components/Icon/Icon"; +import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; + +interface IStatusIndicatorProps { + frequency: number; + checked: boolean; +} + +const generateClassTag = (rawValue: string): string => { + if (rawValue === DEFAULT_EMPTY_CELL_VALUE) { + return "indeterminate"; + } + return rawValue.replace(" ", "-").toLowerCase(); +}; + +const QueryFrequencyIndicator = ({ + frequency, + checked, +}: IStatusIndicatorProps): JSX.Element => { + const classTag = generateClassTag(frequency.toString()); + const frequencyClassName = classnames( + "query-frequency-indicator", + `query-frequency-indicator--${classTag}`, + `frequency--${classTag}` + ); + const readableQueryFrequency = () => { + switch (frequency) { + case 0: + return "Never"; + case 300: + case 600: + case 900: + case 1800: // 5, 10, 15, 30 minutes + return `${(frequency / 60).toString()} minutes`; + case 3600: + return "Hourly"; + case 21600: + case 43200: // 6, 12 hours + return `${(frequency / 3600).toString()} hours`; + case 86400: + return "Daily"; + case 604800: + return "Weekly"; + default: + return "Unknown"; + } + }; + + const frequencyIcon = () => { + if (frequency === 0) { + return checked ? ( + + ) : ( + + ); + } + return ; + }; + + return ( +
+ {frequencyIcon()} + {readableQueryFrequency()} +
+ ); +}; + +export default QueryFrequencyIndicator; diff --git a/frontend/components/QueryFrequencyIndicator/_styles.scss b/frontend/components/QueryFrequencyIndicator/_styles.scss new file mode 100644 index 0000000000..f5b5f74d01 --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/_styles.scss @@ -0,0 +1,14 @@ +.query-frequency-indicator { + width: 100px; + display: flex; + align-items: center; + padding: 8px 12px; + + .icon { + padding-right: $pad-small; + } +} + +.grey { + color: $ui-fleet-black-33; +} diff --git a/frontend/components/QueryFrequencyIndicator/index.ts b/frontend/components/QueryFrequencyIndicator/index.ts new file mode 100644 index 0000000000..4f84c00133 --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/index.ts @@ -0,0 +1 @@ +export { default } from "./QueryFrequencyIndicator"; diff --git a/frontend/components/StatusIndicator/StatusIndicator.stories.tsx b/frontend/components/StatusIndicator/StatusIndicator.stories.tsx index f430ad8109..24c1eba6cc 100644 --- a/frontend/components/StatusIndicator/StatusIndicator.stories.tsx +++ b/frontend/components/StatusIndicator/StatusIndicator.stories.tsx @@ -8,7 +8,6 @@ const meta: Meta = { args: { value: "100", tooltip: { - id: 1, tooltipText: "Tooltip text", }, }, diff --git a/frontend/components/StatusIndicator/StatusIndicator.tsx b/frontend/components/StatusIndicator/StatusIndicator.tsx index 7bae7226c2..19d8b780d6 100644 --- a/frontend/components/StatusIndicator/StatusIndicator.tsx +++ b/frontend/components/StatusIndicator/StatusIndicator.tsx @@ -2,58 +2,72 @@ import React from "react"; import classnames from "classnames"; import ReactTooltip from "react-tooltip"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; +import { uniqueId } from "lodash"; +import { COLORS } from "styles/var/colors"; interface IStatusIndicatorProps { value: string; tooltip?: { - id: number; - tooltipText: string; + tooltipText: string | JSX.Element; position?: "top" | "bottom"; }; + customIndicatorType?: string; } -const generateClassTag = (rawValue: string): string => { +const generateIndicatorStateClassTag = ( + rawValue: string, + customIndicatorType?: string +): string => { if (rawValue === DEFAULT_EMPTY_CELL_VALUE) { return "indeterminate"; } - return rawValue.replace(" ", "-").toLowerCase(); + const prefix = customIndicatorType ? `${customIndicatorType}-` : ""; + return `${prefix}${rawValue.replace(" ", "-").toLowerCase()}`; }; const StatusIndicator = ({ value, tooltip, + customIndicatorType, }: IStatusIndicatorProps): JSX.Element => { - const classTag = generateClassTag(value); - const statusClassName = classnames( + const indicatorStateClassTag = generateIndicatorStateClassTag( + value, + customIndicatorType + ); + const indicatorClassNames = classnames( "status-indicator", - `status-indicator--${classTag}`, - `status--${classTag}` + `status-indicator--${indicatorStateClassTag}`, + `status--${indicatorStateClassTag}` ); - const indicatorContent = tooltip ? ( - <> - - {value} - - - {tooltip.tooltipText} - - - ) : ( - <>{value} - ); - return {indicatorContent}; + let indicatorContent; + if (tooltip) { + const tooltipId = uniqueId(); + indicatorContent = ( + <> + + {value} + + + {tooltip.tooltipText} + + + ); + } else { + indicatorContent = <>{value}; + } + return {indicatorContent}; }; export default StatusIndicator; diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx index da2c39d855..2f6d5eb55e 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx @@ -8,9 +8,7 @@ const PERFORMANCE_IMPACT = { indicator: "Minimal", id: 3 }; describe("Pill cell", () => { it("renders pill text and tooltip on hover", async () => { - const { user } = renderWithSetup( - - ); + const { user } = renderWithSetup(); await user.hover(screen.getByText("Minimal")); diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx index 528858b357..8fc83621ca 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx @@ -75,9 +75,8 @@ const PillCell = ({ case "Undetermined": return ( <> - To see performance
impact, this query must
run as a - scheduled query
on {hostDetails ? "this" : "at least one"}{" "} - host. + To see performance impact, this query must have run with{" "} + automations on {hostDetails ? "this" : "at least one"} host. ); default: diff --git a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.stories.tsx b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.stories.tsx index b49cbacc36..90f07ef649 100644 --- a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.stories.tsx +++ b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.stories.tsx @@ -6,7 +6,7 @@ const meta: Meta = { title: "Components/Table/PlatformCell", component: PlatformCell, args: { - value: ["darwin", "windows", "linux"], + platforms: ["darwin", "windows", "linux"], }, }; diff --git a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx index 683c4ea0b2..8a9129c7c0 100644 --- a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx +++ b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx @@ -1,14 +1,15 @@ import React from "react"; -import { getByTestId, render, screen, within } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; +import { SupportedPlatform } from "interfaces/platform"; import PlatformCell from "./PlatformCell"; -const PLATFORMS = ["windows", "darwin", "linux", "chrome"]; +const PLATFORMS: SupportedPlatform[] = ["windows", "darwin", "linux", "chrome"]; describe("Platform cell", () => { it("renders platform icons in correct order", () => { - render(); + render(); const icons = screen.queryAllByTestId("icon"); const appleIcon = screen.queryByTestId("apple-icon"); @@ -23,7 +24,7 @@ describe("Platform cell", () => { expect(icons[3].firstChild).toBe(chromeIcon); }); it("renders empty state", () => { - render(); + render(); const icons = screen.queryAllByTestId("icon"); const emptyText = screen.queryByText(DEFAULT_EMPTY_CELL_VALUE); diff --git a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx index f782643725..61a875cd5f 100644 --- a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx +++ b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx @@ -1,8 +1,10 @@ import React from "react"; import Icon from "components/Icon"; +import { SupportedPlatform } from "interfaces/platform"; +import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; interface IPlatformCellProps { - value: string[]; + platforms: SupportedPlatform[]; } const baseClass = "platform-cell"; @@ -14,7 +16,7 @@ const ICONS: Record = { chrome: "chrome", }; -const DISPLAY_ORDER = [ +const DISPLAY_ORDER: SupportedPlatform[] = [ "darwin", "windows", "linux", @@ -23,9 +25,7 @@ const DISPLAY_ORDER = [ // "Invalid query", ]; -const PlatformCell = ({ - value: platforms, -}: IPlatformCellProps): JSX.Element => { +const PlatformCell = ({ platforms }: IPlatformCellProps): JSX.Element => { const orderedList = DISPLAY_ORDER.filter((platform) => platforms.includes(platform) ); @@ -43,7 +43,9 @@ const PlatformCell = ({ ) : null; }) ) : ( - --- + + {DEFAULT_EMPTY_CELL_VALUE} + )} ); diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx index 403ab3188c..3678f09b4b 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx @@ -1,4 +1,6 @@ +import { uniqueId } from "lodash"; import React from "react"; +import ReactTooltip from "react-tooltip"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; interface ITextCellProps { @@ -6,6 +8,7 @@ interface ITextCellProps { formatter?: (val: any) => JSX.Element | string; // string, number, or null greyed?: boolean; classes?: string; + emptyCellTooltipText?: JSX.Element | string; } const TextCell = ({ @@ -13,6 +16,7 @@ const TextCell = ({ formatter = (val) => val, // identity function if no formatter is provided greyed, classes = "w250", + emptyCellTooltipText, }: ITextCellProps): JSX.Element => { let val = value; @@ -22,9 +26,32 @@ const TextCell = ({ if (!val) { greyed = true; } + + const renderEmptyCell = () => { + if (emptyCellTooltipText) { + const tooltipId = uniqueId(); + return ( + <> + + {DEFAULT_EMPTY_CELL_VALUE} + + + {emptyCellTooltipText} + + + ); + } + return DEFAULT_EMPTY_CELL_VALUE; + }; + return ( - {formatter(val) || DEFAULT_EMPTY_CELL_VALUE} + {formatter(val) || renderEmptyCell()} ); }; diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index c1ce50aeb8..7ada8b5274 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -216,10 +216,11 @@ $shadow-transition-width: 10px; .link-cell, .text-cell { display: block; - overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; margin: 0; + .__react_component_tooltip { + white-space: normal; + } } .w400 { max-width: calc(400px - 48px); @@ -234,6 +235,9 @@ $shadow-transition-width: 10px; .grey-cell { color: $ui-fleet-black-50; font-style: italic; + .__react_component_tooltip { + font-style: normal; + } } } diff --git a/frontend/components/buttons/RevealButton/RevealButton.tsx b/frontend/components/buttons/RevealButton/RevealButton.tsx index 830e56cebf..01b606f892 100644 --- a/frontend/components/buttons/RevealButton/RevealButton.tsx +++ b/frontend/components/buttons/RevealButton/RevealButton.tsx @@ -47,7 +47,7 @@ const RevealButton = ({ {caretPosition === "before" && ( )} diff --git a/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx b/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx index a707fe2643..30b882c15a 100644 --- a/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx +++ b/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx @@ -103,7 +103,6 @@ class InputFieldWithIcon extends InputField { { [`${baseClass}__icon--active`]: value } ); - console.log("iconSvg", iconSvg); return (
{this.props.label && this.renderHeading()} diff --git a/frontend/components/icons/Clock.tsx b/frontend/components/icons/Clock.tsx new file mode 100644 index 0000000000..c739a19aa7 --- /dev/null +++ b/frontend/components/icons/Clock.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +import { COLORS, Colors } from "styles/var/colors"; +import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes"; + +interface IClockProps { + color?: Colors; + size?: IconSizes; +} + +const Clock = ({ + color = "ui-fleet-black-75", + size = "small", +}: IClockProps) => { + return ( + + + + + ); +}; + +export default Clock; diff --git a/frontend/components/icons/Warning.tsx b/frontend/components/icons/Warning.tsx new file mode 100644 index 0000000000..a3e1ce156c --- /dev/null +++ b/frontend/components/icons/Warning.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +import { COLORS, Colors } from "styles/var/colors"; +import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes"; + +interface IWarningProps { + color?: Colors; + size?: IconSizes; +} + +const Warning = ({ + color = "status-warning", + size = "small", +}: IWarningProps) => { + return ( + + + + ); +}; + +export default Warning; diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts index bb5fe334aa..bda1176aa3 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -51,6 +51,8 @@ import Pending from "./Pending"; import PendingPartial from "./PendingPartial"; import ErrorOutline from "./ErrorOutline"; import Error from "./Error"; +import Warning from "./Warning"; +import Clock from "./Clock"; import Copy from "./Copy"; import Eye from "./Eye"; @@ -110,6 +112,8 @@ export const ICON_MAP = { "pending-partial": PendingPartial, error: Error, "error-outline": ErrorOutline, + warning: Warning, + clock: Clock, darwin: Apple, macOS: Apple, windows: Windows, diff --git a/frontend/components/side_panels/QuerySidePanel/QueryTablePlatforms/QueryTablePlatforms.tsx b/frontend/components/side_panels/QuerySidePanel/QueryTablePlatforms/QueryTablePlatforms.tsx index 228bb18473..c517e21350 100644 --- a/frontend/components/side_panels/QuerySidePanel/QueryTablePlatforms/QueryTablePlatforms.tsx +++ b/frontend/components/side_panels/QuerySidePanel/QueryTablePlatforms/QueryTablePlatforms.tsx @@ -1,11 +1,11 @@ import React from "react"; -import { IOsqueryPlatform } from "interfaces/platform"; +import { OsqueryPlatform } from "interfaces/platform"; import { PLATFORM_DISPLAY_NAMES } from "utilities/constants"; import Icon from "components/Icon"; interface IPLatformListItemProps { - platform: IOsqueryPlatform; + platform: OsqueryPlatform; } const baseClassListItem = "platform-list-item"; @@ -20,7 +20,7 @@ const PlatformListItem = ({ platform }: IPLatformListItemProps) => { }; // TODO: remove when freebsd is removed -type IPlatformsWithFreebsd = IOsqueryPlatform | "freebsd"; +type IPlatformsWithFreebsd = OsqueryPlatform | "freebsd"; interface IQueryTablePlatformsProps { platforms: IPlatformsWithFreebsd[]; @@ -38,7 +38,7 @@ const QueryTablePlatforms = ({ platforms }: IQueryTablePlatformsProps) => { return ( ); }); diff --git a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tests.tsx b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tests.tsx index 09f2c16c9b..92133a1423 100644 --- a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tests.tsx +++ b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tests.tsx @@ -43,7 +43,6 @@ describe("SiteTopNav - component", () => { expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); - expect(screen.getByText(/schedule/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); expect(screen.getByText(/settings/i)).toBeInTheDocument(); expect(screen.getByText(/manage users/i)).toBeInTheDocument(); @@ -80,7 +79,6 @@ describe("SiteTopNav - component", () => { expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); - expect(screen.getByText(/schedule/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); expect(screen.getByText(/my account/i)).toBeInTheDocument(); expect(screen.getByText(/documentation/i)).toBeInTheDocument(); @@ -122,7 +120,6 @@ describe("SiteTopNav - component", () => { expect(screen.getByText(/sign out/i)).toBeInTheDocument(); expect(screen.queryByText(/controls/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/schedule/i)).not.toBeInTheDocument(); expect(screen.queryByText(/settings/i)).not.toBeInTheDocument(); expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument(); }); @@ -152,7 +149,6 @@ describe("SiteTopNav - component", () => { expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); - expect(screen.getByText(/schedule/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); expect(screen.getByText(/settings/i)).toBeInTheDocument(); expect(screen.getByText(/manage users/i)).toBeInTheDocument(); @@ -189,7 +185,6 @@ describe("SiteTopNav - component", () => { expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); - expect(screen.getByText(/schedule/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); expect(screen.getByText(/my account/i)).toBeInTheDocument(); expect(screen.getByText(/documentation/i)).toBeInTheDocument(); @@ -231,7 +226,6 @@ describe("SiteTopNav - component", () => { expect(screen.getByText(/sign out/i)).toBeInTheDocument(); expect(screen.queryByText(/controls/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/schedule/i)).not.toBeInTheDocument(); expect(screen.queryByText(/settings/i)).not.toBeInTheDocument(); expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument(); }); @@ -264,7 +258,6 @@ describe("SiteTopNav - component", () => { expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); - expect(screen.getByText(/schedule/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); expect(screen.getByText(/settings/i)).toBeInTheDocument(); expect(screen.getByText(/my account/i)).toBeInTheDocument(); @@ -302,7 +295,6 @@ describe("SiteTopNav - component", () => { expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); - expect(screen.getByText(/schedule/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); expect(screen.getByText(/my account/i)).toBeInTheDocument(); expect(screen.getByText(/documentation/i)).toBeInTheDocument(); @@ -344,7 +336,6 @@ describe("SiteTopNav - component", () => { expect(screen.getByText(/sign out/i)).toBeInTheDocument(); expect(screen.queryByText(/controls/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/schedule/i)).not.toBeInTheDocument(); expect(screen.queryByText(/settings/i)).not.toBeInTheDocument(); expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument(); }); diff --git a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx index 5cea8246ed..7524230c86 100644 --- a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx +++ b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx @@ -39,13 +39,10 @@ const REGEX_DETAIL_PAGES = { PACK_NEW: /\/packs\/new/i, POLICY_EDIT: /\/policies\/\d+/i, POLICY_NEW: /\/policies\/new/i, - QUERY_EDIT: /\/queries\/\d+/i, - QUERY_NEW: /\/queries\/new/i, SOFTWARE_DETAILS: /\/software\/\d+/i, }; const REGEX_GLOBAL_PAGES = { - MANAGE_QUERIES: /\/queries\/manage/i, MANAGE_PACKS: /\/packs\/manage/i, ORGANIZATION: /\/settings\/organization/i, USERS: /\/settings\/users/i, diff --git a/frontend/components/top_nav/SiteTopNav/navItems.ts b/frontend/components/top_nav/SiteTopNav/navItems.ts index 203e14f597..4b828087ed 100644 --- a/frontend/components/top_nav/SiteTopNav/navItems.ts +++ b/frontend/components/top_nav/SiteTopNav/navItems.ts @@ -77,14 +77,6 @@ export default ( regex: new RegExp(`^${URL_PREFIX}/queries/`), pathname: PATHS.MANAGE_QUERIES, }, - }, - { - name: "Schedule", - location: { - regex: new RegExp(`^${URL_PREFIX}/(schedule|packs)/`), - pathname: PATHS.MANAGE_SCHEDULE, - }, - exclude: !isMaintainerOrAdmin, withParams: { type: "query", names: ["team_id"] }, }, { diff --git a/frontend/context/policy.tsx b/frontend/context/policy.tsx index 0562242aee..ad8c1f43b7 100644 --- a/frontend/context/policy.tsx +++ b/frontend/context/policy.tsx @@ -9,7 +9,7 @@ import { find } from "lodash"; import { osqueryTables } from "utilities/osquery_tables"; import { IOsQueryTable, DEFAULT_OSQUERY_TABLE } from "interfaces/osquery_table"; -import { IPlatformString } from "interfaces/platform"; +import { SelectedPlatformString } from "interfaces/platform"; enum ACTIONS { SET_LAST_EDITED_QUERY_INFO = "SET_LAST_EDITED_QUERY_INFO", @@ -25,7 +25,7 @@ interface ISetLastEditedQueryInfo { lastEditedQueryBody?: string; lastEditedQueryResolution?: string; lastEditedQueryCritical?: boolean; - lastEditedQueryPlatform?: IPlatformString | null; + lastEditedQueryPlatform?: SelectedPlatformString | null; defaultPolicy?: boolean; } @@ -55,7 +55,7 @@ type InitialStateType = { lastEditedQueryBody: string; lastEditedQueryResolution: string; lastEditedQueryCritical: boolean; - lastEditedQueryPlatform: IPlatformString | null; + lastEditedQueryPlatform: SelectedPlatformString | null; defaultPolicy: boolean; setLastEditedQueryId: (value: number) => void; setLastEditedQueryName: (value: string) => void; @@ -63,7 +63,7 @@ type InitialStateType = { setLastEditedQueryBody: (value: string) => void; setLastEditedQueryResolution: (value: string) => void; setLastEditedQueryCritical: (value: boolean) => void; - setLastEditedQueryPlatform: (value: IPlatformString | null) => void; + setLastEditedQueryPlatform: (value: SelectedPlatformString | null) => void; setDefaultPolicy: (value: boolean) => void; policyTeamId: number; setPolicyTeamId: (id: number) => void; @@ -210,7 +210,7 @@ const PolicyProvider = ({ children }: Props): JSX.Element => { [] ); const setLastEditedQueryPlatform = useCallback( - (lastEditedQueryPlatform: IPlatformString | null | undefined) => { + (lastEditedQueryPlatform: SelectedPlatformString | null | undefined) => { dispatch({ type: ACTIONS.SET_LAST_EDITED_QUERY_INFO, lastEditedQueryPlatform, diff --git a/frontend/context/query.tsx b/frontend/context/query.tsx index b7e2127b91..9c3b401c70 100644 --- a/frontend/context/query.tsx +++ b/frontend/context/query.tsx @@ -4,6 +4,8 @@ import { find } from "lodash"; import { osqueryTables } from "utilities/osquery_tables"; 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"; type Props = { children: ReactNode; @@ -16,11 +18,19 @@ type InitialStateType = { lastEditedQueryDescription: string; lastEditedQueryBody: string; lastEditedQueryObserverCanRun: boolean; + lastEditedQueryFrequency: number; + lastEditedQueryPlatforms: SelectedPlatformString; + lastEditedQueryMinOsqueryVersion: string; + lastEditedQueryLoggingType: QueryLoggingOption; setLastEditedQueryId: (value: number) => void; setLastEditedQueryName: (value: string) => void; setLastEditedQueryDescription: (value: string) => void; setLastEditedQueryBody: (value: string) => void; setLastEditedQueryObserverCanRun: (value: boolean) => void; + setLastEditedQueryFrequency: (value: number) => void; + setLastEditedQueryPlatforms: (value: SelectedPlatformString) => void; + setLastEditedQueryMinOsqueryVersion: (value: string) => void; + setLastEditedQueryLoggingType: (value: string) => void; setSelectedOsqueryTable: (tableName: string) => void; }; @@ -32,11 +42,19 @@ const initialState = { lastEditedQueryDescription: DEFAULT_QUERY.description, lastEditedQueryBody: DEFAULT_QUERY.query, lastEditedQueryObserverCanRun: DEFAULT_QUERY.observer_can_run, + lastEditedQueryFrequency: DEFAULT_QUERY.interval, + lastEditedQueryPlatforms: DEFAULT_QUERY.platform, + lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version, + lastEditedQueryLoggingType: DEFAULT_QUERY.logging, setLastEditedQueryId: () => null, setLastEditedQueryName: () => null, setLastEditedQueryDescription: () => null, setLastEditedQueryBody: () => null, setLastEditedQueryObserverCanRun: () => null, + setLastEditedQueryFrequency: () => null, + setLastEditedQueryPlatforms: () => null, + setLastEditedQueryMinOsqueryVersion: () => null, + setLastEditedQueryLoggingType: () => null, setSelectedOsqueryTable: () => null, }; @@ -77,6 +95,22 @@ const reducer = (state: InitialStateType, action: any) => { typeof action.lastEditedQueryObserverCanRun === "undefined" ? state.lastEditedQueryObserverCanRun : action.lastEditedQueryObserverCanRun, + lastEditedQueryFrequency: + typeof action.lastEditedQueryFrequency === "undefined" + ? state.lastEditedQueryFrequency + : action.lastEditedQueryFrequency, + lastEditedQueryPlatforms: + typeof action.lastEditedQueryPlatforms === "undefined" + ? state.lastEditedQueryPlatforms + : action.lastEditedQueryPlatforms, + lastEditedQueryMinOsqueryVersion: + typeof action.lastEditedQueryMinOsqueryVersion === "undefined" + ? state.lastEditedQueryMinOsqueryVersion + : action.lastEditedQueryMinOsqueryVersion, + lastEditedQueryLoggingType: + typeof action.lastEditedQueryLoggingType === "undefined" + ? state.lastEditedQueryLoggingType + : action.lastEditedQueryLoggingType, }; default: return state; @@ -95,6 +129,10 @@ const QueryProvider = ({ children }: Props) => { lastEditedQueryDescription: state.lastEditedQueryDescription, lastEditedQueryBody: state.lastEditedQueryBody, lastEditedQueryObserverCanRun: state.lastEditedQueryObserverCanRun, + lastEditedQueryFrequency: state.lastEditedQueryFrequency, + lastEditedQueryPlatforms: state.lastEditedQueryPlatforms, + lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion, + lastEditedQueryLoggingType: state.lastEditedQueryLoggingType, setLastEditedQueryId: (lastEditedQueryId: number) => { dispatch({ type: actions.SET_LAST_EDITED_QUERY_INFO, @@ -127,6 +165,32 @@ const QueryProvider = ({ children }: Props) => { lastEditedQueryObserverCanRun, }); }, + setLastEditedQueryFrequency: (lastEditedQueryFrequency: number) => { + dispatch({ + type: actions.SET_LAST_EDITED_QUERY_INFO, + lastEditedQueryFrequency, + }); + }, + setLastEditedQueryPlatforms: (lastEditedQueryPlatforms: string) => { + dispatch({ + type: actions.SET_LAST_EDITED_QUERY_INFO, + lastEditedQueryPlatforms, + }); + }, + setLastEditedQueryMinOsqueryVersion: ( + lastEditedQueryMinOsqueryVersion: string + ) => { + dispatch({ + type: actions.SET_LAST_EDITED_QUERY_INFO, + lastEditedQueryMinOsqueryVersion, + }); + }, + setLastEditedQueryLoggingType: (lastEditedQueryLoggingType: string) => { + dispatch({ + type: actions.SET_LAST_EDITED_QUERY_INFO, + lastEditedQueryLoggingType, + }); + }, setSelectedOsqueryTable: (tableName: string) => { dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName }); }, diff --git a/frontend/hooks/usePlatformCompatibility.tsx b/frontend/hooks/usePlatformCompatibility.tsx index 2185117e4e..af4c51c452 100644 --- a/frontend/hooks/usePlatformCompatibility.tsx +++ b/frontend/hooks/usePlatformCompatibility.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from "react"; import { useDebouncedCallback } from "use-debounce"; -import { IOsqueryPlatform, SUPPORTED_PLATFORMS } from "interfaces/platform"; +import { OsqueryPlatform, SUPPORTED_PLATFORMS } from "interfaces/platform"; import checkPlatformCompatibility from "utilities/sql_tools"; import PlatformCompatibility from "components/PlatformCompatibility"; @@ -16,7 +16,7 @@ const DEBOUNCE_DELAY = 300; const usePlatformCompatibility = (): IPlatformCompatibility => { const [compatiblePlatforms, setCompatiblePlatforms] = useState< - IOsqueryPlatform[] | null + OsqueryPlatform[] | null >(null); const [error, setError] = useState(null); diff --git a/frontend/hooks/usePlatformSelector.tsx b/frontend/hooks/usePlatformSelector.tsx index c1e91399d3..912d4e4831 100644 --- a/frontend/hooks/usePlatformSelector.tsx +++ b/frontend/hooks/usePlatformSelector.tsx @@ -1,7 +1,10 @@ import React, { useCallback, useEffect, useState } from "react"; import { forEach } from "lodash"; -import { IPlatformString, SUPPORTED_PLATFORMS } from "interfaces/platform"; +import { + SelectedPlatformString, + SUPPORTED_PLATFORMS, +} from "interfaces/platform"; import PlatformSelector from "components/PlatformSelector"; @@ -13,7 +16,7 @@ export interface IPlatformSelector { } const usePlatformSelector = ( - platformContext: IPlatformString | null | undefined, + platformContext: SelectedPlatformString | null | undefined, baseClass = "" ): IPlatformSelector => { const [checkDarwin, setCheckDarwin] = useState(false); diff --git a/frontend/interfaces/osquery_table.ts b/frontend/interfaces/osquery_table.ts index 289f5a43aa..36f5295e9d 100644 --- a/frontend/interfaces/osquery_table.ts +++ b/frontend/interfaces/osquery_table.ts @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import { IOsqueryPlatform } from "./platform"; +import { OsqueryPlatform } from "./platform"; export default PropTypes.shape({ columns: PropTypes.arrayOf( @@ -28,7 +28,7 @@ export interface IQueryTableColumn { hidden: boolean; required: boolean; index: boolean; - platforms?: IOsqueryPlatform[]; + platforms?: OsqueryPlatform[]; requires_user_context?: boolean; } @@ -36,7 +36,7 @@ export interface IOsQueryTable { name: string; description: string; url: string; - platforms: IOsqueryPlatform[]; + platforms: OsqueryPlatform[]; evented: boolean; cacheable: boolean; columns: IQueryTableColumn[]; diff --git a/frontend/interfaces/platform.ts b/frontend/interfaces/platform.ts index 3dc3d12a42..434f07a61f 100644 --- a/frontend/interfaces/platform.ts +++ b/frontend/interfaces/platform.ts @@ -1,4 +1,4 @@ -export type IOsqueryPlatform = +export type OsqueryPlatform = | "darwin" | "macOS" | "windows" @@ -8,14 +8,17 @@ export type IOsqueryPlatform = | "chrome" | "ChromeOS"; -export type ISelectedPlatform = - | "all" - | "darwin" - | "windows" - | "linux" - | "chrome"; +export type SupportedPlatform = "darwin" | "windows" | "linux" | "chrome"; -export type IPlatformString = +export const SUPPORTED_PLATFORMS: SupportedPlatform[] = [ + "darwin", + "windows", + "linux", + "chrome", +]; +export type SelectedPlatform = SupportedPlatform | "all"; + +export type SelectedPlatformString = | "" | "darwin" | "windows" @@ -33,15 +36,8 @@ export type IPlatformString = | "windows,chrome" | "linux,chrome"; -export const SUPPORTED_PLATFORMS = [ - "darwin", - "windows", - "linux", - "chrome", -] as const; - // TODO: revisit this approach pending resolution of https://github.com/fleetdm/fleet/issues/3555. -export const MACADMINS_EXTENSION_TABLES: Record = { +export const MACADMINS_EXTENSION_TABLES: Record = { file_lines: ["darwin", "linux", "windows"], filevault_users: ["darwin"], google_chrome_profiles: ["darwin", "linux", "windows"], diff --git a/frontend/interfaces/policy.ts b/frontend/interfaces/policy.ts index e01fc8e746..5183653883 100644 --- a/frontend/interfaces/policy.ts +++ b/frontend/interfaces/policy.ts @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import { IPlatformString } from "interfaces/platform"; +import { SelectedPlatformString } from "interfaces/platform"; // Legacy PropTypes used on host interface export default PropTypes.shape({ @@ -31,7 +31,7 @@ export interface IPolicy { author_name: string; author_email: string; resolution: string; - platform: IPlatformString; + platform: SelectedPlatformString; team_id?: number; created_at: string; updated_at: string; @@ -80,7 +80,7 @@ export interface IPolicyFormData { description?: string | number | boolean | undefined; resolution?: string | number | boolean | undefined; critical?: boolean; - platform?: IPlatformString; + platform?: SelectedPlatformString; name?: string | number | boolean | undefined; query?: string | number | boolean | undefined; team_id?: number; @@ -95,6 +95,6 @@ export interface IPolicyNew { query: string; resolution: string; critical: boolean; - platform: IPlatformString; + platform: SelectedPlatformString; mdm_required?: boolean; } diff --git a/frontend/interfaces/query.ts b/frontend/interfaces/query.ts index cd3b6be1c7..d6a948cd25 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -1,37 +1,22 @@ -import PropTypes from "prop-types"; import { IFormField } from "./form_field"; -import packInterface, { IPack } from "./pack"; -import scheduledQueryStatsInterface, { - IScheduledQueryStats, -} from "./scheduled_query_stats"; +import { IPack } from "./pack"; +import { ISchedulableQuery } from "./schedulable_query"; +import { IScheduledQueryStats } from "./scheduled_query_stats"; -export default PropTypes.shape({ - created_at: PropTypes.string, - updated_at: PropTypes.string, - id: PropTypes.number, - name: PropTypes.string, - description: PropTypes.string, - query: PropTypes.string, - saved: PropTypes.bool, - author_id: PropTypes.number, - author_name: PropTypes.string, - observer_can_run: PropTypes.bool, - packs: PropTypes.arrayOf(packInterface), - stats: scheduledQueryStatsInterface, -}); export interface IQueryFormData { description?: string | number | boolean | undefined; name?: string | number | boolean | undefined; query?: string | number | boolean | undefined; observer_can_run?: string | number | boolean | undefined; + automations_enabled?: boolean; } export interface IStoredQueryResponse { - query: IQuery; + query: ISchedulableQuery; } export interface IFleetQueriesResponse { - queries: IQuery[]; + queries: ISchedulableQuery[]; } export interface IQuery { diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index bd2961b547..9d86c98b85 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -1,6 +1,6 @@ import { IFormField } from "./form_field"; import { IPack } from "./pack"; -import { IPlatformString } from "./platform"; +import { SelectedPlatformString, SupportedPlatform } from "./platform"; // Query itself export interface ISchedulableQuery { @@ -12,7 +12,7 @@ export interface ISchedulableQuery { query: string; team_id: number | null; interval: number; - platform: IPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted + platform: SelectedPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted min_osquery_version: string; automations_enabled: boolean; logging: QueryLoggingOption; @@ -24,6 +24,11 @@ export interface ISchedulableQuery { packs: IPack[]; stats: ISchedulableQueryStats; } + +export interface IEnhancedQuery extends ISchedulableQuery { + performance: string; + platforms: SupportedPlatform[]; +} export interface ISchedulableQueryStats { user_time_p50?: number; user_time_p95?: number; @@ -46,6 +51,10 @@ export interface IListQueriesResponse { queries: ISchedulableQuery[]; } +export interface IQueryKeyQueriesLoadAll { + scope: "queries"; + teamId: number | undefined; +} // Create a new query /** POST /api/v1/fleet/queries */ export interface ICreateQueryRequestBody { @@ -55,7 +64,7 @@ export interface ICreateQueryRequestBody { observer_can_run?: boolean; team_id?: number; // global query if ommitted interval?: number; // default 0 means never run - platform?: IPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted + platform?: SelectedPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted min_osquery_version?: string; // default all versions if ommitted automations_enabled?: boolean; // whether to send data to the configured log destination according to the query's `interval`. Default false if ommitted. logging?: QueryLoggingOption; @@ -67,9 +76,14 @@ export interface ICreateQueryRequestBody { /** PATCH /api/v1/fleet/queries/{id} */ export interface IModifyQueryRequestBody extends Omit { - id: number; + id?: number; name?: string; query?: string; + description?: string; + observer_can_run?: boolean; + frequency?: number; + platform?: SelectedPlatformString; + min_osquery_version?: string; } // response is ISchedulableQuery // better way to indicate this? @@ -77,7 +91,7 @@ export interface IModifyQueryRequestBody // Delete a query by name /** DELETE /api/v1/fleet/queries/{name} */ export interface IDeleteQueryRequestBody { - team_id?: number; // searches for a global query if ommitted + team_id?: number; // searches for a global query if omitted } // Delete a query by id @@ -100,7 +114,7 @@ export interface IQueryFormFields { query: IFormField; observer_can_run: IFormField; frequency: IFormField; - platforms: IFormField; + platforms: IFormField; min_osquery_version: IFormField; logging: IFormField; } diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx index d1a9febd4b..ec4659c4a5 100644 --- a/frontend/pages/DashboardPage/DashboardPage.tsx +++ b/frontend/pages/DashboardPage/DashboardPage.tsx @@ -21,7 +21,7 @@ import { IMdmSolution, IMdmSummaryResponse, } from "interfaces/mdm"; -import { ISelectedPlatform } from "interfaces/platform"; +import { SelectedPlatform } from "interfaces/platform"; import { ISoftwareResponse, ISoftwareCountResponse } from "interfaces/software"; import { ITeam } from "interfaces/team"; import { useTeamIdParam } from "hooks/useTeamIdParam"; @@ -107,7 +107,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { includeNoTeam: false, }); - const [selectedPlatform, setSelectedPlatform] = useState( + const [selectedPlatform, setSelectedPlatform] = useState( "all" ); const [ @@ -757,7 +757,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { className={`${baseClass}__platform_dropdown`} options={PLATFORM_DROPDOWN_OPTIONS} searchable={false} - onChange={(value: ISelectedPlatform) => { + onChange={(value: SelectedPlatform) => { const selectedPlatformOption = PLATFORM_DROPDOWN_OPTIONS.find( (platform) => platform.value === value ); diff --git a/frontend/pages/DashboardPage/cards/HostsSummary/HostsSummary.tsx b/frontend/pages/DashboardPage/cards/HostsSummary/HostsSummary.tsx index 463f0c0e26..a02f4861e4 100644 --- a/frontend/pages/DashboardPage/cards/HostsSummary/HostsSummary.tsx +++ b/frontend/pages/DashboardPage/cards/HostsSummary/HostsSummary.tsx @@ -3,7 +3,7 @@ import PATHS from "router/paths"; import labelsAPI from "services/entities/labels"; import DataError from "components/DataError"; -import { ISelectedPlatform } from "interfaces/platform"; +import { SelectedPlatform } from "interfaces/platform"; import { useQuery } from "react-query"; import { ILabelSpecResponse } from "interfaces/label"; @@ -20,7 +20,7 @@ interface IHostSummaryProps { isLoadingHostsSummary: boolean; showHostsUI: boolean; errorHosts: boolean; - selectedPlatform?: ISelectedPlatform; + selectedPlatform?: SelectedPlatform; } const HostsSummary = ({ diff --git a/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx b/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx index f07fabd96f..cf7b085087 100644 --- a/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx +++ b/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx @@ -5,7 +5,7 @@ import { OS_END_OF_LIFE_LINK_BY_PLATFORM, OS_VENDOR_BY_PLATFORM, } from "interfaces/operating_system"; -import { ISelectedPlatform } from "interfaces/platform"; +import { SelectedPlatform } from "interfaces/platform"; import { getOSVersions, IGetOSVersionsQueryKey, @@ -26,7 +26,7 @@ import generateTableHeaders from "./OperatingSystemsTableConfig"; interface IOperatingSystemsCardProps { currentTeamId: number | undefined; - selectedPlatform: ISelectedPlatform; + selectedPlatform: SelectedPlatform; showTitle: boolean; /** controls the displaying of description text under the title. Defaults to `true` */ showDescription?: boolean; @@ -42,7 +42,7 @@ const DEFAULT_SORT_HEADER = "hosts_count"; const PAGE_SIZE = 8; const baseClass = "operating-systems"; -const EmptyOperatingSystems = (platform: ISelectedPlatform): JSX.Element => ( +const EmptyOperatingSystems = (platform: SelectedPlatform): JSX.Element => ( { const value = cellProps.cell.value; const tooltip = { - id: cellProps.row.original.id, tooltipText: getHostStatusTooltipText(value), }; return ; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx index 6110452dbe..3ac219c2e0 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx @@ -54,22 +54,6 @@ describe("Host Actions Dropdown", () => { }); }); - it("renders the Query action as disabled if the host is offline", async () => { - const render = createCustomRenderer(); - - const { user } = render( - - ); - - await user.click(screen.getByText("Actions")); - - expect(screen.getByText("Query").parentNode).toHaveClass("is-disabled"); - }); - it("renders the Show Disk Encryption Key action when on premium tier and we store the disk encryption key", async () => { const render = createCustomRenderer({ context: { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index f67a3f13d7..f121702357 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -18,16 +18,20 @@ import { IHost, IDeviceMappingResponse, IMacadminsResponse, - IPackStats, IHostResponse, IHostMdmData, + IPackStats, } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { IHostPolicy } from "interfaces/policy"; -import { IQuery, IFleetQueriesResponse } from "interfaces/query"; import { IQueryStats } from "interfaces/query_stats"; import { ISoftware } from "interfaces/software"; import { ITeam } from "interfaces/team"; +import { + IListQueriesResponse, + IQueryKeyQueriesLoadAll, + ISchedulableQuery, +} from "interfaces/schedulable_query"; import Spinner from "components/Spinner"; import TabsWrapper from "components/TabsWrapper"; @@ -48,7 +52,6 @@ import UsersCard from "../cards/Users"; import PoliciesCard from "../cards/Policies"; import ScheduleCard from "../cards/Schedule"; import PacksCard from "../cards/Packs"; -import SelectQueryModal from "./modals/SelectQueryModal"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; import OSPolicyModal from "./modals/OSPolicyModal"; import UnenrollMdmModal from "./modals/UnenrollMdmModal"; @@ -61,6 +64,7 @@ import DiskEncryptionKeyModal from "./modals/DiskEncryptionKeyModal"; import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown"; import MacSettingsModal from "../MacSettingsModal"; import BootstrapPackageModal from "./modals/BootstrapPackageModal"; +import SelectQueryModal from "./modals/SelectQueryModal"; const baseClass = "host-details"; @@ -151,24 +155,25 @@ const HostDetailsPage = ({ const [refetchStartTime, setRefetchStartTime] = useState(null); const [showRefetchSpinner, setShowRefetchSpinner] = useState(false); - const [packsState, setPacksState] = useState(); const [schedule, setSchedule] = useState(); + const [packsState, setPacksState] = useState(); const [hostSoftware, setHostSoftware] = useState([]); const [usersState, setUsersState] = useState<{ username: string }[]>([]); const [usersSearchString, setUsersSearchString] = useState(""); const [pathname, setPathname] = useState(""); const { data: fleetQueries, error: fleetQueriesError } = useQuery< - IFleetQueriesResponse, + IListQueriesResponse, Error, - IQuery[] - >("fleet queries", () => queryAPI.loadAll(), { + ISchedulableQuery[], + IQueryKeyQueriesLoadAll[] + >([{ scope: "queries", teamId: undefined }], () => queryAPI.loadAll(), { enabled: !!hostIdFromURL, refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, retry: false, - select: (data: IFleetQueriesResponse) => data.queries, + select: (data: IListQueriesResponse) => data.queries, }); const { data: teams } = useQuery( @@ -312,8 +317,8 @@ const HostDetailsPage = ({ }, { packs: [], schedule: [] } ); - setPacksState(packStatsByType.packs); setSchedule(packStatsByType.schedule); + setPacksState(packStatsByType.packs); } }, onError: (error) => handlePageError(error), @@ -484,9 +489,9 @@ const HostDetailsPage = ({ router.push(PATHS.NEW_QUERY + TAGGED_TEMPLATES.queryByHostRoute(host?.id)); }; - const onQueryHostSaved = (selectedQuery: IQuery) => { + const onQueryHostSaved = (selectedQuery: ISchedulableQuery) => { router.push( - PATHS.EDIT_QUERY(selectedQuery) + + PATHS.EDIT_QUERY(selectedQuery.id) + TAGGED_TEMPLATES.queryByHostRoute(host?.id) ); }; diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx index c5378d5c90..e28d5e7b52 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useContext } from "react"; import { filter, includes } from "lodash"; -import { IQuery } from "interfaces/query"; import { AppContext } from "context/app"; import Button from "components/buttons/Button"; @@ -11,12 +10,13 @@ import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon"; import DataError from "components/DataError"; import permissions from "utilities/permissions"; +import { ISchedulableQuery } from "interfaces/schedulable_query"; export interface ISelectQueryModalProps { onCancel: () => void; onQueryHostCustom: () => void; - onQueryHostSaved: (selectedQuery: IQuery) => void; - queries: IQuery[] | []; + onQueryHostSaved: (selectedQuery: ISchedulableQuery) => void; + queries: ISchedulableQuery[] | []; queryErrors: Error | null; isOnlyObserver?: boolean; hostsTeamId: number | null; diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index ad828e0f50..d3db64be7b 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -187,7 +187,6 @@ const HostSummary = ({ { ), }, diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx index 033aa29fde..171281c340 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx @@ -22,7 +22,7 @@ const TAGGED_TEMPLATES = { }; const DEFAULT_SORT_DIRECTION = "asc"; -const DEFAULT_SORT_HEADER = "updated_at"; +const DEFAULT_SORT_HEADER = "name"; interface IPoliciesTableProps { policiesList: IPolicyStats[]; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx index 30b08fb234..825f00db6f 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx @@ -15,6 +15,7 @@ import PATHS from "router/paths"; import sortUtils from "utilities/sort"; import { PolicyResponse } from "utilities/constants"; import { buildQueryStringFromParams } from "utilities/url"; +import { COLORS } from "styles/var/colors"; import PassingColumnHeader from "../PassingColumnHeader"; interface IGetToggleAllRowsSelectedProps { @@ -138,7 +139,7 @@ const generateTableHeaders = ( type="dark" effect="solid" id={`critical-tooltip-${cellProps.row.original.id}`} - backgroundColor="#3e4771" + backgroundColor={COLORS["tooltip-bg"]} > This policy has been marked as critical. {isSandboxMode && ( diff --git a/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx b/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx index a1aa92ce72..1f8a9a2948 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx @@ -14,7 +14,7 @@ import usePlatformCompatibility from "hooks/usePlatformCompatibility"; import usePlatformSelector from "hooks/usePlatformSelector"; import { IPolicy, IPolicyFormData } from "interfaces/policy"; -import { IOsqueryPlatform, IPlatformString } from "interfaces/platform"; +import { OsqueryPlatform, SelectedPlatformString } from "interfaces/platform"; import { DEFAULT_POLICIES } from "pages/policies/constants"; import Avatar from "components/Avatar"; @@ -226,7 +226,7 @@ const PolicyForm = ({ }); } - let selectedPlatforms: IOsqueryPlatform[] = []; + let selectedPlatforms: OsqueryPlatform[] = []; if (isEditMode || defaultPolicy) { selectedPlatforms = getSelectedPlatforms(); } else { @@ -234,7 +234,9 @@ const PolicyForm = ({ setSelectedPlatforms(selectedPlatforms); } - const newPlatformString = selectedPlatforms.join(",") as IPlatformString; + const newPlatformString = selectedPlatforms.join( + "," + ) as SelectedPlatformString; if (!defaultPolicy) { setLastEditedQueryPlatform(newPlatformString); diff --git a/frontend/pages/policies/PolicyPage/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx b/frontend/pages/policies/PolicyPage/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx index d30d85f2b3..e96617c986 100644 --- a/frontend/pages/policies/PolicyPage/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx +++ b/frontend/pages/policies/PolicyPage/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx @@ -5,7 +5,7 @@ import { AppContext } from "context/app"; import { PolicyContext } from "context/policy"; import { IPlatformSelector } from "hooks/usePlatformSelector"; import { IPolicyFormData } from "interfaces/policy"; -import { IPlatformString } from "interfaces/platform"; +import { SelectedPlatformString } from "interfaces/platform"; import useDeepEffect from "hooks/useDeepEffect"; // @ts-ignore @@ -81,7 +81,7 @@ const SaveNewPolicyModal = ({ const newPlatformString = platformSelector .getSelectedPlatforms() - .join(",") as IPlatformString; + .join(",") as SelectedPlatformString; setLastEditedQueryPlatform(newPlatformString); const { valid: validName, errors: newErrors } = validatePolicyName(name); diff --git a/frontend/pages/policies/constants.ts b/frontend/pages/policies/constants.ts index 2352b0ab3a..6bc9ded8a3 100644 --- a/frontend/pages/policies/constants.ts +++ b/frontend/pages/policies/constants.ts @@ -1,7 +1,7 @@ import { IPolicyNew } from "interfaces/policy"; -import { IPlatformString } from "interfaces/platform"; +import { SelectedPlatformString } from "interfaces/platform"; -const DEFAULT_POLICY_PLATFORM: IPlatformString = ""; +const DEFAULT_POLICY_PLATFORM: SelectedPlatformString = ""; export const DEFAULT_POLICY = { id: 1, diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index f6d46297ff..348952b6b9 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -2,10 +2,10 @@ import React, { useContext, useCallback, useEffect, - useMemo, useState, + useMemo, } from "react"; -import { RouteProps, InjectedRouter } from "react-router"; +import { InjectedRouter } from "react-router"; import { useQuery } from "react-query"; import { pick } from "lodash"; @@ -13,48 +13,52 @@ import { AppContext } from "context/app"; import { TableContext } from "context/table"; import { NotificationContext } from "context/notification"; import { performanceIndicator } from "utilities/helpers"; -import { IOsqueryPlatform } from "interfaces/platform"; -import { IQuery, IFleetQueriesResponse } from "interfaces/query"; -import fleetQueriesAPI from "services/entities/queries"; +import { SupportedPlatform } from "interfaces/platform"; +import { API_ALL_TEAMS_ID } from "interfaces/team"; +import { + IEnhancedQuery, + IQueryKeyQueriesLoadAll, + ISchedulableQuery, +} from "interfaces/schedulable_query"; +import queriesAPI from "services/entities/queries"; import PATHS from "router/paths"; -import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; import checkPlatformCompatibility from "utilities/sql_tools"; import Button from "components/buttons/Button"; import Spinner from "components/Spinner"; import TableDataError from "components/DataError"; import MainContent from "components/MainContent"; +import TeamsDropdown from "components/TeamsDropdown"; +import useTeamIdParam from "hooks/useTeamIdParam"; +import RevealButton from "components/buttons/RevealButton"; import QueriesTable from "./components/QueriesTable"; import DeleteQueryModal from "./components/DeleteQueryModal"; +import ManageAutomationsModal from "./components/ManageAutomationsModal/ManageAutomationsModal"; +import PreviewDataModal from "./components/PreviewDataModal/PreviewDataModal"; const baseClass = "manage-queries-page"; interface IManageQueriesPageProps { - route: RouteProps; router: InjectedRouter; // v3 location: { - pathname?: string; + pathname: string; query: { platform?: string; page?: string; query?: string; order_key?: string; order_direction?: "asc" | "desc"; + team_id?: string; }; search: string; }; } -interface IQueryTableData extends IQuery { - performance: string; - platforms: string[]; -} - -const getPlatforms = (queryString: string): Array => { +const getPlatforms = (queryString: string): SupportedPlatform[] => { const { platforms } = checkPlatformCompatibility(queryString); - return platforms || [DEFAULT_EMPTY_CELL_VALUE]; + return platforms ?? []; }; -const enhanceQuery = (q: IQuery) => { +const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { return { ...q, performance: performanceIndicator( @@ -71,51 +75,101 @@ const ManageQueriesPage = ({ const queryParams = location.query; const { + isGlobalAdmin, + isTeamAdmin, isOnlyObserver, isObserverPlus, isAnyTeamObserverPlus, + isOnGlobalTeam, setFilteredQueriesPath, filteredQueriesPath, + isPremiumTier, + isSandboxMode, + config, } = useContext(AppContext); const { setResetSelectedRows } = useContext(TableContext); const { renderFlash } = useContext(NotificationContext); - const [queriesList, setQueriesList] = useState( - null - ); + const { + userTeams, + currentTeamId, + handleTeamChange, + teamIdForApi, + isRouteOk, + } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); + + const isAnyTeamSelected = currentTeamId !== -1; + const [selectedQueryIds, setSelectedQueryIds] = useState([]); const [showDeleteQueryModal, setShowDeleteQueryModal] = useState(false); + const [showManageAutomationsModal, setShowManageAutomationsModal] = useState( + false + ); + const [showPreviewDataModal, setShowPreviewDataModal] = useState(false); const [isUpdatingQueries, setIsUpdatingQueries] = useState(false); + const [showInheritedQueries, setShowInheritedQueries] = useState(false); + const [isUpdatingAutomations, setIsUpdatingAutomations] = useState(false); const { - data: fleetQueries, - error: fleetQueriesError, - isFetching: isFetchingFleetQueries, - refetch: refetchFleetQueries, - } = useQuery( - "fleet queries by platform", - () => fleetQueriesAPI.loadAll(), + data: curTeamEnhancedQueries, + error: curTeamQueriesError, + isFetching: isFetchingCurTeamQueries, + refetch: refetchCurTeamQueries, + } = useQuery< + IEnhancedQuery[], + Error, + IEnhancedQuery[], + IQueryKeyQueriesLoadAll[] + >( + [{ scope: "queries", teamId: teamIdForApi }], + ({ queryKey: [{ teamId }] }) => + queriesAPI.loadAll(teamId).then(({ queries }) => { + return queries.map(enhanceQuery); + }), { refetchOnWindowFocus: false, - select: (data: IFleetQueriesResponse) => data.queries, + enabled: isRouteOk, + staleTime: 5000, } ); - const enhancedQueriesList = useMemo(() => { - const enhancedQueries = fleetQueries?.map((q: IQuery) => { - const query = enhanceQuery(q); - return query; - }); - - return enhancedQueries || []; - }, [fleetQueries]); - - useEffect(() => { - if (!isFetchingFleetQueries && enhancedQueriesList) { - setQueriesList(enhancedQueriesList); + // If a team is selected, inherit global queries + const { + data: globalEnhancedQueries, + error: globalQueriesError, + isFetching: isFetchingGlobalQueries, + refetch: refetchGlobalQueries, + } = useQuery< + IEnhancedQuery[], + Error, + IEnhancedQuery[], + IQueryKeyQueriesLoadAll[] + >( + [{ scope: "queries", teamId: API_ALL_TEAMS_ID }], + ({ queryKey: [{ teamId }] }) => + queriesAPI.loadAll(teamId).then(({ queries }) => { + return queries.map(enhanceQuery); + }), + { + refetchOnWindowFocus: false, + enabled: isRouteOk && isAnyTeamSelected, + staleTime: 5000, } - }, [enhancedQueriesList, isFetchingFleetQueries]); + ); + + const automatedQueryIds = useMemo(() => { + return curTeamEnhancedQueries + ? curTeamEnhancedQueries + .filter((query) => query.automations_enabled) + .map((query) => query.id) + : []; + }, [curTeamEnhancedQueries]); useEffect(() => { const path = location.pathname + location.search; @@ -124,7 +178,7 @@ const ManageQueriesPage = ({ } }, [location, filteredQueriesPath, setFilteredQueriesPath]); - const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY); + const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY(currentTeamId)); const toggleDeleteQueryModal = useCallback(() => { setShowDeleteQueryModal(!showDeleteQueryModal); @@ -135,80 +189,205 @@ const ManageQueriesPage = ({ setSelectedQueryIds(selectedTableQueryIds); }; - const onDeleteQuerySubmit = useCallback(async () => { - const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries"; + const refetchAllQueries = useCallback(() => { + refetchCurTeamQueries(); + refetchGlobalQueries(); + }, [refetchCurTeamQueries, refetchGlobalQueries]); + const toggleManageAutomationsModal = useCallback(() => { + setShowManageAutomationsModal(!showManageAutomationsModal); + }, [showManageAutomationsModal, setShowManageAutomationsModal]); + + const onManageAutomationsClick = () => { + toggleManageAutomationsModal(); + }; + + const togglePreviewDataModal = useCallback(() => { + // Manage automation modal must close/open every time preview data modal opens/closes + setShowManageAutomationsModal(!showManageAutomationsModal); + setShowPreviewDataModal(!showPreviewDataModal); + }, [ + showPreviewDataModal, + setShowPreviewDataModal, + showManageAutomationsModal, + setShowManageAutomationsModal, + ]); + + const onDeleteQuerySubmit = useCallback(async () => { + const bulk = selectedQueryIds.length > 1; setIsUpdatingQueries(true); - const deleteQueries = selectedQueryIds.map((id) => - fleetQueriesAPI.destroy(id) - ); - try { - await Promise.all(deleteQueries).then(() => { - renderFlash("success", `Successfully deleted ${queryOrQueries}.`); - setResetSelectedRows(true); - refetchFleetQueries(); - }); - renderFlash("success", `Successfully deleted ${queryOrQueries}.`); + if (bulk) { + await queriesAPI.bulkDestroy(selectedQueryIds); + } else { + await queriesAPI.destroy(selectedQueryIds[0]); + } + renderFlash( + "success", + `Successfully deleted ${bulk ? "queries" : "query"}.` + ); + setResetSelectedRows(true); + refetchAllQueries(); } catch (errorResponse) { renderFlash( "error", - `There was an error deleting your ${queryOrQueries}. Please try again later.` + `There was an error deleting your ${ + bulk ? "queries" : "query" + }. Please try again later.` ); } finally { toggleDeleteQueryModal(); setIsUpdatingQueries(false); } - }, [refetchFleetQueries, selectedQueryIds, toggleDeleteQueryModal]); + }, [refetchAllQueries, selectedQueryIds, toggleDeleteQueryModal]); - const isTableDataLoading = isFetchingFleetQueries || queriesList === null; - - return ( - -
-
-
-
-

- Queries -

-
-
- {(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) && - !!fleetQueries?.length && ( -
- -
- )} -
-
-

Manage queries to ask specific questions about your devices.

-
-
- {isTableDataLoading && !fleetQueriesError && } - {!isTableDataLoading && fleetQueriesError ? ( - - ) : ( - { + if (isPremiumTier) { + if (userTeams) { + if (userTeams.length > 1 || isOnGlobalTeam) { + return ( + - )} -
+ ); + } else if (!isOnGlobalTeam && userTeams.length === 1) { + return

{userTeams[0].name}

; + } + } + } + return

Queries

; + }; + + const renderCurrentScopeQueriesTable = () => { + if (isFetchingCurTeamQueries) { + return ; + } + if (curTeamQueriesError) { + return ; + } + return ( + + ); + }; + + const renderShowInheritedQueriesTableButton = () => { + const inheritedQueryCount = globalEnhancedQueries?.length; + return ( + schedule run on this team’s hosts.' + } + onClick={() => { + setShowInheritedQueries(!showInheritedQueries); + }} + /> + ); + }; + + const renderInheritedQueriesTable = () => { + if (isFetchingGlobalQueries) { + return ; + } + if (globalQueriesError) { + return ; + } + return ( + + ); + }; + + const renderInheritedQueriesSection = () => { + return ( + <> + {renderShowInheritedQueriesTableButton()} + {showInheritedQueries && renderInheritedQueriesTable()} + + ); + }; + + const onSaveQueryAutomations = useCallback( + async (newAutomatedQueryIds) => { + setIsUpdatingAutomations(true); + + // Query ids added to turn on automations + const turnOnAutomations = newAutomatedQueryIds.filter( + (query: number) => !automatedQueryIds.includes(query) + ); + // Query ids removed to turn off automations + const turnOffAutomations = automatedQueryIds.filter( + (query: number) => !newAutomatedQueryIds.includes(query) + ); + + // Update query automations using queries/{id} manage_automations parameter + const updateAutomatedQueries: Promise[] = []; + turnOnAutomations.map((id: number) => + updateAutomatedQueries.push( + queriesAPI.update(id, { automations_enabled: true }) + ) + ); + turnOffAutomations.map((id: number) => + updateAutomatedQueries.push( + queriesAPI.update(id, { automations_enabled: false }) + ) + ); + + try { + await Promise.all(updateAutomatedQueries).then(() => { + renderFlash("success", `Successfully updated query automations.`); + refetchAllQueries(); + }); + } catch (errorResponse) { + renderFlash( + "error", + `There was an error updating your query automations. Please try again later.` + ); + } finally { + toggleManageAutomationsModal(); + setIsUpdatingAutomations(false); + } + }, + [refetchAllQueries, automatedQueryIds, toggleManageAutomationsModal] + ); + + // const isTableDataLoading = isFetchingFleetQueries || queriesList === null; + + const renderModals = () => { + return ( + <> {showDeleteQueryModal && ( )} + {showManageAutomationsModal && ( + + )} + {showPreviewDataModal && ( + + )} + + ); + }; + + return ( + +
+
+
+
+
{renderHeader()}
+
+
+
+ {(isGlobalAdmin || isTeamAdmin) && ( + + )} + {(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) && + !!curTeamEnhancedQueries?.length && ( + <> + + + )} +
+
+
+

+ Manage and schedule queries to ask questions and collect telemetry + for all hosts{isAnyTeamSelected && " assigned to this team"}. +

+
+ {renderCurrentScopeQueriesTable()} + {isAnyTeamSelected && + globalEnhancedQueries && + globalEnhancedQueries?.length > 0 && + renderInheritedQueriesSection()} + {renderModals()}
); diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index d88be01d9c..2eb5f294fc 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -56,14 +56,17 @@ &__action-button-container { display: flex; - align-items: flex-start; - } - - .form-field--dropdown { - margin: 0; + gap: $pad-small; } .queries-table { + .controls { + .form-field { + &--dropdown { + margin: 0; + } + } + } &__platform-dropdown { width: 159px; @@ -95,99 +98,100 @@ } .data-table-block { - .data-table__table { - thead { - .name__header { - width: auto; - } - .platforms__header { - width: $col-sm; - } - .author_name__header { - display: none; - width: 0; - } - .updated_at__header { - display: none; - width: 0; - } - @media (min-width: $break-md) { - .author_name__header { - display: table-cell; + .data-table { + &__wrapper { + overflow-x: scroll; + overflow-y: hidden; + } + &__table { + thead { + .name__header { width: auto; } - } - @media (min-width: $break-lg) { - .author_name__header { - width: $col-md; + .platforms__header { + width: $col-sm; } .updated_at__header { - display: table-cell; - width: auto; + display: none; + width: 0; } - } - } - tbody { - .name__cell { - max-width: $col-lg; - - .children-wrapper { - display: flex; - gap: $pad-xsmall; - - .observer-can-run-tooltip { - font-weight: $regular; + .performance__header { + display: none; + width: 0; + @media (min-width: $break-md) { + display: table-cell; + width: auto; + } + } + @media (min-width: $break-lg) { + .author_name__header { + width: $col-md; + } + .updated_at__header { + display: table-cell; + width: auto; } } } - - @media (max-width: $break-md) { + tbody { .name__cell { - .w400 { - max-width: calc(400px - 81px); + max-width: $col-lg; + + .children-wrapper { + display: flex; + gap: $pad-xsmall; + + .observer-can-run-tooltip { + font-weight: $regular; + } } } - } - .platforms__cell { - max-width: $col-md; - } - .author_name__cell { - display: none; - max-width: $col-md; - img, - div, - span { - display: flex; - align-items: center; + + @media (max-width: $break-md) { + .name__cell { + .w400 { + max-width: calc(400px - 81px); + } + } } - div { - padding-right: $pad-small; + .platforms__cell { + max-width: $col-md; } - .author-name { - display: block; - } - } - .updated_at__cell { - display: none; - max-width: $col-md; - } - @media (min-width: $break-md) { - .author_name__cell { - display: table-cell; - } - } - @media (min-width: $break-lg) { .updated_at__cell { - display: table-cell; + display: none; + max-width: $col-md; + } + .performance__cell { + display: none; + max-width: $col-md; + } + @media (min-width: $break-md) { + .performance__cell { + display: table-cell; + } + } + @media (min-width: $break-lg) { + .updated_at__cell { + display: table-cell; + } } } } } } + .query-name-cell { + .children-wrapper { + .query-name-text { + text-overflow: ellipsis; + overflow: hidden; + } + } + } .query-icon { position: relative; top: 2px; + display: block; } } } diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx new file mode 100644 index 0000000000..ff49667d43 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import Modal from "components/Modal"; + +const baseClass = "automations-modal"; + +interface IAutomationsModalProps { + onExit: () => void; +} + +const AutomationsModal = ({ onExit }: IAutomationsModalProps): JSX.Element => { + return ( + +
+ + ); +}; + +export default AutomationsModal; diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx new file mode 100644 index 0000000000..0e9d5daae1 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -0,0 +1,208 @@ +import React, { useState, useEffect } from "react"; +import { omit } from "lodash"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; +import InfoBanner from "components/InfoBanner/InfoBanner"; +import CustomLink from "components/CustomLink/CustomLink"; +import Checkbox from "components/forms/fields/Checkbox/Checkbox"; +import QueryFrequencyIndicator from "components/QueryFrequencyIndicator/QueryFrequencyIndicator"; +import LogDestinationIndicator from "components/LogDestinationIndicator/LogDestinationIndicator"; + +import { ISchedulableQuery } from "interfaces/schedulable_query"; + +interface IManageAutomationsModalProps { + isUpdatingAutomations: boolean; + handleSubmit: (formData: any) => void; // TODO + onCancel: () => void; + togglePreviewDataModal: () => void; + availableQueries?: ISchedulableQuery[]; + automatedQueryIds: number[]; + logDestination: string; +} + +interface ICheckedQuery { + name?: string; + id: number; + isChecked: boolean; + interval: number; +} + +const useCheckboxListStateManagement = ( + allQueries: ISchedulableQuery[], + automatedQueryIds: number[] | undefined +) => { + const [queryItems, setQueryItems] = useState(() => { + return allQueries.map(({ name, id, interval }) => ({ + name, + id, + isChecked: !!automatedQueryIds?.includes(id), + interval, + })); + }); + + const updateQueryItems = (queryId: number) => { + setQueryItems((prevItems) => + prevItems.map((query) => + query.id !== queryId ? query : { ...query, isChecked: !query.isChecked } + ) + ); + }; + + return { queryItems, updateQueryItems }; +}; + +const baseClass = "manage-automations-modal"; + +const ManageAutomationsModal = ({ + isUpdatingAutomations, + automatedQueryIds, + handleSubmit, + onCancel, + togglePreviewDataModal, + availableQueries, + logDestination, +}: IManageAutomationsModalProps): JSX.Element => { + // TODO: Error handling, if any + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + + // Client side sort queries alphabetically + const sortedAvailableQueries = + availableQueries?.sort((a, b) => + a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + ) || []; + + const { queryItems, updateQueryItems } = useCheckboxListStateManagement( + sortedAvailableQueries, + automatedQueryIds || [] + ); + + const onSubmit = (evt: React.MouseEvent | KeyboardEvent) => { + evt.preventDefault(); + + const newQueryIds: number[] = []; + queryItems?.forEach((p) => p.isChecked && newQueryIds.push(p.id)); + + handleSubmit(newQueryIds); + }; + + useEffect(() => { + const listener = (event: KeyboardEvent) => { + if (event.code === "Enter" || event.code === "NumpadEnter") { + event.preventDefault(); + onSubmit(event); + } + }; + document.addEventListener("keydown", listener); + return () => { + document.removeEventListener("keydown", listener); + }; + }); + + return ( + +
+
+ Query automations let you send data to your log destination on a + schedule. Data is sent according to a query’s frequency. +
+ {availableQueries?.length ? ( +
+

+ Choose which queries will send data: +

+
+ {queryItems && + queryItems.map((queryItem) => { + const { isChecked, name, id, interval } = queryItem; + return ( +
+ { + updateQueryItems(id); + !isChecked && + setErrors((errs) => omit(errs, "queryItems")); + }} + > + {name} + + +
+ ); + })} +
+
+ ) : ( +
+ You have no queries. +

Add a query to turn on automations.

+
+ )} +
+

+ Log destination: +

+
+ +
+
+ Users with the admin role can  + +
+
+ +

Automations currently run on macOS, Windows, and Linux hosts.

+

+ Interested in query automations for your Chromebooks?   + +

+
+
+
+ +
+
+ + +
+
+
+
+ ); +}; + +export default ManageAutomationsModal; diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss new file mode 100644 index 0000000000..78a5f043f7 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss @@ -0,0 +1,65 @@ +.manage-automations-modal { + display: flex; + flex-direction: column; + gap: $pad-xlarge; + + &__selection { + margin-bottom: $pad-small; + } + + &__checkboxes { + display: flex; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + border-radius: 4px; + border: 1px solid $ui-fleet-black-10; + } + + &__query-item { + width: 100%; + display: flex; + justify-content: space-between; + + &:not(:last-child) { + border-bottom: 1px solid $ui-fleet-black-10; + } + } + + &__configure { + color: $ui-fleet-black-75; + } + + .info-banner { + &__info { + display: flex; + flex-direction: column; + gap: 8px; + p { + margin: 0; + } + } + } + + .fleet-checkbox { + height: 20px; + display: flex; + align-items: center; + + &__label { + width: 490px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .form-field--checkbox { + display: flex; + padding: 8px 12px; + justify-content: space-between; + align-items: center; + align-self: stretch; + margin-bottom: 0; + } +} diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts new file mode 100644 index 0000000000..c9128e3c2d --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts @@ -0,0 +1 @@ +export { default } from "./ManageAutomationsModal"; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx similarity index 100% rename from frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx rename to frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx diff --git a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts similarity index 100% rename from frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts rename to frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index b36eee3747..f5ad0b06f6 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -39,11 +39,13 @@ interface IQueriesTableProps { query?: string; order_key?: string; order_direction?: "asc" | "desc"; + team_id?: string; }; + isInherited?: boolean; } -const DEFAULT_SORT_DIRECTION = "desc"; -const DEFAULT_SORT_HEADER = "updated_at"; +const DEFAULT_SORT_DIRECTION = "asc"; +const DEFAULT_SORT_HEADER = "name"; const DEFAULT_PAGE_SIZE = 20; const DEFAULT_PLATFORM = "all"; @@ -88,16 +90,16 @@ const QueriesTable = ({ isOnlyObserver, isObserverPlus, isAnyTeamObserverPlus, - queryParams, router, + queryParams, + isInherited = false, }: IQueriesTableProps): JSX.Element | null => { const { currentUser } = useContext(AppContext); // Functions to avoid race conditions const initialSearchQuery = (() => queryParams?.query ?? "")(); const initialSortHeader = (() => - (queryParams?.order_key as "updated_at" | "name" | "author") ?? - "updated_at")(); + (queryParams?.order_key as "name" | "updated_at" | "author") ?? "name")(); const initialSortDirection = (() => (queryParams?.order_direction as "asc" | "desc") ?? "asc")(); const initialPlatform = (() => @@ -143,6 +145,7 @@ const QueriesTable = ({ ) { newQueryParams.page = 0; } + newQueryParams.team_id = queryParams?.team_id; const locationPath = getNextLocationPath({ pathPrefix: PATHS.MANAGE_QUERIES, queryParams: newQueryParams, @@ -233,19 +236,21 @@ const QueriesTable = ({ }; const tableHeaders = useMemo( - () => currentUser && generateTableHeaders({ currentUser }), - [currentUser] + () => currentUser && generateTableHeaders({ currentUser, isInherited }), + [currentUser, isInherited] ); - const searchable = !(queriesList?.length === 0 && searchQuery === ""); + const searchable = + !(queriesList?.length === 0 && searchQuery === "") && !isInherited; return tableHeaders && !isLoading ? (
) : ( diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 26e5bcc58f..744a829bc6 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -7,12 +7,15 @@ import formatDistanceToNow from "date-fns/formatDistanceToNow"; import PATHS from "router/paths"; import permissionsUtils from "utilities/permissions"; -import { IQuery } from "interfaces/query"; import { IUser } from "interfaces/user"; -import { addGravatarUrlToResource } from "utilities/helpers"; +import { secondsToDhms } from "utilities/helpers"; +import { + IEnhancedQuery, + ISchedulableQuery, +} from "interfaces/schedulable_query"; +import { SupportedPlatform } from "interfaces/platform"; import Icon from "components/Icon"; -import Avatar from "components/Avatar"; import Checkbox from "components/forms/fields/Checkbox"; import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; @@ -20,10 +23,12 @@ import PlatformCell from "components/TableContainer/DataTable/PlatformCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; import PillCell from "components/TableContainer/DataTable/PillCell"; import TooltipWrapper from "components/TooltipWrapper"; +import { COLORS } from "styles/var/colors"; +import QueryAutomationsStatusIndicator from "../QueryAutomationsStatusIndicator"; interface IQueryRow { id: string; - original: IQuery; + original: ISchedulableQuery; } interface IGetToggleAllRowsSelectedProps { @@ -46,7 +51,7 @@ interface IHeaderProps { } interface IRowProps { row: { - original: IQuery; + original: IEnhancedQuery; getToggleRowSelectedProps: () => IGetToggleAllRowsSelectedProps; toggleRowSelected: () => void; }; @@ -55,13 +60,26 @@ interface IRowProps { interface ICellProps extends IRowProps { cell: { - value: string; + value: string | number | boolean; }; } +interface INumberCellProps extends IRowProps { + cell: { + value: number; + }; +} + +interface IStringCellProps extends IRowProps { + cell: { value: string }; +} + +interface IBoolCellProps extends IRowProps { + cell: { value: boolean }; +} interface IPlatformCellProps extends IRowProps { cell: { - value: string[]; + value: SupportedPlatform[]; }; } @@ -69,7 +87,10 @@ interface IDataColumn { Header: ((props: IHeaderProps) => JSX.Element) | string; Cell: | ((props: ICellProps) => JSX.Element) - | ((props: IPlatformCellProps) => JSX.Element); + | ((props: IPlatformCellProps) => JSX.Element) + | ((props: IStringCellProps) => JSX.Element) + | ((props: INumberCellProps) => JSX.Element) + | ((props: IBoolCellProps) => JSX.Element); id?: string; title?: string; accessor?: string; @@ -80,17 +101,16 @@ interface IDataColumn { interface IGenerateTableHeaders { currentUser: IUser; + isInherited?: boolean; } // NOTE: cellProps come from react-table // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties const generateTableHeaders = ({ currentUser, + isInherited = false, }: IGenerateTableHeaders): IDataColumn[] => { const isOnlyObserver = permissionsUtils.isOnlyObserver(currentUser); - const isAnyTeamMaintainerOrTeamAdmin = permissionsUtils.isAnyTeamMaintainerOrTeamAdmin( - currentUser - ); const tableHeaders: IDataColumn[] = [ { @@ -105,7 +125,7 @@ const generateTableHeaders = ({ Cell: (cellProps: ICellProps): JSX.Element => { return (
{cellProps.cell.value}
@@ -124,7 +144,7 @@ const generateTableHeaders = ({ type="dark" effect="solid" id={`observer-can-run-tooltip-${cellProps.row.original.id}`} - backgroundColor="#3e4771" + backgroundColor={COLORS["tooltip-bg"]} > Observers can run this query. @@ -132,7 +152,10 @@ const generateTableHeaders = ({ )} } - path={PATHS.EDIT_QUERY(cellProps.row.original)} + path={PATHS.EDIT_QUERY( + cellProps.row.original.id, + cellProps.row.original.team_id ?? undefined + )} /> ); }, @@ -140,38 +163,37 @@ const generateTableHeaders = ({ }, { title: "Platform", - Header: "Platform", + Header: "Compatible with", disableSortBy: true, accessor: "platforms", Cell: (cellProps: IPlatformCellProps): JSX.Element => { - return ; + return ; }, }, { - title: "Author", - Header: (cellProps) => ( - - ), - accessor: "author_name", - Cell: (cellProps: ICellProps): JSX.Element => { - const { author_name, author_email } = cellProps.row.original; - const author = author_name === currentUser.name ? "You" : author_name; + title: "Frequency", + Header: "Frequency", + disableSortBy: true, + accessor: "interval", + Cell: (cellProps: INumberCellProps): JSX.Element => { + const val = cellProps.cell.value + ? `Every ${secondsToDhms(cellProps.cell.value)}` + : undefined; return ( - - - {author} - + + Assign a frequency and turn automations on to + collect data at an interval. + + } + /> ); }, - sortType: "caseInsensitive", }, { + title: "Performance impact", Header: () => { return (
@@ -189,7 +211,7 @@ const generateTableHeaders = ({ }, disableSortBy: true, accessor: "performance", - Cell: (cellProps: ICellProps) => ( + Cell: (cellProps: IStringCellProps) => ( ), }, + { + title: "Automations", + Header: "Automations", + disableSortBy: true, + accessor: "automations_enabled", + Cell: (cellProps: IBoolCellProps): JSX.Element => { + return ( + + ); + }, + }, { title: "Last modified", Header: (cellProps) => ( @@ -207,7 +243,7 @@ const generateTableHeaders = ({ /> ), accessor: "updated_at", - Cell: (cellProps: ICellProps): JSX.Element => ( + Cell: (cellProps: INumberCellProps): JSX.Element => ( { const { getToggleAllRowsSelectedProps, - rows, - selectedFlatRows, toggleAllRowsSelected, - toggleRowSelected, } = cellProps; const { checked, indeterminate } = getToggleAllRowsSelectedProps(); - const disableToggleAllRowsSelected = () => { - /* Team admin or team maintainer can only delete queries they authored - If team admin or team maintainer authored 0 queries, disable select all queries for deletion */ - if (isAnyTeamMaintainerOrTeamAdmin) { - return ( - rows.filter( - (r: IQueryRow) => r.original.author_id === currentUser.id - ).length === 0 - ); - } - return false; - }; - const checkboxProps = { value: checked, indeterminate, - disabled: disableToggleAllRowsSelected(), // Disable select all if all rows are disabled onChange: () => { - if (!isAnyTeamMaintainerOrTeamAdmin) { - toggleAllRowsSelected(); - } else { - // Team maintainers may only delete the queries that they have authored - // so we need to do some filtering and then modify the toggle select all - // behavior for the header checkbox - const userAuthoredQueries = rows.filter( - (r: IQueryRow) => r.original.author_id === currentUser.id - ); - if ( - selectedFlatRows.length && - selectedFlatRows.length !== userAuthoredQueries.length - ) { - // If some but not all of the user authored queries are already selected, - // we toggle all of the user's unselected queries to true - userAuthoredQueries.forEach((r: IQueryRow) => - toggleRowSelected(r.id, true) - ); - } else { - // Otherwise, we toggle all of the user's queries to the opposite of their current state - userAuthoredQueries.forEach((r: IQueryRow) => - toggleRowSelected(r.id) - ); - } - } + toggleAllRowsSelected(); }, }; return ; @@ -283,44 +278,9 @@ const generateTableHeaders = ({ const checkboxProps = { value: checked, onChange: () => row.toggleRowSelected(), - disabled: - isAnyTeamMaintainerOrTeamAdmin && - row.original.author_id !== currentUser.id, }; - // If the user is a team maintainer, we only enable checkboxes for queries - // that they authored and we include a tooltip to explain disabled checkboxes - return ( - <> -
- -
{" "} - - <> - You can only delete a
query if you are the author. - -
- - ); + // v4.35.0 Any team admin or maintainer now can add, edit, delete their team's queries + return ; }, disableHidden: true, }); diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator.tsx new file mode 100644 index 0000000000..34edebdf79 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator.tsx @@ -0,0 +1,44 @@ +import StatusIndicator from "components/StatusIndicator"; +import React from "react"; + +interface IQueryAutomationsStatusIndicator { + automationsEnabled: boolean; + interval: number; +} + +const QueryAutomationsStatusIndicator = ({ + automationsEnabled, + interval, +}: IQueryAutomationsStatusIndicator) => { + let status; + if (automationsEnabled) { + if (interval === 0) { + status = "paused"; + } else { + status = "on"; + } + } else { + status = "off"; + } + + const tooltip = + status === "paused" + ? { + tooltipText: ( + <> + Automations will resume for this query when a + frequency is set. + + ), + } + : undefined; + return ( + + ); +}; + +export default QueryAutomationsStatusIndicator; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/_styles.scss b/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/_styles.scss new file mode 100644 index 0000000000..ba531246a7 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/_styles.scss @@ -0,0 +1,21 @@ +.status-indicator { + // Query automations status + &--query-automations-on { + &:before { + background-color: $ui-success; + } + } + &--query-automations-off { + &:before { + background-color: $ui-offline; + } + } + &--query-automations-paused { + .status-tooltip { + text-transform: none; + } + &:before { + background-color: $ui-offline; + } + } +} diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/index.ts b/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/index.ts new file mode 100644 index 0000000000..e2d4e68a35 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/index.ts @@ -0,0 +1 @@ +export { default } from "./QueryAutomationsStatusIndicator"; diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx index 75f636686a..ca25321e57 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tsx @@ -12,7 +12,10 @@ import statusAPI from "services/entities/status"; import { IHost, IHostResponse } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { ITeam } from "interfaces/team"; -import { IQueryFormData, IQuery, IStoredQueryResponse } from "interfaces/query"; +import { + IGetQueryResponse, + ISchedulableQuery, +} from "interfaces/schedulable_query"; import { ITarget } from "interfaces/target"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; @@ -23,12 +26,15 @@ 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: { - query: { host_ids: string }; + pathname: string; + query: { host_ids: string; team_id?: string }; + search: string; }; } @@ -37,9 +43,18 @@ const baseClass = "query-page"; const QueryPage = ({ router, params: { id: paramsQueryId }, - location: { query: URLQuerySearch }, + 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 { @@ -57,6 +72,10 @@ const QueryPage = ({ setLastEditedQueryDescription, setLastEditedQueryBody, setLastEditedQueryObserverCanRun, + setLastEditedQueryFrequency, + setLastEditedQueryLoggingType, + setLastEditedQueryMinOsqueryVersion, + setLastEditedQueryPlatforms, } = useContext(QueryContext); const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false); @@ -78,19 +97,23 @@ const QueryPage = ({ isLoading: isStoredQueryLoading, data: storedQuery, error: storedQueryError, - } = useQuery( + } = useQuery( ["query", queryId], () => queryAPI.load(queryId as number), { enabled: !!queryId, refetchOnWindowFocus: false, - select: (data: IStoredQueryResponse) => data.query, + 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), } @@ -99,9 +122,9 @@ const QueryPage = ({ useQuery( "hostFromURL", () => - hostAPI.loadHostDetails(parseInt(URLQuerySearch.host_ids as string, 10)), + hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)), { - enabled: !!URLQuerySearch.host_ids && !queryParamHostsAdded, + enabled: !!location.query.host_ids && !queryParamHostsAdded, select: (data: IHostResponse) => data.host, onSuccess: (host) => { setTargetedHosts((prevHosts) => @@ -119,10 +142,6 @@ const QueryPage = ({ } ); - const { mutateAsync: createQuery } = useMutation((formData: IQueryFormData) => - queryAPI.create(formData) - ); - const detectIsFleetQueryRunnable = () => { statusAPI.live_query().catch(() => { setIsLiveQueryRunnable(false); @@ -131,12 +150,18 @@ const QueryPage = ({ useEffect(() => { detectIsFleetQueryRunnable(); - setLastEditedQueryId(DEFAULT_QUERY.id); - setLastEditedQueryName(DEFAULT_QUERY.name); - setLastEditedQueryDescription(DEFAULT_QUERY.description); - setLastEditedQueryBody(DEFAULT_QUERY.query); - setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run); - }, []); + 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]); useEffect(() => { setShowOpenSchemaActionText(!isSidebarOpen); @@ -179,22 +204,23 @@ const QueryPage = ({ const goToQueryEditor = useCallback(() => setStep(QUERIES_PAGE_STEPS[1]), []); const renderScreen = () => { - const step1Opts = { + const step1Props = { router, baseClass, queryIdForEdit: queryId, + teamNameForQuery, + apiTeamIdForQuery, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, storedQueryError, - createQuery, onOsqueryTableSelect, goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]), onOpenSchemaSidebar, renderLiveQueryWarning, }; - const step2Opts = { + const step2Props = { baseClass, queryId, selectedTargets, @@ -211,7 +237,7 @@ const QueryPage = ({ setTargetsTotalCount, }; - const step3Opts = { + const step3Props = { queryId, selectedTargets, storedQuery, @@ -222,11 +248,11 @@ const QueryPage = ({ switch (step) { case QUERIES_PAGE_STEPS[2]: - return ; + return ; case QUERIES_PAGE_STEPS[3]: - return ; + return ; default: - return ; + return ; } }; diff --git a/frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx b/frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx deleted file mode 100644 index 836450c456..0000000000 --- a/frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { size } from "lodash"; - -import { IQueryFormData } from "interfaces/query"; -import useDeepEffect from "hooks/useDeepEffect"; - -import Checkbox from "components/forms/fields/Checkbox"; -// @ts-ignore -import InputField from "components/forms/fields/InputField"; -import Button from "components/buttons/Button"; -import Modal from "components/Modal"; - -export interface INewQueryModalProps { - baseClass: string; - queryValue: string; - isLoading: boolean; - onCreateQuery: (formData: IQueryFormData) => void; - setIsSaveModalOpen: (isOpen: boolean) => void; - backendValidators: { [key: string]: string }; -} - -const validateQueryName = (name: string) => { - const errors: { [key: string]: string } = {}; - - if (!name) { - errors.name = "Query name must be present"; - } - - const valid = !size(errors); - return { valid, errors }; -}; - -const NewQueryModal = ({ - baseClass, - queryValue, - isLoading, - onCreateQuery, - setIsSaveModalOpen, - backendValidators, -}: INewQueryModalProps): JSX.Element => { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [observerCanRun, setObserverCanRun] = useState(false); - const [errors, setErrors] = useState<{ [key: string]: string }>( - backendValidators - ); - - useDeepEffect(() => { - if (name) { - setErrors({}); - } - }, [name]); - - useEffect(() => { - setErrors(backendValidators); - }, [backendValidators]); - - const handleUpdate = (evt: React.MouseEvent) => { - evt.preventDefault(); - - const { valid, errors: newErrors } = validateQueryName(name); - setErrors({ - ...errors, - ...newErrors, - }); - - if (valid) { - onCreateQuery({ - description, - name, - query: queryValue, - observer_can_run: observerCanRun, - }); - } - }; - - return ( - setIsSaveModalOpen(false)}> - <> -
- setName(value)} - value={name} - error={errors.name} - inputClassName={`${baseClass}__query-save-modal-name`} - label="Name" - placeholder="What is your query called?" - autofocus - /> - setDescription(value)} - value={description} - inputClassName={`${baseClass}__query-save-modal-description`} - label="Description" - type="textarea" - placeholder="What information does your query reveal? (optional)" - /> - - Observers can run - -

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

-
- - -
- - -
- ); -}; - -export default NewQueryModal; diff --git a/frontend/pages/queries/QueryPage/components/NewQueryModal/index.ts b/frontend/pages/queries/QueryPage/components/NewQueryModal/index.ts deleted file mode 100644 index acf83db4a9..0000000000 --- a/frontend/pages/queries/QueryPage/components/NewQueryModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./NewQueryModal"; diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index a3f7b80ad0..aaa32d89d3 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -1,17 +1,35 @@ -import React, { useState, useContext, useEffect, KeyboardEvent } from "react"; +import React, { + useState, + useContext, + useEffect, + KeyboardEvent, + useCallback, +} from "react"; import { InjectedRouter } from "react-router"; -import { size } from "lodash"; +import { pull, size } from "lodash"; import classnames from "classnames"; import { useDebouncedCallback } from "use-debounce"; +import { COLORS } from "styles/var/colors"; import PATHS from "router/paths"; import { AppContext } from "context/app"; import { QueryContext } from "context/query"; import { NotificationContext } from "context/notification"; import { addGravatarUrlToResource } from "utilities/helpers"; +import { + FREQUENCY_DROPDOWN_OPTIONS, + SCHEDULE_PLATFORM_DROPDOWN_OPTIONS, + MIN_OSQUERY_VERSION_OPTIONS, + LOGGING_TYPE_OPTIONS, +} from "utilities/constants"; import usePlatformCompatibility from "hooks/usePlatformCompatibility"; import { IApiError } from "interfaces/errors"; -import { IQuery, IQueryFormData } from "interfaces/query"; +import { + ISchedulableQuery, + ICreateQueryRequestBody, + QueryLoggingOption, +} from "interfaces/schedulable_query"; +import { SelectedPlatformString } from "interfaces/platform"; import queryAPI from "services/entities/queries"; import { IAceEditor } from "react-ace/lib/types"; @@ -23,10 +41,12 @@ import validateQuery from "components/forms/validators/validate_query"; import Button from "components/buttons/Button"; import RevealButton from "components/buttons/RevealButton"; import Checkbox from "components/forms/fields/Checkbox"; +// @ts-ignore +import Dropdown from "components/forms/fields/Dropdown"; import Spinner from "components/Spinner"; import Icon from "components/Icon/Icon"; import AutoSizeInputField from "components/forms/fields/AutoSizeInputField"; -import NewQueryModal from "../NewQueryModal"; +import SaveQueryModal from "../SaveQueryModal"; import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png"; const baseClass = "query-form"; @@ -34,15 +54,17 @@ const baseClass = "query-form"; interface IQueryFormProps { router: InjectedRouter; queryIdForEdit: number | null; + apiTeamIdForQuery?: number; + teamNameForQuery?: string; showOpenSchemaActionText: boolean; - storedQuery: IQuery | undefined; + storedQuery: ISchedulableQuery | undefined; isStoredQueryLoading: boolean; isQuerySaving: boolean; isQueryUpdating: boolean; - onCreateQuery: (formData: IQueryFormData) => void; + saveQuery: (formData: ICreateQueryRequestBody) => void; onOsqueryTableSelect: (tableName: string) => void; goToSelectTargets: () => void; - onUpdate: (formData: IQueryFormData) => void; + onUpdate: (formData: ICreateQueryRequestBody) => void; onOpenSchemaSidebar: () => void; renderLiveQueryWarning: () => JSX.Element | null; backendValidators: { [key: string]: string }; @@ -63,12 +85,14 @@ const validateQuerySQL = (query: string) => { const QueryForm = ({ router, queryIdForEdit, + apiTeamIdForQuery, + teamNameForQuery, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, isQuerySaving, isQueryUpdating, - onCreateQuery, + saveQuery, onOsqueryTableSelect, goToSelectTargets, onUpdate, @@ -84,10 +108,18 @@ const QueryForm = ({ lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, + lastEditedQueryFrequency, + lastEditedQueryPlatforms, + lastEditedQueryMinOsqueryVersion, + lastEditedQueryLoggingType, setLastEditedQueryName, setLastEditedQueryDescription, setLastEditedQueryBody, setLastEditedQueryObserverCanRun, + setLastEditedQueryFrequency, + setLastEditedQueryPlatforms, + setLastEditedQueryMinOsqueryVersion, + setLastEditedQueryLoggingType, } = useContext(QueryContext); const { @@ -104,13 +136,14 @@ const QueryForm = ({ const savedQueryMode = !!queryIdForEdit; const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined - const [isSaveModalOpen, setIsSaveModalOpen] = useState(false); + const [showSaveQueryModal, setShowSaveQueryModal] = useState(false); const [showQueryEditor, setShowQueryEditor] = useState( isObserverPlus || isAnyTeamObserverPlus || false ); const [isEditingName, setIsEditingName] = useState(false); const [isEditingDescription, setIsEditingDescription] = useState(false); const [isSaveAsNewLoading, setIsSaveAsNewLoading] = useState(false); + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const platformCompatibility = usePlatformCompatibility(); const { setCompatiblePlatforms } = platformCompatibility; @@ -133,7 +166,7 @@ const QueryForm = ({ } debounceSQL(lastEditedQueryBody); - }, [lastEditedQueryBody, lastEditedQueryId]); + }, [lastEditedQueryBody, lastEditedQueryId, isStoredQueryLoading]); const hasTeamMaintainerPermissions = savedQueryMode ? isAnyTeamMaintainerOrTeamAdmin && @@ -142,6 +175,10 @@ const QueryForm = ({ storedQuery.author_id === currentUser.id : isAnyTeamMaintainerOrTeamAdmin; + const toggleSaveQueryModal = () => { + setShowSaveQueryModal(!showSaveQueryModal); + }; + const onLoad = (editor: IAceEditor) => { editor.setOptions({ enableLinking: true, @@ -173,6 +210,50 @@ const QueryForm = ({ } }; + const onChangeSelectFrequency = useCallback( + (value: number) => { + setLastEditedQueryFrequency(value); + }, + [setLastEditedQueryFrequency] + ); + + const toggleAdvancedOptions = () => { + setShowAdvancedOptions(!showAdvancedOptions); + }; + + const onChangeSelectPlatformOptions = useCallback( + (values: string) => { + const valArray = values.split(","); + + // Remove All if another OS is chosen + // else if Remove OS if All is chosen + if (valArray.indexOf("") === 0 && valArray.length > 1) { + setLastEditedQueryPlatforms( + pull(valArray, "").join(",") as SelectedPlatformString + ); + } else if (valArray.length > 1 && valArray.indexOf("") > -1) { + setLastEditedQueryPlatforms(""); + } else { + setLastEditedQueryPlatforms(values as SelectedPlatformString); + } + }, + [setLastEditedQueryPlatforms] + ); + + const onChangeMinOsqueryVersionOptions = useCallback( + (value: string) => { + setLastEditedQueryMinOsqueryVersion(value); + }, + [setLastEditedQueryMinOsqueryVersion] + ); + + const onChangeSelectLoggingType = useCallback( + (value: QueryLoggingOption) => { + setLastEditedQueryLoggingType(value); + }, + [setLastEditedQueryLoggingType] + ); + const promptSaveAsNewQuery = () => ( evt: React.MouseEvent ) => { @@ -192,17 +273,21 @@ const QueryForm = ({ if (valid) { setIsSaveAsNewLoading(true); - queryAPI .create({ name: lastEditedQueryName, description: lastEditedQueryDescription, query: lastEditedQueryBody, + team_id: apiTeamIdForQuery, observer_can_run: lastEditedQueryObserverCanRun, + interval: lastEditedQueryFrequency, + platform: lastEditedQueryPlatforms, + min_osquery_version: lastEditedQueryMinOsqueryVersion, + logging: lastEditedQueryLoggingType, }) - .then((response: { query: IQuery }) => { + .then((response: { query: ISchedulableQuery }) => { setIsSaveAsNewLoading(false); - router.push(PATHS.EDIT_QUERY(response.query)); + router.push(PATHS.EDIT_QUERY(response.query.id)); renderFlash("success", `Successfully added query.`); }) .catch((createError: { data: IApiError }) => { @@ -212,11 +297,16 @@ const QueryForm = ({ name: `Copy of ${lastEditedQueryName}`, description: lastEditedQueryDescription, query: lastEditedQueryBody, + team_id: apiTeamIdForQuery, observer_can_run: lastEditedQueryObserverCanRun, + interval: lastEditedQueryFrequency, + platform: lastEditedQueryPlatforms, + min_osquery_version: lastEditedQueryMinOsqueryVersion, + logging: lastEditedQueryLoggingType, }) - .then((response: { query: IQuery }) => { + .then((response: { query: ISchedulableQuery }) => { setIsSaveAsNewLoading(false); - router.push(PATHS.EDIT_QUERY(response.query)); + router.push(PATHS.EDIT_QUERY(response.query.id)); renderFlash( "success", `Successfully added query as "Copy of ${lastEditedQueryName}".` @@ -228,9 +318,19 @@ const QueryForm = ({ "already exists" ) ) { + let teamErrorText; + if (apiTeamIdForQuery !== 0) { + if (teamNameForQuery) { + teamErrorText = `the ${teamNameForQuery} team`; + } else { + teamErrorText = "this team"; + } + } else { + teamErrorText = "all teams"; + } renderFlash( "error", - `"Copy of ${lastEditedQueryName}" already exists. Please rename your query and try again.` + `A query called "Copy of ${lastEditedQueryName}" already exists for ${teamErrorText}.` ); } setIsSaveAsNewLoading(false); @@ -260,13 +360,17 @@ const QueryForm = ({ if (valid) { if (!savedQueryMode) { - setIsSaveModalOpen(true); + setShowSaveQueryModal(true); } else { onUpdate({ name: lastEditedQueryName, description: lastEditedQueryDescription, query: lastEditedQueryBody, observer_can_run: lastEditedQueryObserverCanRun, + interval: lastEditedQueryFrequency, + platform: lastEditedQueryPlatforms, + min_osquery_version: lastEditedQueryMinOsqueryVersion, + logging: lastEditedQueryLoggingType, }); } } @@ -445,7 +549,7 @@ const QueryForm = ({ variant="blue-green" onClick={goToSelectTargets} > - Run query + Live query
)} @@ -482,21 +586,72 @@ const QueryForm = ({ {renderPlatformCompatibility()} {savedQueryMode && ( - <> - - setLastEditedQueryObserverCanRun(value) - } - wrapperClassName={`${baseClass}__query-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. -

- +
+
+ + If automations are on, this is how often your query collects data. +
+
+ + setLastEditedQueryObserverCanRun(value) + } + wrapperClassName={`${baseClass}__query-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. +

+
+ + {showAdvancedOptions && ( +
+ + + +
+ )} +
)} {renderLiveQueryWarning()}
@@ -530,9 +687,11 @@ const QueryForm = ({ className="save-loading" variant="brand" onClick={promptSaveQuery()} + // Button disabled for team maintainer/admins viewing global queries disabled={ isAnyTeamMaintainerOrTeamAdmin && - !hasTeamMaintainerPermissions + !storedQuery?.team_id && + !!queryIdForEdit } isLoading={isQueryUpdating} > @@ -541,16 +700,15 @@ const QueryForm = ({
{" "} <> - You can only save -
changes to a query if you -
are the author. + You can only save changes +
to a team level query.
@@ -561,16 +719,16 @@ const QueryForm = ({ variant="blue-green" onClick={goToSelectTargets} > - Run query + Live query
- {isSaveModalOpen && ( - diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss b/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss index c53d661326..5a9ab0d49f 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss +++ b/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss @@ -154,6 +154,22 @@ font-size: $x-small; } + &__edit-options { + > div:not(:last-child) { + margin-bottom: $pad-large; + } + } + + &__frequency { + .form-field { + margin-bottom: $pad-small; + } + } + + &__advanced-options { + margin-top: $pad-medium; + } + &__query-observer-can-run-wrapper { margin: 0; margin-top: $pad-large; diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx b/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx new file mode 100644 index 0000000000..d559aa9866 --- /dev/null +++ b/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx @@ -0,0 +1,257 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { pull, size } from "lodash"; + +import useDeepEffect from "hooks/useDeepEffect"; + +import Checkbox from "components/forms/fields/Checkbox"; +// @ts-ignore +import InputField from "components/forms/fields/InputField"; +// @ts-ignore +import Dropdown from "components/forms/fields/Dropdown"; +import Button from "components/buttons/Button"; +import Modal from "components/Modal"; +import { + FREQUENCY_DROPDOWN_OPTIONS, + LOGGING_TYPE_OPTIONS, + MIN_OSQUERY_VERSION_OPTIONS, + SCHEDULE_PLATFORM_DROPDOWN_OPTIONS, +} from "utilities/constants"; +import RevealButton from "components/buttons/RevealButton"; +import { SelectedPlatformString } from "interfaces/platform"; +import { + ICreateQueryRequestBody, + ISchedulableQuery, + QueryLoggingOption, +} from "interfaces/schedulable_query"; + +const baseClass = "save-query-modal"; +export interface ISaveQueryModalProps { + queryValue: string; + apiTeamIdForQuery?: number; // query will be global if omitted + isLoading: boolean; + saveQuery: (formData: ICreateQueryRequestBody) => void; + toggleSaveQueryModal: () => void; + backendValidators: { [key: string]: string }; + existingQuery?: ISchedulableQuery; +} + +const validateQueryName = (name: string) => { + const errors: { [key: string]: string } = {}; + + if (!name) { + errors.name = "Query name must be present"; + } + + const valid = !size(errors); + return { valid, errors }; +}; + +const SaveQueryModal = ({ + queryValue, + apiTeamIdForQuery, + isLoading, + saveQuery, + toggleSaveQueryModal, + backendValidators, + existingQuery, +}: ISaveQueryModalProps): JSX.Element => { + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [selectedFrequency, setSelectedFrequency] = useState( + existingQuery?.interval ?? 3600 + ); + const [ + selectedPlatformOptions, + setSelectedPlatformOptions, + ] = useState(existingQuery?.platform ?? ""); + const [ + selectedMinOsqueryVersionOptions, + setSelectedMinOsqueryVersionOptions, + ] = useState(existingQuery?.min_osquery_version ?? ""); + const [ + selectedLoggingType, + setSelectedLoggingType, + ] = useState(existingQuery?.logging ?? "snapshot"); + const [observerCanRun, setObserverCanRun] = useState(false); + const [errors, setErrors] = useState<{ [key: string]: string }>( + backendValidators + ); + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + const toggleAdvancedOptions = () => { + setShowAdvancedOptions(!showAdvancedOptions); + }; + + useDeepEffect(() => { + if (name) { + setErrors({}); + } + }, [name]); + + useEffect(() => { + setErrors(backendValidators); + }, [backendValidators]); + + const onClickSaveQuery = (evt: React.MouseEvent) => { + evt.preventDefault(); + + const { valid, errors: newErrors } = validateQueryName(name); + setErrors({ + ...errors, + ...newErrors, + }); + + if (valid) { + saveQuery({ + // from modal fields + name, + description, + interval: selectedFrequency, + observer_can_run: observerCanRun, + platform: selectedPlatformOptions, + min_osquery_version: selectedMinOsqueryVersionOptions, + logging: selectedLoggingType, + // from previous New query page + query: queryValue, + // from doubly previous ManageQueriesPage + team_id: apiTeamIdForQuery, + }); + } + }; + + const onChangeSelectPlatformOptions = useCallback( + (values: string) => { + const valArray = values.split(","); + + // Remove All if another OS is chosen + // else if Remove OS if All is chosen + if (valArray.indexOf("") === 0 && valArray.length > 1) { + // TODO - inmprove type safety of all 3 options + setSelectedPlatformOptions( + pull(valArray, "").join(",") as SelectedPlatformString + ); + } else if (valArray.length > 1 && valArray.indexOf("") > -1) { + setSelectedPlatformOptions(""); + } else { + setSelectedPlatformOptions(values as SelectedPlatformString); + } + }, + [setSelectedPlatformOptions] + ); + + return ( + + <> +
+ setName(value)} + value={name} + error={errors.name} + inputClassName={`${baseClass}__name`} + label="Name" + placeholder="What is your query called?" + autofocus + /> + 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. +

+ + Observers can run + +

+ Users with the Observer role will be able to run this query as a + live query. +

+ + {showAdvancedOptions && ( + <> + +

+ If automations are turned on, your query collects data on + compatible platforms. +
+ If you want more control, override platforms. +

+ + + + )} +
+ + +
+ + +
+ ); +}; + +export default SaveQueryModal; diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss b/frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss new file mode 100644 index 0000000000..a4d1337350 --- /dev/null +++ b/frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss @@ -0,0 +1,32 @@ +.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/components/SaveQueryModal/index.ts b/frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts new file mode 100644 index 0000000000..fd4708d1b7 --- /dev/null +++ b/frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts @@ -0,0 +1 @@ +export { default } from "./SaveQueryModal"; diff --git a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx index 75321ef8c9..7b94956efd 100644 --- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx +++ b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx @@ -7,7 +7,10 @@ import queryAPI from "services/entities/queries"; import { AppContext } from "context/app"; import { QueryContext } from "context/query"; import { NotificationContext } from "context/notification"; -import { IQueryFormData, IQuery } from "interfaces/query"; +import { + ICreateQueryRequestBody, + ISchedulableQuery, +} from "interfaces/schedulable_query"; import PATHS from "router/paths"; import debounce from "utilities/debounce"; import deepDifference from "utilities/deep_difference"; @@ -19,16 +22,12 @@ interface IQueryEditorProps { router: InjectedRouter; baseClass: string; queryIdForEdit: number | null; - storedQuery: IQuery | undefined; + teamNameForQuery?: string; + apiTeamIdForQuery?: number; + storedQuery: ISchedulableQuery | undefined; storedQueryError: Error | null; showOpenSchemaActionText: boolean; isStoredQueryLoading: boolean; - createQuery: UseMutateAsyncFunction< - { query: IQuery }, - unknown, - IQueryFormData, - unknown - >; onOsqueryTableSelect: (tableName: string) => void; goToSelectTargets: () => void; onOpenSchemaSidebar: () => void; @@ -39,11 +38,12 @@ const QueryEditor = ({ router, baseClass, queryIdForEdit, + teamNameForQuery, + apiTeamIdForQuery, storedQuery, storedQueryError, showOpenSchemaActionText, isStoredQueryLoading, - createQuery, onOsqueryTableSelect, goToSelectTargets, onOpenSchemaSidebar, @@ -59,6 +59,10 @@ const QueryEditor = ({ lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, + lastEditedQueryFrequency, + lastEditedQueryLoggingType, + lastEditedQueryPlatforms, + lastEditedQueryMinOsqueryVersion, } = useContext(QueryContext); const [isQuerySaving, setIsQuerySaving] = useState(false); @@ -77,29 +81,35 @@ const QueryEditor = ({ [key: string]: string; }>({}); - const onSaveQueryFormSubmit = debounce(async (formData: IQueryFormData) => { + const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => { setIsQuerySaving(true); try { - const { query }: { query: IQuery } = await createQuery(formData); - router.push(PATHS.EDIT_QUERY(query)); + const { query } = await queryAPI.create(formData); + router.push(PATHS.EDIT_QUERY(query.id)); renderFlash("success", "Query created!"); setBackendValidators({}); } catch (createError: any) { - console.error(createError); if (createError.data.errors[0].reason.includes("already exists")) { - setBackendValidators({ name: "A query with this name 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: IQueryFormData) => { + const onUpdateQuery = async (formData: ICreateQueryRequestBody) => { if (!queryIdForEdit) { return false; } @@ -111,6 +121,10 @@ const QueryEditor = ({ lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, + lastEditedQueryFrequency, + lastEditedQueryPlatforms, + lastEditedQueryLoggingType, + lastEditedQueryMinOsqueryVersion, }); try { @@ -149,12 +163,14 @@ const QueryEditor = ({
void, - onEditScheduledQueryClick: (selectedQuery: IEditScheduledQuery) => void, - onShowQueryClick: (selectedQuery: IEditScheduledQuery) => void, - allScheduledQueriesList: IScheduledQuery[], - allScheduledQueriesError: Error | null, - toggleScheduleEditorModal: () => void, - isOnGlobalTeam: boolean, - selectedTeamData: ITeam | undefined, - isLoadingGlobalScheduledQueries: boolean, - isLoadingTeamScheduledQueries: boolean, - errorQueries: Error | null -): JSX.Element => { - return allScheduledQueriesError || errorQueries ? ( - - ) : ( - - ); -}; - -const renderAllTeamsTable = ( - router: InjectedRouter, - allTeamsScheduledQueriesList: IScheduledQuery[], - allTeamsScheduledQueriesError: Error | null, - isOnGlobalTeam: boolean, - selectedTeamData: ITeam | undefined, - isLoadingGlobalScheduledQueries: boolean, - isLoadingTeamScheduledQueries: boolean -): JSX.Element => { - return allTeamsScheduledQueriesError ? ( - - ) : ( -
- -
- ); -}; - -interface IFormData { - interval: number; - name?: string; - shard: number; - query?: string; - query_id?: number; - logging_type: string; - platform: string; - version: string; - team_id?: number; -} - -interface ITeamSchedulesPageProps { - params: { - team_id: string; - }; - router: InjectedRouter; // v3 - route: any; - location: any; -} - -const ManageSchedulePage = ({ - router, - location, -}: ITeamSchedulesPageProps): JSX.Element => { - const { renderFlash } = useContext(NotificationContext); - const { MANAGE_PACKS } = paths; - const handleAdvanced = () => router.push(MANAGE_PACKS); - - const { - isOnGlobalTeam, - isPremiumTier, - isFreeTier, - isSandboxMode, - } = useContext(AppContext); - - const { - currentTeamId, - isAnyTeamSelected, - isRouteOk, - teamIdForApi, - userTeams, - handleTeamChange, - } = useTeamIdParam({ - location, - router, - includeAllTeams: true, - includeNoTeam: false, - permittedAccessByTeamRole: { - admin: true, - maintainer: true, - observer: false, - observer_plus: false, - }, - }); - - const { data: teams, isLoading: isLoadingTeams } = useQuery< - ILoadTeamsResponse, - Error, - ITeam[] - >(["teams"], () => teamsAPI.loadAll(), { - enabled: isRouteOk && !!isPremiumTier, - refetchOnMount: false, - refetchOnWindowFocus: false, - select: (data) => data.teams, - }); - - const { - data: fleetQueries, - isLoading: isLoadingFleetQueries, - error: errorQueries, - } = useQuery( - ["fleetQueries"], - () => fleetQueriesAPI.loadAll(), - { - enabled: isRouteOk, - refetchOnMount: false, - refetchOnWindowFocus: false, - select: (data) => data.queries, - } - ); - - const { - data: globalScheduledQueries, - error: globalScheduledQueriesError, - isLoading: isLoadingGlobalScheduledQueries, - refetch: refetchGlobalScheduledQueries, - } = useQuery< - ILoadAllGlobalScheduledQueriesResponse, - Error, - IScheduledQuery[] - >(["globalScheduledQueries"], () => globalScheduledQueriesAPI.loadAll(), { - enabled: isRouteOk, - select: (data) => data.global_schedule, - }); - - const { - data: teamScheduledQueries, - error: teamScheduledQueriesError, - isLoading: isLoadingTeamScheduledQueries, - refetch: refetchTeamScheduledQueries, - } = useQuery( - ["teamScheduledQueries", teamIdForApi], - () => teamScheduledQueriesAPI.loadAll(teamIdForApi), - { - enabled: isRouteOk && isPremiumTier && !!teamIdForApi, - select: (data) => data.scheduled, - } - ); - - const refetchScheduledQueries = useCallback(() => { - refetchGlobalScheduledQueries(); - if (isAnyTeamSelected) { - refetchTeamScheduledQueries(); - } - }, [ - isAnyTeamSelected, - refetchGlobalScheduledQueries, - refetchTeamScheduledQueries, - ]); - - const allScheduledQueriesList = - (isAnyTeamSelected ? teamScheduledQueries : globalScheduledQueries) || []; - const allScheduledQueriesError = isAnyTeamSelected - ? teamScheduledQueriesError - : globalScheduledQueriesError; - - const inheritedScheduledQueriesList = globalScheduledQueries; - const inheritedScheduledQueriesError = globalScheduledQueriesError; - - const inheritedQueryOrQueries = - inheritedScheduledQueriesList?.length === 1 ? "query" : "queries"; - - const selectedTeamData = isAnyTeamSelected - ? teams?.find((team: ITeam) => teamIdForApi === team.id) - : undefined; - - const [isUpdatingScheduledQuery, setIsUpdatingScheduledQuery] = useState( - false - ); - const [showInheritedQueries, setShowInheritedQueries] = useState(false); - const [showScheduleEditorModal, setShowScheduleEditorModal] = useState(false); - const [showShowQueryModal, setShowShowQueryModal] = useState(false); - const [showPreviewDataModal, setShowPreviewDataModal] = useState(false); - const [ - showRemoveScheduledQueryModal, - setShowRemoveScheduledQueryModal, - ] = useState(false); - const [selectedQueryIds, setSelectedQueryIds] = useState( - [] - ); - const [ - selectedScheduledQuery, - setSelectedScheduledQuery, - ] = useState(); - - const toggleInheritedQueries = () => { - setShowInheritedQueries(!showInheritedQueries); - }; - - const togglePreviewDataModal = useCallback(() => { - setShowPreviewDataModal(!showPreviewDataModal); - }, [setShowPreviewDataModal, showPreviewDataModal]); - - const toggleScheduleEditorModal = useCallback(() => { - setSelectedScheduledQuery(undefined); // create modal renders - setShowScheduleEditorModal(!showScheduleEditorModal); - }, [showScheduleEditorModal, setShowScheduleEditorModal]); - - const toggleShowQueryModal = useCallback(() => { - setSelectedScheduledQuery(undefined); - setShowShowQueryModal(!showShowQueryModal); - }, [showShowQueryModal, setShowShowQueryModal]); - - const toggleRemoveScheduledQueryModal = useCallback(() => { - setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal); - }, [showRemoveScheduledQueryModal, setShowRemoveScheduledQueryModal]); - - const onRemoveScheduledQueryClick = ( - selectedTableQueryIds: number[] - ): void => { - toggleRemoveScheduledQueryModal(); - setSelectedQueryIds(selectedTableQueryIds); - }; - - const onShowQueryClick = (selectedQuery: IEditScheduledQuery): void => { - toggleShowQueryModal(); - setSelectedScheduledQuery(selectedQuery); - }; - - const onEditScheduledQueryClick = ( - selectedQuery: IEditScheduledQuery - ): void => { - toggleScheduleEditorModal(); - setSelectedScheduledQuery(selectedQuery); // edit modal renders - }; - - const onRemoveScheduledQuerySubmit = useCallback(() => { - setIsUpdatingScheduledQuery(true); - const promises = selectedQueryIds.map((id: number) => { - return isAnyTeamSelected - ? teamScheduledQueriesAPI.destroy(teamIdForApi, id) - : globalScheduledQueriesAPI.destroy({ id }); - }); - const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries"; - return Promise.all(promises) - .then(() => { - renderFlash( - "success", - `Successfully removed scheduled ${queryOrQueries}.` - ); - toggleRemoveScheduledQueryModal(); - refetchScheduledQueries(); - }) - .catch(() => { - renderFlash( - "error", - `Unable to remove scheduled ${queryOrQueries}. Please try again.` - ); - toggleRemoveScheduledQueryModal(); - }) - .finally(() => { - refetchGlobalScheduledQueries(); - setIsUpdatingScheduledQuery(false); - }); - }, [ - selectedQueryIds, - isAnyTeamSelected, - teamIdForApi, - renderFlash, - toggleRemoveScheduledQueryModal, - refetchScheduledQueries, - refetchGlobalScheduledQueries, - ]); - - const onAddScheduledQuerySubmit = useCallback( - (formData: IFormData, editQuery: IEditScheduledQuery | undefined) => { - setIsUpdatingScheduledQuery(true); - if (editQuery) { - const updatedAttributes = deepDifference(formData, editQuery); - - const editResponse = - editQuery.type === "team_scheduled_query" - ? teamScheduledQueriesAPI.update(editQuery, updatedAttributes) - : globalScheduledQueriesAPI.update(editQuery, updatedAttributes); - - editResponse - .then(() => { - renderFlash( - "success", - `Successfully updated ${formData.name} in the schedule.` - ); - refetchScheduledQueries(); - toggleScheduleEditorModal(); - }) - .catch(() => { - renderFlash( - "error", - "Could not update scheduled query. Please try again." - ); - }) - .finally(() => { - setIsUpdatingScheduledQuery(false); - refetchGlobalScheduledQueries(); - }); - } else { - const createResponse = isAnyTeamSelected - ? teamScheduledQueriesAPI.create({ ...formData }) - : globalScheduledQueriesAPI.create({ ...formData }); - - createResponse - .then(() => { - renderFlash( - "success", - `Successfully added ${formData.name} to the schedule.` - ); - refetchScheduledQueries(); - toggleScheduleEditorModal(); - }) - .catch(() => { - renderFlash("error", "Could not schedule query. Please try again."); - }) - .finally(() => { - setIsUpdatingScheduledQuery(false); - refetchGlobalScheduledQueries(); - }); - } - }, - [ - isAnyTeamSelected, - refetchGlobalScheduledQueries, - refetchScheduledQueries, - renderFlash, - toggleScheduleEditorModal, - ] - ); - - if (!isRouteOk || (isPremiumTier && !userTeams?.length)) { - return ( -
- -
- ); - } - - return ( - -
-
-
-
-
- {isFreeTier &&

Schedule

} - {isPremiumTier && - userTeams && - (userTeams.length > 1 || isOnGlobalTeam) && ( - - )} - {isPremiumTier && - !isOnGlobalTeam && - userTeams && - userTeams.length === 1 &&

{userTeams[0].name}

} -
-
-
- {allScheduledQueriesList?.length !== 0 && !allScheduledQueriesError && ( -
- {/* NOTE: Product decision to remove packs from UI - {isOnGlobalTeam && ( - - )} */} - -
- )} -
-
- {!isLoadingTeams && ( -
- {isAnyTeamSelected ? ( -

- Schedule queries for{" "} - all hosts assigned to this team -

- ) : ( -

- Schedule queries to run at regular intervals across{" "} - all of your hosts -

- )} -
- )} -
-
- {isLoadingTeams || - isLoadingFleetQueries || - isLoadingGlobalScheduledQueries || - isLoadingTeamScheduledQueries ? ( - - ) : ( - renderTable( - router, - onRemoveScheduledQueryClick, - onEditScheduledQueryClick, - onShowQueryClick, - allScheduledQueriesList, - allScheduledQueriesError, - toggleScheduleEditorModal, - isOnGlobalTeam || false, - selectedTeamData, - isLoadingGlobalScheduledQueries, - isLoadingTeamScheduledQueries, - errorQueries - ) - )} -
- {/* must use ternary for NaN */} - {isAnyTeamSelected && - inheritedScheduledQueriesList && - inheritedScheduledQueriesList.length > 0 ? ( - schedule run on this team’s hosts.' - } - onClick={toggleInheritedQueries} - /> - ) : null} - {showInheritedQueries && - inheritedScheduledQueriesList && - renderAllTeamsTable( - router, - inheritedScheduledQueriesList, - inheritedScheduledQueriesError, - isOnGlobalTeam || false, - selectedTeamData, - isLoadingGlobalScheduledQueries, - isLoadingTeamScheduledQueries - )} - {showScheduleEditorModal && fleetQueries && ( - - )} - {showRemoveScheduledQueryModal && ( - - )} - {showShowQueryModal && ( - - )} -
-
- ); -}; - -export default ManageSchedulePage; diff --git a/frontend/pages/schedule/ManageSchedulePage/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/_styles.scss deleted file mode 100644 index f93847b34b..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/_styles.scss +++ /dev/null @@ -1,122 +0,0 @@ -.manage-schedule-page { - &__header-wrap { - display: flex; - align-items: center; - justify-content: space-between; - height: 38px; - } - - &__header { - display: flex; - align-items: center; - - .form-field { - margin-bottom: 0; - } - } - - &__text { - margin-right: $pad-large; - } - - &__title { - font-size: $large; - - .fleeticon { - color: $core-fleet-blue; - margin-right: 15px; - } - - .fleeticon-success-check { - color: $ui-success; - } - - .fleeticon-offline { - color: $ui-error; - } - } - - &__description { - margin: 0; - margin-bottom: $pad-xxlarge; - - h2 { - text-transform: uppercase; - color: $core-fleet-black; - font-weight: $regular; - font-size: $small; - } - - p { - color: $ui-fleet-black-75; - margin: 0; - font-size: $x-small; - font-style: italic; - } - } - - &__action-button-container { - display: flex; - align-items: flex-start; - } - - &__advanced-button { - margin-right: $pad-medium; - } - - .Select.is-open { - .Select-value-label { - color: $core-vibrant-blue !important; - } - } - - .schedule-table { - .data-table-block { - .data-table__table { - thead { - .query_name__header { - width: $col-lg; - } - .interval__header { - width: auto; - } - .actions__header { - width: auto; - } - @media (min-width: $break-lg) { - .interval__header { - width: 0; - } - } - } - tbody { - .query_name__cell { - width: $col-lg; - max-width: 175px; // Truncates at smaller widths - } - .interval__cell { - width: auto; - } - .actions__cell { - width: auto; - } - @media (min-width: $break-lg) { - .interval_cell { - width: 0; - } - } - } - } - } - - .empty-table__container { - max-width: 465px; // Fixes wider font causing orphaned word on all teams empty state - } - } - - .no-team-schedule { - border: 1px solid #e2e4ea; - box-sizing: border-box; - border-radius: 8px; - } -} diff --git a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss deleted file mode 100644 index f965ee2b20..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss +++ /dev/null @@ -1,14 +0,0 @@ -.preview-data-modal { - &__sandbox-info { - margin-top: $pad-medium; - - p { - margin: 0; - margin-bottom: $pad-medium; - } - - p:last-child { - margin-bottom: 0; - } - } -} diff --git a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx deleted file mode 100644 index fa08aeb3a3..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; - -import Modal from "components/Modal"; -import Button from "components/buttons/Button"; - -const baseClass = "remove-scheduled-query-modal"; - -interface IRemoveScheduledQueryModalProps { - isUpdatingScheduledQuery: boolean; - onCancel: () => void; - onSubmit: () => void; -} - -const RemoveScheduledQueryModal = ({ - isUpdatingScheduledQuery, - onCancel, - onSubmit, -}: IRemoveScheduledQueryModalProps): JSX.Element => { - return ( - -
- Are you sure you want to remove the selected queries from the schedule? -
- - -
-
-
- ); -}; - -export default RemoveScheduledQueryModal; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts deleted file mode 100644 index 90280fc7bd..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./RemoveScheduledQueryModal"; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx deleted file mode 100644 index f634f803c5..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx +++ /dev/null @@ -1,364 +0,0 @@ -/* This component is used for creating and editing both global and team scheduled queries */ - -import React, { useState, useCallback, useContext } from "react"; -import { pull } from "lodash"; -import { AppContext } from "context/app"; - -import { IQuery } from "interfaces/query"; -import { IEditScheduledQuery } from "interfaces/scheduled_query"; - -import Modal from "components/Modal"; -import Button from "components/buttons/Button"; -import RevealButton from "components/buttons/RevealButton"; -import InfoBanner from "components/InfoBanner/InfoBanner"; -// @ts-ignore -import Dropdown from "components/forms/fields/Dropdown"; -// @ts-ignore -import InputField from "components/forms/fields/InputField"; -import CustomLink from "components/CustomLink"; -import { - FREQUENCY_DROPDOWN_OPTIONS, - SCHEDULE_PLATFORM_DROPDOWN_OPTIONS, - LOGGING_TYPE_OPTIONS, - MIN_OSQUERY_VERSION_OPTIONS, -} from "utilities/constants"; - -import PreviewDataModal from "../PreviewDataModal"; - -const baseClass = "schedule-editor-modal"; - -interface IFormData { - interval: number; - name?: string; - shard: number; - query?: string; - query_id?: number; - logging_type: string; - platform: string; - version: string; - team_id?: number; -} - -interface IScheduleEditorModalProps { - allQueries: IQuery[]; - onClose: () => void; - onScheduleSubmit: ( - formData: IFormData, - editQuery: IEditScheduledQuery | undefined - ) => void; - editQuery?: IEditScheduledQuery; - teamId?: number; - togglePreviewDataModal: () => void; - showPreviewDataModal: boolean; - isUpdatingScheduledQuery: boolean; -} -interface INoQueryOption { - id: number; - name: string; -} - -const generateLoggingType = (query: IEditScheduledQuery) => { - if (query.snapshot) { - return "snapshot"; - } - if (query.removed) { - return "differential"; - } - return "differential_ignore_removals"; -}; - -const generateLoggingDestination = (loggingConfig: string): string => { - switch (loggingConfig) { - case "filesystem": - return "the filesystem"; - case "firehose": - return "AWS Kinesis Firehose"; - case "kinesis": - return "AWS Kinesis"; - case "lambda": - return "AWS Lambda"; - case "pubsub": - return "GCP PubSub"; - case "stdout": - return "the standard output stream"; - default: - return loggingConfig; - } -}; - -const ScheduleEditorModal = ({ - onClose, - onScheduleSubmit, - allQueries, - editQuery, - teamId, - togglePreviewDataModal, - showPreviewDataModal, - isUpdatingScheduledQuery, -}: IScheduleEditorModalProps): JSX.Element => { - const { config } = useContext(AppContext); - - const loggingConfig = config?.logging.result.plugin || "unknown"; - - const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); - const [selectedQuery, setSelectedQuery] = useState< - IEditScheduledQuery | INoQueryOption - >(); - const [selectedFrequency, setSelectedFrequency] = useState( - editQuery ? editQuery.interval : 86400 - ); - const [selectedPlatformOptions, setSelectedPlatformOptions] = useState( - editQuery?.platform || "" - ); - const [selectedLoggingType, setSelectedLoggingType] = useState( - editQuery ? generateLoggingType(editQuery) : "snapshot" - ); - const [ - selectedMinOsqueryVersionOptions, - setSelectedMinOsqueryVersionOptions, - ] = useState(editQuery?.version || ""); - const [selectedShard, setSelectedShard] = useState( - editQuery?.shard ? editQuery?.shard.toString() : "" - ); - - const createQueryDropdownOptions = () => { - const queryOptions = allQueries.map((q) => { - return { - value: String(q.id), - label: q.name, - }; - }); - return queryOptions; - }; - - const toggleAdvancedOptions = () => { - setShowAdvancedOptions(!showAdvancedOptions); - }; - - const onChangeSelectQuery = useCallback( - (queryId: string) => { - const queryWithId: IQuery | undefined = allQueries.find( - (query: IQuery) => query.id === parseInt(queryId, 10) - ); - setSelectedQuery(queryWithId); - }, - [allQueries, setSelectedQuery] - ); - - const onChangeSelectFrequency = useCallback( - (value: number) => { - setSelectedFrequency(value); - }, - [setSelectedFrequency] - ); - - const onChangeSelectPlatformOptions = useCallback( - (values: string) => { - const valArray = values.split(","); - - // Remove All if another OS is chosen - // else if Remove OS if All is chosen - if (valArray.indexOf("") === 0 && valArray.length > 1) { - setSelectedPlatformOptions(pull(valArray, "").join(",")); - } else if (valArray.length > 1 && valArray.indexOf("") > -1) { - setSelectedPlatformOptions(""); - } else { - setSelectedPlatformOptions(values); - } - }, - [setSelectedPlatformOptions] - ); - - const onChangeSelectLoggingType = useCallback( - (value: string) => { - setSelectedLoggingType(value); - }, - [setSelectedLoggingType] - ); - - const onChangeMinOsqueryVersionOptions = useCallback( - (value: string) => { - setSelectedMinOsqueryVersionOptions(value); - }, - [setSelectedMinOsqueryVersionOptions] - ); - - const onChangeShard = useCallback( - (value: string) => { - setSelectedShard(value); - }, - [setSelectedShard] - ); - - const onFormSubmit = (): void => { - const query_id = () => { - if (editQuery) { - return editQuery.query_id; - } - return selectedQuery?.id; - }; - - const name = () => { - if (editQuery) { - return editQuery.name; - } - return selectedQuery?.name; - }; - - onScheduleSubmit( - { - shard: parseInt(selectedShard, 10), - interval: selectedFrequency, - query_id: query_id(), - name: name(), - logging_type: selectedLoggingType, - platform: selectedPlatformOptions, - version: selectedMinOsqueryVersionOptions, - team_id: teamId, - }, - editQuery - ); - }; - - if (showPreviewDataModal) { - return ; - } - - return ( - -
-

- Scheduled queries can currently be run on macOS, Windows, and Linux - hosts. Interested in collecting data from your Chromebooks?{" "} - -

- {!editQuery && ( - - )} - - -

- Your configured log destination is {loggingConfig}. -

-

- {loggingConfig === "unknown" - ? "" - : `This means that when this query is run on your hosts, the data will - be sent to ${generateLoggingDestination(loggingConfig)}.`} -

-

- Check out the Fleet documentation on  - - . -

-
-
- - {showAdvancedOptions && ( -
- - - - -
- )} -
-
-
- -
-
- - -
-
- -
- ); -}; - -export default ScheduleEditorModal; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss deleted file mode 100644 index 5682c40509..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss +++ /dev/null @@ -1,26 +0,0 @@ -.schedule-editor-modal { - &__platform-compatibility { - margin-bottom: $pad-large; - } - - &__sandbox-info { - margin-top: $pad-medium; - - p { - margin: 0; - margin-bottom: $pad-medium; - } - - p:last-child { - margin-bottom: 0; - } - } - - &__info-header { - font-weight: $bold; - } - - .Select-value-label { - font-size: $small; - } -} diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts deleted file mode 100644 index 2840b8d26c..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ScheduleEditorModal"; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx deleted file mode 100644 index e5acf57f5d..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Component when there is an error retrieving schedule set up in fleet - */ -import React from "react"; -import { InjectedRouter } from "react-router"; -import paths from "router/paths"; - -import { - IScheduledQuery, - IEditScheduledQuery, -} from "interfaces/scheduled_query"; -import { ITeam } from "interfaces/team"; -import { IEmptyTableProps } from "interfaces/empty_table"; - -import Button from "components/buttons/Button"; -import CustomLink from "components/CustomLink"; -import TableContainer from "components/TableContainer"; -import EmptyTable from "components/EmptyTable"; -import { - generateInheritedQueriesTableHeaders, - generateTableHeaders, - generateDataSet, -} from "./ScheduleTableConfig"; - -const baseClass = "schedule-table"; - -const TAGGED_TEMPLATES = { - hostsByTeamRoute: (teamId: number | undefined | null) => { - return `${teamId ? `/?team_id=${teamId}` : ""}`; - }, -}; -interface IScheduleTableProps { - router: InjectedRouter; // v3 - onRemoveScheduledQueryClick?: (selectedIds: number[]) => void; - onEditScheduledQueryClick?: (selectedQuery: IEditScheduledQuery) => void; - onShowQueryClick?: (selectedQuery: IEditScheduledQuery) => void; - allScheduledQueriesList: IScheduledQuery[]; - toggleScheduleEditorModal?: () => void; - inheritedQueries?: boolean; - isOnGlobalTeam: boolean; - selectedTeamData: ITeam | undefined; - loadingInheritedQueriesTableData: boolean; - loadingTeamQueriesTableData: boolean; -} - -const ScheduleTable = ({ - router, - onRemoveScheduledQueryClick, - onEditScheduledQueryClick, - onShowQueryClick, - allScheduledQueriesList, - toggleScheduleEditorModal, - inheritedQueries, - isOnGlobalTeam, - selectedTeamData, - loadingInheritedQueriesTableData, - loadingTeamQueriesTableData, -}: IScheduleTableProps): JSX.Element => { - const { MANAGE_PACKS, MANAGE_HOSTS } = paths; - - const handleAdvanced = () => router.push(MANAGE_PACKS); - - const emptyState = () => { - const emptySchedule: IEmptyTableProps = { - iconName: "empty-schedule", - header: ( - <> - Schedule queries to run at regular intervals on{" "} - all your hosts - - ), - additionalInfo: ( - <> - Want to learn more?  - - - ), - primaryButton: ( - - ), - }; - - if (selectedTeamData) { - emptySchedule.header = ( - <> - Schedule queries for all hosts assigned to{" "} - - {selectedTeamData.name} - - - ); - } - - /* NOTE: Product decision to remove packs from UI - if (isOnGlobalTeam) { - emptySchedule.info = ( - <>Or go to your osquery packs via the ‘Advanced’ button. - ); - emptySchedule.secondaryButton = ( - - ); - } - */ - return emptySchedule; - }; - - const onActionSelection = ( - action: string, - scheduledQuery: IEditScheduledQuery - ): void => { - switch (action) { - case "edit": - if (onEditScheduledQueryClick) { - onEditScheduledQueryClick(scheduledQuery); - } - break; - case "showQuery": - if (onShowQueryClick) { - onShowQueryClick(scheduledQuery); - } - break; - default: - if (onRemoveScheduledQueryClick) { - onRemoveScheduledQueryClick([scheduledQuery.id]); - } - break; - } - }; - - const tableHeaders = generateTableHeaders(onActionSelection); - const loadingTableData = selectedTeamData?.id - ? loadingTeamQueriesTableData - : loadingInheritedQueriesTableData; - - if (inheritedQueries) { - const inheritedQueriesTableHeaders = generateInheritedQueriesTableHeaders(); - - return ( -
- - EmptyTable({ - iconName: emptyState().iconName, - header: emptyState().header, - info: emptyState().info, - additionalInfo: emptyState().additionalInfo, - primaryButton: emptyState().primaryButton, - secondaryButton: emptyState().secondaryButton, - }) - } - /> -
- ); - } - - return ( -
- - EmptyTable({ - iconName: emptyState().iconName, - header: emptyState().header, - info: emptyState().info, - additionalInfo: emptyState().additionalInfo, - primaryButton: emptyState().primaryButton, - secondaryButton: emptyState().secondaryButton, - }) - } - isClientSidePagination - /> -
- ); -}; - -export default ScheduleTable; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx deleted file mode 100644 index c4001ab957..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx +++ /dev/null @@ -1,272 +0,0 @@ -/* 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 { performanceIndicator, secondsToDhms } from "utilities/helpers"; - -// @ts-ignore -import Checkbox from "components/forms/fields/Checkbox"; -import TextCell from "components/TableContainer/DataTable/TextCell"; -import DropdownCell from "components/TableContainer/DataTable/DropdownCell"; -import PillCell from "components/TableContainer/DataTable/PillCell"; -import { IDropdownOption } from "interfaces/dropdownOption"; -import { - IScheduledQuery, - IEditScheduledQuery, -} from "interfaces/scheduled_query"; -import TooltipWrapper from "components/TooltipWrapper"; - -interface IGetToggleAllRowsSelectedProps { - checked: boolean; - indeterminate: boolean; - title: string; - onChange: () => void; - style: { cursor: string }; -} -interface IHeaderProps { - column: { - title: string; - isSortedDesc: boolean; - }; - getToggleAllRowsSelectedProps: () => IGetToggleAllRowsSelectedProps; - toggleAllRowsSelected: () => void; -} - -interface IRowProps { - row: { - original: IEditScheduledQuery; - getToggleRowSelectedProps: () => IGetToggleAllRowsSelectedProps; - toggleRowSelected: () => void; - }; -} - -interface ICellProps extends IRowProps { - cell: { - value: string | number | boolean; - }; -} - -interface INumberCellProps extends IRowProps { - cell: { - value: number; - }; -} - -interface IPillCellProps extends IRowProps { - cell: { - value: { indicator: string; id: number }; - }; -} - -interface IDropdownCellProps extends IRowProps { - cell: { - value: IDropdownOption[]; - }; -} - -interface IDataColumn { - Header: ((props: IHeaderProps) => JSX.Element) | string; - Cell: - | ((props: ICellProps) => JSX.Element) - | ((props: INumberCellProps) => JSX.Element) - | ((props: IPillCellProps) => JSX.Element) - | ((props: IDropdownCellProps) => JSX.Element); - id?: string; - title?: string; - accessor?: string; - disableHidden?: boolean; - disableSortBy?: boolean; -} -interface IAllScheduledQueryTableData { - name: string; - interval: number; - actions: IDropdownOption[]; - id: number; - type: string; -} - -// NOTE: cellProps come from react-table -// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties -const generateTableHeaders = ( - actionSelectHandler: ( - value: string, - scheduledQuery: IEditScheduledQuery - ) => void -): IDataColumn[] => { - return [ - { - id: "selection", - Header: (cellProps: IHeaderProps): JSX.Element => { - const props = cellProps.getToggleAllRowsSelectedProps(); - const checkboxProps = { - value: props.checked, - indeterminate: props.indeterminate, - onChange: () => cellProps.toggleAllRowsSelected(), - }; - return ; - }, - Cell: (cellProps: ICellProps): JSX.Element => { - const props = cellProps.row.getToggleRowSelectedProps(); - const checkboxProps = { - value: props.checked, - onChange: () => cellProps.row.toggleRowSelected(), - }; - return ; - }, - disableHidden: true, - }, - { - title: "Name", - Header: "Name", - disableSortBy: true, - accessor: "query_name", - Cell: (cellProps: ICellProps): JSX.Element => ( - - ), - }, - { - title: "Frequency", - Header: "Frequency", - disableSortBy: true, - accessor: "interval", - Cell: (cellProps: INumberCellProps): JSX.Element => ( - - ), - }, - { - Header: () => { - return ( -
- - performance impact
- across all hosts where this
- query was scheduled.`} - > - Performance impact -
-
- ); - }, - disableSortBy: true, - accessor: "performance", - Cell: (cellProps: IPillCellProps) => ( - - ), - }, - { - title: "Actions", - Header: "", - disableSortBy: true, - accessor: "actions", - Cell: (cellProps: IDropdownCellProps) => ( - - actionSelectHandler(value, cellProps.row.original) - } - placeholder={"Actions"} - /> - ), - }, - ]; -}; - -const generateInheritedQueriesTableHeaders = (): IDataColumn[] => { - return [ - { - title: "Query", - Header: "Query", - disableSortBy: true, - accessor: "query_name", - Cell: (cellProps: ICellProps): JSX.Element => ( - - ), - }, - { - title: "Frequency", - Header: "Frequency", - disableSortBy: true, - accessor: "interval", - Cell: (cellProps: INumberCellProps): JSX.Element => ( - - ), - }, - { - title: "Performance impact", - Header: "Performance impact", - disableSortBy: true, - accessor: "performance", - Cell: (cellProps: IPillCellProps) => ( - - ), - }, - ]; -}; - -const generateActionDropdownOptions = (): IDropdownOption[] => { - const dropdownOptions = [ - { - label: "Edit", - disabled: false, - value: "edit", - }, - { - label: "Show query", - disabled: false, - value: "showQuery", - }, - { - label: "Remove", - disabled: false, - value: "remove", - }, - ]; - return dropdownOptions; -}; - -const enhanceAllScheduledQueryData = ( - allScheduledQueries: IScheduledQuery[], - teamId: number | undefined -): IAllScheduledQueryTableData[] => { - return allScheduledQueries.map((scheduledQuery: IScheduledQuery) => { - const scheduledQueryPerformance = { - user_time_p50: scheduledQuery.stats?.user_time_p50, - system_time_p50: scheduledQuery.stats?.system_time_p50, - total_executions: scheduledQuery.stats?.total_executions, - }; - return { - name: scheduledQuery.name, - query_name: scheduledQuery.query_name, - interval: scheduledQuery.interval, - actions: generateActionDropdownOptions(), - id: scheduledQuery.id, - query: scheduledQuery.query, - query_id: scheduledQuery.query_id, - snapshot: scheduledQuery.snapshot, - removed: scheduledQuery.removed, - platform: scheduledQuery.platform, - version: scheduledQuery.version, - shard: scheduledQuery.shard, - type: teamId ? "team_scheduled_query" : "global_scheduled_query", - performance: { - indicator: performanceIndicator(scheduledQueryPerformance), - id: scheduledQuery.id, - }, - }; - }); -}; - -const generateDataSet = ( - allScheduledQueries: IScheduledQuery[], - teamId: number | undefined -): IAllScheduledQueryTableData[] => { - return [...enhanceAllScheduledQueryData(allScheduledQueries, teamId)]; -}; - -export { - generateInheritedQueriesTableHeaders, - generateTableHeaders, - generateDataSet, -}; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts deleted file mode 100644 index fb4310e446..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ScheduleTable"; diff --git a/frontend/pages/schedule/ManageSchedulePage/index.ts b/frontend/pages/schedule/ManageSchedulePage/index.ts deleted file mode 100644 index ab51b9b30f..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ManageSchedulePage"; diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index d0e716d5a8..3d704fd00e 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -33,7 +33,6 @@ import ManageSoftwarePage from "pages/software/ManageSoftwarePage"; import ManageQueriesPage from "pages/queries/ManageQueriesPage"; import ManagePacksPage from "pages/packs/ManagePacksPage"; import ManagePoliciesPage from "pages/policies/ManagePoliciesPage"; -import ManageSchedulePage from "pages/schedule/ManageSchedulePage"; import PackComposerPage from "pages/packs/PackComposerPage"; import PolicyPage from "pages/policies/PolicyPage"; import QueryPage from "pages/queries/QueryPage"; @@ -171,8 +170,8 @@ const routes = ( - + @@ -206,14 +205,6 @@ const routes = ( - - - - - - - - diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index 8afb68c524..164f0328db 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -1,4 +1,3 @@ -import { IQuery } from "../interfaces/query"; import { IPolicy } from "../interfaces/policy"; import URL_PREFIX from "./url_prefix"; @@ -45,8 +44,10 @@ export default { EDIT_LABEL: (labelId: number): string => { return `${URL_PREFIX}/labels/${labelId}`; }, - EDIT_QUERY: (query: IQuery): string => { - return `${URL_PREFIX}/queries/${query.id}`; + EDIT_QUERY: (queryId: number, teamId?: number): string => { + return `${URL_PREFIX}/queries/${queryId}${ + teamId ? `?team_id=${teamId}` : "" + }`; }, EDIT_POLICY: (policy: IPolicy): string => { return `${URL_PREFIX}/policies/${policy.id}${ @@ -110,7 +111,8 @@ export default { MANAGE_POLICIES: `${URL_PREFIX}/policies/manage`, NEW_LABEL: `${URL_PREFIX}/labels/new`, NEW_POLICY: `${URL_PREFIX}/policies/new`, - NEW_QUERY: `${URL_PREFIX}/queries/new`, + NEW_QUERY: (teamId?: number) => + `${URL_PREFIX}/queries/new${teamId ? `?team_id=${teamId}` : ""}`, RESET_PASSWORD: `${URL_PREFIX}/login/reset`, SETUP: `${URL_PREFIX}/setup`, USER_SETTINGS: `${URL_PREFIX}/profile`, diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index 6361eee1a2..b8abf7061b 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -8,7 +8,7 @@ import { reconcileMutuallyExclusiveHostParams, reconcileMutuallyInclusiveHostParams, } from "utilities/url"; -import { ISelectedPlatform } from "interfaces/platform"; +import { SelectedPlatform } from "interfaces/platform"; import { ISoftware } from "interfaces/software"; import { FileVaultProfileStatus, @@ -127,7 +127,7 @@ const getSortParams = (sortOptions?: ISortOption[]) => { }; }; -const createMdmParams = (platform?: ISelectedPlatform, teamId?: number) => { +const createMdmParams = (platform?: SelectedPlatform, teamId?: number) => { if (platform === "all") { return buildQueryStringFromParams({ team_id: teamId }); } @@ -328,7 +328,7 @@ export default { return sendRequest("GET", HOST_MDM(id)); }, - getMdmSummary: (platform?: ISelectedPlatform, teamId?: number) => { + getMdmSummary: (platform?: SelectedPlatform, teamId?: number) => { const { MDM_SUMMARY } = endpoints; if (!platform || platform === "linux") { diff --git a/frontend/services/entities/operating_systems.ts b/frontend/services/entities/operating_systems.ts index 9650814fb4..c8a0b44d70 100644 --- a/frontend/services/entities/operating_systems.ts +++ b/frontend/services/entities/operating_systems.ts @@ -2,7 +2,7 @@ import sendRequest from "services"; import endpoints from "utilities/endpoints"; import { IOperatingSystemVersion } from "interfaces/operating_system"; -import { IOsqueryPlatform } from "interfaces/platform"; +import { OsqueryPlatform } from "interfaces/platform"; import { buildQueryStringFromParams } from "utilities/url"; // TODO: add platforms to this constant as new ones are supported @@ -14,7 +14,7 @@ export const OS_VERSIONS_API_SUPPORTED_PLATFORMS = [ export interface IGetOSVersionsRequest { id?: number; - platform?: IOsqueryPlatform; + platform?: OsqueryPlatform; teamId?: number; } diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 1d47ab2360..89765f6481 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -1,20 +1,22 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import sendRequest, { getError } from "services"; import endpoints from "utilities/endpoints"; -import { IQueryFormData } from "interfaces/query"; import { ISelectedTargets } from "interfaces/target"; import { AxiosResponse } from "axios"; +import { + ICreateQueryRequestBody, + IModifyQueryRequestBody, +} from "interfaces/schedulable_query"; +import { buildQueryStringFromParams } from "utilities/url"; + +// Mock API requests to be used in developing FE for #7765 in parallel with BE development +// import { sendRequest } from "services/mock_service/service/service"; export default { - create: ({ description, name, query, observer_can_run }: IQueryFormData) => { + create: (createQueryRequestBody: ICreateQueryRequestBody) => { const { QUERIES } = endpoints; - return sendRequest("POST", QUERIES, { - description, - name, - query, - observer_can_run, - }); + return sendRequest("POST", QUERIES, createQueryRequestBody); }, destroy: (id: string | number) => { const { QUERIES } = endpoints; @@ -22,16 +24,26 @@ export default { return sendRequest("DELETE", path); }, + bulkDestroy: (ids: number[]) => { + const { QUERIES } = endpoints; + const path = `${QUERIES}/delete`; + return sendRequest("POST", path, { ids }); + }, load: (id: number) => { const { QUERIES } = endpoints; const path = `${QUERIES}/${id}`; return sendRequest("GET", path); }, - loadAll: () => { + loadAll: (teamId?: number) => { const { QUERIES } = endpoints; + const queryString = buildQueryStringFromParams({ team_id: teamId }); + const path = `${QUERIES}`; - return sendRequest("GET", QUERIES); + return sendRequest( + "GET", + queryString ? path.concat(`?${queryString}`) : path + ); }, run: async ({ query, @@ -62,7 +74,7 @@ export default { throw new Error(getError(response as AxiosResponse)); } }, - update: (id: number, updateParams: IQueryFormData) => { + update: (id: number, updateParams: IModifyQueryRequestBody) => { const { QUERIES } = endpoints; const path = `${QUERIES}/${id}`; diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index ab7c5583ec..50ebcbf1f9 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -22,6 +22,17 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { // request query string is hostname, uuid, or mac address; response is host detail excluding any // expensive data operations "targets?query={*}": RESPONSES.hosts, + // "SchedulableQueries" to be used in developing frontend for #7765 + queries: RESPONSES.globalQueries, + "queries/1": RESPONSES.globalQuery1, + "queries/2": RESPONSES.globalQuery2, + "queries/3": RESPONSES.globalQuery3, + "queries/4": RESPONSES.teamQuery1, + "queries/5": RESPONSES.globalQuery4, + "queries/6": RESPONSES.globalQuery5, + "queries/7": RESPONSES.globalQuery6, + "queries/8": RESPONSES.teamQuery2, + "queries?team_id=13": RESPONSES.teamQueries, }, POST: { // request body is ISelectedTargets @@ -31,6 +42,16 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { targets_offline: 1, targets_missing_in_action: 0, }, + // "SchedulableQueries" to be used in developing frontend for #7765 + queries: { + description: "Ok", + name: "New query name", + observer_can_run: false, + query: "SELECT * FROM osquery_info;", + id: 1, + team_id: null, + platform: "linux", + }, }, } as IResponses; diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index 39ccc7f329..d20d275f8f 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -364,8 +364,261 @@ const labels = { ], }; +// "SchedulableQueries" to be used in developing frontend for #7765 +const globalQueries = { + queries: [ + { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 1, + name: + "Test Query (every hour, 3 platforms, snapshot, no observer run, no min osversion)", + description: "A test query", + query: "SELECT * FROM users;", + team_id: null, + interval: 3600, // Every hour + platform: "darwin,windows,linux", + min_osquery_version: "", + automations_enabled: true, + logging: "snapshot", + saved: true, + author_id: 1, + author_name: "Test User", + author_email: "test@example.com", + observer_can_run: false, + packs: [], + stats: { + system_time_p50: null, + system_time_p95: null, + user_time_p50: null, + user_time_p95: null, + total_executions: null, + }, + }, + { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 2, + name: + "Test Query 2 (every 12 hours, no platforms, observers can run, min version 5.8.1, differential)", + description: "A second test query", + query: "SELECT * FROM osquery_info", + team_id: null, + interval: 43200, // Every 12 hours + platform: "", + min_osquery_version: "5.8.1", + automations_enabled: false, + logging: "differential", + saved: false, + author_id: 2, + author_name: "Test User 2", + author_email: "test2@example.com", + observer_can_run: true, + packs: [], + stats: { + system_time_p50: null, + system_time_p95: null, + user_time_p50: null, + user_time_p95: null, + total_executions: null, + }, + }, + { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 3, + name: "Test Query 3", + description: "A third test query (Select all from windows_crashes", + query: "SELECT * FROM windows_crashes", + team_id: null, + interval: 604800, // Weekly + platform: "", + min_osquery_version: "", + automations_enabled: true, + logging: "differential", + saved: false, + author_id: 2, + author_name: "Test User 2", + author_email: "test2@example.com", + observer_can_run: true, + packs: [], + stats: { + system_time_p50: null, + system_time_p95: null, + user_time_p50: null, + user_time_p95: null, + total_executions: null, + }, + }, + { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 5, + name: "Test Query 4 (Never runs)", + description: "A third test query", + query: "SELECT * FROM osquery_info", + team_id: 2, + interval: 0, // Never + platform: "", + min_osquery_version: "", + automations_enabled: true, + logging: "differential", + saved: false, + author_id: 2, + author_name: "Test User 2", + author_email: "test2@example.com", + observer_can_run: true, + packs: [], + stats: { + system_time_p50: null, + system_time_p95: null, + user_time_p50: null, + user_time_p95: null, + total_executions: null, + }, + }, + { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 6, + name: "Test Query 5 runs every 5 minutes!", + description: "A fifth test query", + query: "SELECT * FROM osquery_info", + team_id: 2, + interval: 604800, // Every week + platform: "windows", + min_osquery_version: "", + automations_enabled: false, + logging: "differential", + saved: false, + author_id: 2, + author_name: "Test User 2", + author_email: "test2@example.com", + observer_can_run: true, + packs: [], + stats: { + system_time_p50: null, + system_time_p95: null, + user_time_p50: null, + user_time_p95: null, + total_executions: null, + }, + }, + { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 7, + name: "Test Query 6 runs every 6 hours", + description: "A 6th test query", + query: "SELECT * FROM osquery_info", + team_id: null, + interval: 21600, // 6 hours + platform: "", + min_osquery_version: "", + automations_enabled: false, + logging: "snapshot", + saved: false, + author_id: 2, + author_name: "Test User", + author_email: "test@example.com", + observer_can_run: true, + packs: [], + stats: { + system_time_p50: null, + system_time_p95: null, + user_time_p50: null, + user_time_p95: null, + total_executions: null, + }, + }, + ], +}; + +const teamQueries = { + queries: [ + { + created_at: "2023-06-08T15:31:35Z", + updated_at: "2023-06-08T15:31:35Z", + id: 4, + name: "test specific team query 2", + description: "", + query: "SELECT * FROM video_info;", + team_id: 13, + platform: "windows", + min_osquery_version: "", + automations_enabled: true, + logging: "snapshot", + saved: true, + interval: 0, + observer_can_run: true, + author_id: 1, + author_name: "Jacob", + author_email: "jacob@fleetdm.com", + packs: [], + stats: { + system_time_p50: 1, + // system_time_p95: null, + user_time_p50: 1, + // user_time_p95: null, + total_executions: 1, + }, + performance: "Undetermined", + platforms: ["windows"], + }, + { + created_at: "2023-06-08T15:31:35Z", + updated_at: "2023-06-08T15:31:35Z", + id: 8, + name: "test specific team query", + description: "", + query: "SELECT * FROM osquery_info;", + team_id: 43, + platform: "darwin", + min_osquery_version: "", + automations_enabled: true, + logging: "snapshot", + saved: true, + // interval: 1200, + interval: 0, + observer_can_run: true, + author_id: 1, + author_name: "Jacob", + author_email: "jacob@fleetdm.com", + packs: [], + stats: { + system_time_p50: 4, + // system_time_p95: null, + user_time_p50: 10, + // user_time_p95: null, + total_executions: 1, + }, + performance: "Undetermined", + platforms: ["darwin"], + }, + ], +}; + +const globalQuery1 = { query: globalQueries.queries[0] }; +const globalQuery2 = { query: globalQueries.queries[1] }; +const globalQuery3 = { query: globalQueries.queries[2] }; +const globalQuery4 = { query: globalQueries.queries[4] }; +const globalQuery5 = { query: globalQueries.queries[5] }; +const globalQuery6 = { query: globalQueries.queries[6] }; +const teamQuery1 = { query: teamQueries.queries[0] }; +const teamQuery2 = { query: teamQueries.queries[1] }; + export default { count, hosts, labels, + globalQueries, + globalQuery1, + globalQuery2, + globalQuery3, + globalQuery4, + globalQuery5, + globalQuery6, + teamQueries, + teamQuery1, + teamQuery2, }; diff --git a/frontend/styles/var/colors.scss b/frontend/styles/var/colors.scss index 652b967043..a16b2fa847 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -11,6 +11,7 @@ $site-nav-on-hover: #0e1533; // UI $ui-fleet-black-75: #515774; $ui-fleet-black-50: #8b8fa2; +$ui-fleet-black-33: #b3b6c1; $ui-fleet-black-25: #c5c7d1; $ui-fleet-black-10: #e2e4ea; $ui-fleet-blue-10: #f9fafc; diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts index 15e56ca56e..51cda895c2 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -1,6 +1,7 @@ import URL_PREFIX from "router/url_prefix"; -import { IOsqueryPlatform } from "interfaces/platform"; +import { OsqueryPlatform } from "interfaces/platform"; import paths from "router/paths"; +import { ISchedulableQuery } from "interfaces/schedulable_query"; const { origin } = global.window.location; export const BASE_URL = `${origin}${URL_PREFIX}/api`; @@ -23,7 +24,11 @@ export const DEFAULT_GRAVATAR_LINK_DARK_FALLBACK = "/assets/images/icon-avatar-default-dark-24x24%402x.png"; export const FREQUENCY_DROPDOWN_OPTIONS = [ + { value: 0, label: "Never" }, + { value: 300, label: "Every 5 minutes" }, + { value: 600, label: "Every 10 minutes" }, { value: 900, label: "Every 15 minutes" }, + { value: 1800, label: "Every 30 minutes" }, { value: 3600, label: "Every hour" }, { value: 21600, label: "Every 6 hours" }, { value: 43200, label: "Every 12 hours" }, @@ -47,6 +52,10 @@ export const MAX_OSQUERY_SCHEDULED_QUERY_INTERVAL = 604800; export const MIN_OSQUERY_VERSION_OPTIONS = [ { label: "All", value: "" }, + { label: "5.8.2 +", value: "5.8.2" }, + { label: "5.8.1 +", value: "5.8.1" }, + { label: "5.7.0 +", value: "5.7.0" }, + { label: "5.6.0 +", value: "5.6.0" }, { label: "5.4.0 +", value: "5.4.0" }, { label: "5.3.0 +", value: "5.3.0" }, { label: "5.2.3 +", value: "5.2.4" }, @@ -90,20 +99,26 @@ export const QUERIES_PAGE_STEPS = { 3: "RUN", }; -export const DEFAULT_QUERY = { +export const DEFAULT_QUERY: ISchedulableQuery = { description: "", name: "", query: "SELECT * FROM osquery_info;", id: 0, interval: 0, - last_excuted: "", observer_can_run: false, + platform: "", + min_osquery_version: "", + automations_enabled: false, + logging: "snapshot", author_name: "", updated_at: "", created_at: "", saved: false, author_id: 0, packs: [], + team_id: 0, + author_email: "", + stats: {}, }; export const DEFAULT_CAMPAIGN = { @@ -141,7 +156,7 @@ export const DEFAULT_CAMPAIGN_STATE = { campaign: { ...DEFAULT_CAMPAIGN }, }; -export const PLATFORM_DISPLAY_NAMES: Record = { +export const PLATFORM_DISPLAY_NAMES: Record = { darwin: "macOS", macOS: "macOS", windows: "Windows", @@ -186,8 +201,8 @@ export const PLATFORM_LABEL_DISPLAY_TYPES: Record = { interface IPlatformDropdownOptions { label: "All" | "Windows" | "Linux" | "macOS" | "ChromeOS"; - value: "all" | "windows" | "linux" | "darwin" | "chrome"; - path: string; + value: "all" | "windows" | "linux" | "darwin" | "chrome" | ""; + path?: string; } export const PLATFORM_DROPDOWN_OPTIONS: IPlatformDropdownOptions[] = [ { label: "All", value: "all", path: paths.DASHBOARD }, @@ -198,10 +213,12 @@ export const PLATFORM_DROPDOWN_OPTIONS: IPlatformDropdownOptions[] = [ ]; // Schedules does not support ChromeOS -export const SCHEDULE_PLATFORM_DROPDOWN_OPTIONS: IPlatformDropdownOptions[] = PLATFORM_DROPDOWN_OPTIONS.slice( - 0, - -1 -); +export const SCHEDULE_PLATFORM_DROPDOWN_OPTIONS: IPlatformDropdownOptions[] = [ + { label: "All", value: "" }, // API empty string runs on all platforms + { label: "macOS", value: "darwin" }, + { label: "Windows", value: "windows" }, + { label: "Linux", value: "linux" }, +]; export const PLATFORM_NAME_TO_LABEL_NAME = { all: "", diff --git a/frontend/utilities/osquery_tables.ts b/frontend/utilities/osquery_tables.ts index ea0de23953..f71bbeb0ed 100644 --- a/frontend/utilities/osquery_tables.ts +++ b/frontend/utilities/osquery_tables.ts @@ -4,7 +4,7 @@ import { IOsQueryTable } from "interfaces/osquery_table"; import osqueryFleetTablesJSON from "../../schema/osquery_fleet_schema.json"; // Typecasting explicity here as we are adding more rigid types such as -// IOsqueryPlatform for platform names, instead of just any strings. +// OsqueryPlatform for platform names, instead of just any strings. const queryTable = osqueryFleetTablesJSON as IOsQueryTable[]; export const osqueryTables = queryTable.sort((a, b) => { diff --git a/frontend/utilities/sql_tools.ts b/frontend/utilities/sql_tools.ts index 949b4ebb79..16d7c40b00 100644 --- a/frontend/utilities/sql_tools.ts +++ b/frontend/utilities/sql_tools.ts @@ -3,9 +3,10 @@ import sqliteParser from "sqlite-parser"; import { intersection, isPlainObject } from "lodash"; import { osqueryTables } from "utilities/osquery_tables"; import { - IOsqueryPlatform, + OsqueryPlatform, MACADMINS_EXTENSION_TABLES, SUPPORTED_PLATFORMS, + SupportedPlatform, } from "interfaces/platform"; type IAstNode = Record; @@ -24,10 +25,10 @@ interface ISqlCteNode { // TODO: Is it ever possible that osquery_tables.json would be missing name or platforms? interface IOsqueryTable { name: string; - platforms: IOsqueryPlatform[]; + platforms: OsqueryPlatform[]; } -type IPlatformDictionay = Record; +type IPlatformDictionay = Record; const platformsByTableDictionary: IPlatformDictionay = (osqueryTables as IOsqueryTable[]).reduce( (dictionary: IPlatformDictionay, osqueryTable) => { @@ -64,7 +65,9 @@ const _visit = ( } }; -const filterCompatiblePlatforms = (sqlTables: string[]): IOsqueryPlatform[] => { +const filterCompatiblePlatforms = ( + sqlTables: string[] +): SupportedPlatform[] => { if (!sqlTables.length) { return [...SUPPORTED_PLATFORMS]; // if a query has no tables but is still syntatically valid sql, it is treated as compatible with all platforms } @@ -122,7 +125,7 @@ const parseSqlTables = ( const checkPlatformCompatibility = ( sqlString: string, includeCteTables = false -): { platforms: IOsqueryPlatform[] | null; error: Error | null } => { +): { platforms: SupportedPlatform[] | null; error: Error | null } => { let sqlTables: string[] | undefined; try { sqlTables = parseSqlTables(sqlString, includeCteTables); diff --git a/go.mod b/go.mod index a12eea1008..d125bdee10 100644 --- a/go.mod +++ b/go.mod @@ -86,7 +86,7 @@ require ( github.com/spf13/cast v1.3.1 github.com/spf13/cobra v1.5.0 github.com/spf13/viper v1.8.1 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/theupdateframework/go-tuf v0.5.0 github.com/throttled/throttled/v2 v2.8.0 github.com/tj/assert v0.0.3 @@ -94,21 +94,21 @@ require ( github.com/urfave/cli/v2 v2.23.5 github.com/valyala/fasthttp v1.40.0 go.elastic.co/apm/module/apmgorilla/v2 v2.3.0 - go.elastic.co/apm/module/apmsql v1.15.0 - go.elastic.co/apm/v2 v2.3.0 + go.elastic.co/apm/module/apmsql/v2 v2.4.3 + go.elastic.co/apm/v2 v2.4.3 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.40.0 go.opentelemetry.io/otel v1.14.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0 go.opentelemetry.io/otel/sdk v1.14.0 - golang.org/x/crypto v0.1.0 + golang.org/x/crypto v0.9.0 golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3 - golang.org/x/net v0.8.0 + golang.org/x/net v0.10.0 golang.org/x/oauth2 v0.6.0 golang.org/x/sync v0.1.0 - golang.org/x/sys v0.6.0 - golang.org/x/text v0.8.0 + golang.org/x/sys v0.8.0 + golang.org/x/text v0.9.0 google.golang.org/grpc v1.54.0 gopkg.in/guregu/null.v3 v3.5.0 gopkg.in/ini.v1 v1.67.0 @@ -189,7 +189,6 @@ require ( github.com/docker/distribution v2.8.0+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect - github.com/elastic/go-licenser v0.4.0 // indirect github.com/elastic/go-sysinfo v1.7.1 // indirect github.com/elastic/go-windows v1.0.1 // indirect github.com/emirpasic/gods v1.12.0 // indirect @@ -241,7 +240,6 @@ require ( github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jcchavezs/porto v0.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect @@ -274,7 +272,6 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/slack-go/slack v0.9.4 // indirect @@ -298,7 +295,6 @@ require ( github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/ziutek/mymysql v1.5.4 // indirect - go.elastic.co/apm v1.15.0 // indirect go.elastic.co/apm/module/apmhttp/v2 v2.3.0 // indirect go.elastic.co/fastjson v1.1.0 // indirect go.opencensus.io v0.24.0 // indirect @@ -309,11 +305,8 @@ require ( go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.21.0 // indirect gocloud.dev v0.24.0 // indirect - golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/term v0.6.0 // indirect + golang.org/x/term v0.8.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.6.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.114.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 3dc1ef8ee3..c592b59e89 100644 --- a/go.sum +++ b/go.sum @@ -330,7 +330,6 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -338,14 +337,12 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crewjam/saml v0.0.0-20190521120225-344d075952c9/go.mod h1:w5eu+HNtubx+kRpQL6QFT2F3yIFfYVe6+EzOFVU7Hko= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -395,10 +392,7 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/e-dard/netbug v0.0.0-20151029172837-e64d308a0b20 h1:eDPsdileewX4H5a2Jph4gS8mFf749gzIrzpbnPy1oRs= github.com/e-dard/netbug v0.0.0-20151029172837-e64d308a0b20/go.mod h1:WXFUXJ0Y/SzNqXmhUU7VkE7a2Pag0zZnE2b6I87YWIs= -github.com/elastic/go-licenser v0.3.1/go.mod h1:D8eNQk70FOCVBl3smCGQt/lv7meBeQno2eI1S5apiHQ= -github.com/elastic/go-licenser v0.4.0 h1:jLq6A5SilDS/Iz1ABRkO6BHy91B9jBora8FwGRsDqUI= github.com/elastic/go-licenser v0.4.0/go.mod h1:V56wHMpmdURfibNBggaSBfqgPxyT1Tldns1i87iTEvU= -github.com/elastic/go-sysinfo v1.1.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0= github.com/elastic/go-sysinfo v1.7.1 h1:Wx4DSARcKLllpKT2TnFVdSUJOsybqMYCNQZq1/wO+s0= github.com/elastic/go-sysinfo v1.7.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0= github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= @@ -545,7 +539,6 @@ github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/E github.com/gocarina/gocsv v0.0.0-20220310154401-d4df709ca055 h1:UfcDMw41lSx3XM7UvD1i7Fsu3rMgD55OU5LYwLoR/Yk= github.com/gocarina/gocsv v0.0.0-20220310154401-d4df709ca055/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -774,51 +767,9 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= -github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.7.0/go.mod h1:sF/lPpNEMEOp+IYhyQGdAvrG20gWf6A1tKlr0v7JMeA= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.5/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= -github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= -github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= -github.com/jackc/pgtype v1.5.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= -github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= -github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= -github.com/jackc/pgx/v4 v4.9.0/go.mod h1:MNGWmViCgqbZck9ujOOBN63gK9XVGILXWCvKLGKmnms= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jcchavezs/porto v0.1.0 h1:Xmxxn25zQMmgE7/yHYmh19KcItG81hIwfbEEFnd6w/Q= github.com/jcchavezs/porto v0.1.0/go.mod h1:fESH0gzDHiutHRdX2hv27ojnOVFco37hg1W6E9EZF4A= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= @@ -886,16 +837,12 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.2/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -919,7 +866,6 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -927,9 +873,7 @@ github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqf github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= @@ -1100,8 +1044,6 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM= @@ -1112,9 +1054,6 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= -github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e/go.mod h1:9Tc1SKnfACJb9N7cw2eyuI6xzy845G7uZONBsi5uPEA= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc= @@ -1130,8 +1069,6 @@ github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= github.com/shirou/gopsutil/v3 v3.22.8 h1:a4s3hXogo5mE2PfdfJIonDbstO/P+9JszdfhAHSzD9Y= github.com/shirou/gopsutil/v3 v3.22.8/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -1196,8 +1133,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= @@ -1274,16 +1211,15 @@ github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c h1:TWQ2UvXPkhPxI2KmApKBOCaV6yD2N4mlvqFQ/DlPtpQ= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY= -go.elastic.co/apm v1.15.0 h1:uPk2g/whK7c7XiZyz/YCUnAUBNPiyNeE3ARX3G6Gx7Q= -go.elastic.co/apm v1.15.0/go.mod h1:dylGv2HKR0tiCV+wliJz1KHtDyuD8SPe69oV7VyK6WY= go.elastic.co/apm/module/apmgorilla/v2 v2.3.0 h1:jHw8N252UTwKTk945+Am8AaawhHC6DWpFVeTXQO8Gko= go.elastic.co/apm/module/apmgorilla/v2 v2.3.0/go.mod h1:2LXDBbVhFf9rF65jZecvl78IZMuvSRldQ+9A/fjfIo0= go.elastic.co/apm/module/apmhttp/v2 v2.3.0 h1:yGZyp26uJXUCfRTwvMmDt1d1jJrHgTBBncZfpYAxR8s= go.elastic.co/apm/module/apmhttp/v2 v2.3.0/go.mod h1:JCszLIey4ndJGuUUu5FQjNOiTfaln1dqCqXnRcXVxVc= -go.elastic.co/apm/module/apmsql v1.15.0 h1:QAy7tM9NwWvqMOdl8KZQsCPzy5XwYdGDkHqdd6QmGj8= -go.elastic.co/apm/module/apmsql v1.15.0/go.mod h1:9G1TINaFFEqRYBcxJFQ0HGsRQENJ0MCkahNKKre1Fao= -go.elastic.co/apm/v2 v2.3.0 h1:jsZQsGWyMyga6xRMcYhKtPvrr5en8wqbmJNmxltST/E= +go.elastic.co/apm/module/apmsql/v2 v2.4.3 h1:wFIibO4FLDNm6B5bQt4YAgt1ZS0X2Rd27HYXXaqVPOo= +go.elastic.co/apm/module/apmsql/v2 v2.4.3/go.mod h1:y8TG3VQepEkAZxMZfyPbb9s3J4B7SP9fWiVnwxmIrJg= go.elastic.co/apm/v2 v2.3.0/go.mod h1:HdwVuAeoJMmoqAZZBNN2YVzj3UVLebtqoRCCydyCP+Q= +go.elastic.co/apm/v2 v2.4.3 h1:k6mj63O7IIyqqn3S52C2vBXvaSK9M5FHp0aZHpPH/as= +go.elastic.co/apm/v2 v2.4.3/go.mod h1:+CiBUdrrAGnGCL9TNx7tQz3BrfYV23L8Ljvotoc87so= go.elastic.co/fastjson v1.1.0 h1:3MrGBWWVIxe/xvsbpghtkFoPciPhOCmjsR/HfwEeQR4= go.elastic.co/fastjson v1.1.0/go.mod h1:boNGISWMjQsUPy/t6yqt2/1Wx4YNPSe+mZjlyw9vKKI= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -1325,9 +1261,7 @@ go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+go go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -1336,13 +1270,10 @@ go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpK go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= @@ -1358,16 +1289,13 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1381,8 +1309,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1408,7 +1336,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -1424,8 +1351,6 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1447,7 +1372,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1489,8 +1413,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1542,7 +1466,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1554,7 +1477,6 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1634,16 +1556,16 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1655,8 +1577,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1673,7 +1595,6 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -1681,13 +1602,10 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191025023517-2077df36852e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1734,10 +1652,6 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1911,7 +1825,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= gopkg.in/guregu/null.v3 v3.5.0 h1:xTcasT8ETfMcUHn0zTvIYtQud/9Mx5dJqD554SZct0o= gopkg.in/guregu/null.v3 v3.5.0/go.mod h1:E4tX2Qe3h7QdL+uZ3a0vqvYwKQsRSQKM5V4YltdgH9Y= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/server/authz/policy.rego b/server/authz/policy.rego index a604fce2b3..519c6eb6aa 100644 --- a/server/authz/policy.rego +++ b/server/authz/policy.rego @@ -291,21 +291,6 @@ allow { # Queries ## -# Global admins, maintainers, observer_plus and observers can read queries. -allow { - object.type == "query" - subject.global_role == [admin, maintainer, observer_plus, observer][_] - action == read -} - -# Team admins, maintainers, observer_plus and observers can read queries. -allow { - object.type == "query" - # If role is admin, maintainer, observer_plus or observer on any team. - team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_] - action == read -} - # Global admins, maintainers and gitops can write queries. allow { object.type == "query" @@ -313,54 +298,90 @@ allow { action == write } -# Team admins, maintainers and gitops can create new queries +# Global admins, maintainers, observer_plus and observers can read queries. allow { - object.id == 0 # new queries have ID zero object.type == "query" - team_role(subject, subject.teams[_].id) == [admin, maintainer, gitops][_] + subject.global_role == [admin, maintainer, observer_plus, observer][_] + action == read +} + +# Team admin, maintainers and gitops can write queries for their teams. +allow { + object.type == "query" + not is_null(object.team_id) + team_role(subject, object.team_id) == [admin, maintainer, gitops][_] action == write } -# Team admins, maintainers and gitops can edit and delete only their own queries +# Team admins, maintainers, observer_plus and observers can read queries for their teams. allow { - object.author_id == subject.id object.type == "query" - team_role(subject, subject.teams[_].id) == [admin, maintainer, gitops][_] - action == write + not is_null(object.team_id) + team_role(subject, object.team_id) == [admin, maintainer, observer_plus, observer][_] + action == read } -# Global admins, maintainers and observer_plus can run any query (saved and new). +# Team admins, maintainers, observer_plus and observers can read global queries. +allow { + object.type == "query" + is_null(object.team_id) + team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_] + action == read +} + +# Global admins, maintainers and observer_plus can run any query saved query. allow { object.type == "targeted_query" subject.global_role == [admin, maintainer, observer_plus][_] action = run } + +# Global admins, maintainers and observer_plus can run any new query. allow { object.type == "query" subject.global_role == [admin, maintainer, observer_plus][_] action = run_new } -# Team admin, maintainer and observer_plus running a non-observers_can_run query must have the targets -# filtered to only teams that they maintain. +# Team admin, maintainer and observer_plus running a global non-observers_can_run query +# must have the targets filtered to only teams that they maintain. allow { object.type == "targeted_query" object.observer_can_run == false is_null(subject.global_role) action == run + + is_null(object.team_id) not is_null(object.host_targets.teams) ok_teams := { tmid | tmid := object.host_targets.teams[_]; team_role(subject, tmid) == [admin, maintainer, observer_plus][_] } count(ok_teams) == count(object.host_targets.teams) } -# Team admin, maintainer and observer_plus running a non-observers_can_run query when no target teams are specified. +# Team admin, maintainer and observer_plus running a non-observers_can_run query that belongs to their team +# must have the targets filtered to only teams that they maintain. +allow { + object.type == "targeted_query" + object.observer_can_run == false + is_null(subject.global_role) + action == run + + team_role(subject, object.team_id) == [admin, maintainer, observer_plus][_] + + not is_null(object.host_targets.teams) + ok_teams := { tmid | tmid := object.host_targets.teams[_]; team_role(subject, tmid) == [admin, maintainer, observer_plus][_] } + count(ok_teams) == count(object.host_targets.teams) +} + +# Team admin, maintainer and observer_plus running a global non-observers_can_run query when no target teams are specified. allow { object.type == "targeted_query" object.observer_can_run == false is_null(subject.global_role) action == run + is_null(object.team_id) + # If role is admin, maintainer or observer_plus on any team. team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus][_] @@ -368,6 +389,19 @@ allow { is_null(object.host_targets.teams) } +# Team admin, maintainer and observer_plus running a non-observers_can_run query that belongs to their team when no target teams are specified. +allow { + object.type == "targeted_query" + object.observer_can_run == false + is_null(subject.global_role) + action == run + + team_role(subject, object.team_id) == [admin, maintainer, observer_plus][_] + + # there are no team targets + is_null(object.host_targets.teams) +} + # Team admin, maintainer and observer_plus can run a new query. allow { object.type == "query" @@ -384,25 +418,44 @@ allow { action = run } -# Team admin, maintainer, observer_plus and observer running a observers_can_run query must have the targets +# Team admin, maintainer, observer_plus and observer running a global observers_can_run query must have the targets # filtered to only teams that they observe. allow { object.type == "targeted_query" object.observer_can_run == true is_null(subject.global_role) action == run + + is_null(object.team_id) not is_null(object.host_targets.teams) ok_teams := { tmid | tmid := object.host_targets.teams[_]; team_role(subject, tmid) == [admin, maintainer, observer_plus, observer][_] } count(ok_teams) == count(object.host_targets.teams) } -# Team admin, maintainer, observer_plus and observer running a observers_can_run query and there are no target teams. +# Team admin, maintainer, observer_plus and observer running an observers_can_run query that belongs to their team must have the targets +# filtered to only teams that they observe. allow { object.type == "targeted_query" object.observer_can_run == true is_null(subject.global_role) action == run + + team_role(subject, object.team_id) == [admin, maintainer, observer_plus, observer][_] + + not is_null(object.host_targets.teams) + ok_teams := { tmid | tmid := object.host_targets.teams[_]; team_role(subject, tmid) == [admin, maintainer, observer_plus, observer][_] } + count(ok_teams) == count(object.host_targets.teams) +} + +# Team admin, maintainer, observer_plus and observer running a global observers_can_run query and there are no target teams. +allow { + object.type == "targeted_query" + object.observer_can_run == true + is_null(subject.global_role) + action == run + + is_null(object.team_id) # If role is admin, maintainer, observer_plus or observer on any team. team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_] @@ -411,6 +464,19 @@ allow { is_null(object.host_targets.teams) } +# Team admin, maintainer, observer_plus and observer running an observers_can_run query that belongs to their team and there are no target teams. +allow { + object.type == "targeted_query" + object.observer_can_run == true + is_null(subject.global_role) + action == run + + team_role(subject, object.team_id) == [admin, maintainer, observer_plus, observer][_] + + # there are no team targets + is_null(object.host_targets.teams) +} + ## # Targets ## @@ -431,55 +497,16 @@ allow { } ## -# Packs +# 2017 Packs (deprecated) ## -# Global admins, maintainers and gitops can read/write all types of packs. +# Global admins, maintainers and gitops can read/write 2017 packs. allow { object.type == "pack" subject.global_role == [admin, maintainer, gitops][_] action == [read, write][_] } -# Global admins, maintainers, observers and observer_plus can read the global pack. -allow { - object.type == "pack" - object.is_global_pack == true - subject.global_role == [admin, maintainer, observer, observer_plus][_] - action == read -} - -# Team admins, maintainers, observer_plus and observers can read the global pack. -allow { - object.type == "pack" - object.is_global_pack == true - # If role is admin, maintainer, observer_plus or observer on any team. - team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer][_] - action == read -} - -# Team admins, maintainers, observers, observer_plus can read their team's pack. -# -# NOTE: Action "read" on a team's pack includes listing its scheduled queries. -allow { - object.type == "pack" - not is_null(object.pack_team_id) - team_role(subject, object.pack_team_id) == [admin, maintainer, observer, observer_plus][_] - action == read -} - -# Team admins, maintainers and gitops can add/remove scheduled queries from/to their team's pack. -# -# NOTE: The team's pack is not editable per-se, it's a special pack to group -# all the team's scheduled queries. So the "write" operation only covers -# adding/removing scheduled queries from the pack. -allow { - object.type == "pack" - not is_null(object.pack_team_id) - team_role(subject, object.pack_team_id) == [admin, maintainer, gitops][_] - action == write -} - ## # File Carves ## diff --git a/server/authz/policy_test.go b/server/authz/policy_test.go index b24f6d448c..0dd7f8efd2 100644 --- a/server/authz/policy_test.go +++ b/server/authz/policy_test.go @@ -681,225 +681,405 @@ func TestAuthorizeQuery(t *testing.T) { }, } - query := &fleet.Query{ObserverCanRun: false} - emptyTquery := &fleet.TargetedQuery{Query: query} - team1Query := &fleet.TargetedQuery{HostTargets: fleet.HostTargets{TeamIDs: []uint{1}}, Query: query} - team12Query := &fleet.TargetedQuery{HostTargets: fleet.HostTargets{TeamIDs: []uint{1, 2}}, Query: query} - team2Query := &fleet.TargetedQuery{HostTargets: fleet.HostTargets{TeamIDs: []uint{2}}, Query: query} - team123Query := &fleet.TargetedQuery{HostTargets: fleet.HostTargets{TeamIDs: []uint{1, 2, 3}}, Query: query} + globalQuery := &fleet.Query{ + ObserverCanRun: false, + } + globalQueryNoTargets := &fleet.TargetedQuery{ + Query: globalQuery, + } - observerQuery := &fleet.Query{ObserverCanRun: true} - emptyTobsQuery := &fleet.TargetedQuery{Query: observerQuery} - team1ObsQuery := &fleet.TargetedQuery{HostTargets: fleet.HostTargets{TeamIDs: []uint{1}}, Query: observerQuery} - team12ObsQuery := &fleet.TargetedQuery{HostTargets: fleet.HostTargets{TeamIDs: []uint{1, 2}}, Query: observerQuery} - team2ObsQuery := &fleet.TargetedQuery{HostTargets: fleet.HostTargets{TeamIDs: []uint{2}}, Query: observerQuery} - team123ObsQuery := &fleet.TargetedQuery{HostTargets: fleet.HostTargets{TeamIDs: []uint{1, 2, 3}}, Query: observerQuery} + globalQueryTargetedToTeam1 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{1}}, + Query: globalQuery, + } + globalQueryTargetedToTeam1AndTeam2 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{1, 2}}, + Query: globalQuery, + } + globalQueryTargetedToTeam2 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{2}}, + Query: globalQuery, + } + globalQueryTargetedToTeam1AndTeam2AndTeam3 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{1, 2, 3}}, + Query: globalQuery, + } - teamAdminQuery := &fleet.Query{ID: 1, AuthorID: ptr.Uint(teamAdmin.ID), ObserverCanRun: false} - teamMaintQuery := &fleet.Query{ID: 2, AuthorID: ptr.Uint(teamMaintainer.ID), ObserverCanRun: false} + globalObserverQuery := &fleet.Query{ + ObserverCanRun: true, + } + globalObserverQueryEmptyTargets := &fleet.TargetedQuery{ + Query: globalObserverQuery, + } + globalObserverQueryTargetedToTeam1 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{1}}, + Query: globalObserverQuery, + } + globalObserverQueryTargetedToTeam1AndTeam2 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{1, 2}}, + Query: globalObserverQuery, + } + globalObserverQueryTargetedToTeam2 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{2}}, + Query: globalObserverQuery, + } + globalObserverQueryTargetedToTeam1AndTeam2AndTeam3 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{1, 2, 3}}, + Query: globalObserverQuery, + } + + teamAdminQuery := &fleet.Query{ + ID: 1, + AuthorID: ptr.Uint(teamAdmin.ID), + ObserverCanRun: false, + TeamID: ptr.Uint(1), + } + teamMaintQuery := &fleet.Query{ + ID: 2, + AuthorID: ptr.Uint(teamMaintainer.ID), + ObserverCanRun: false, + TeamID: ptr.Uint(1), + } globalAdminQuery := &fleet.Query{ID: 3, AuthorID: ptr.Uint(test.UserAdmin.ID), ObserverCanRun: false} globalGitOpsQuery := &fleet.Query{ID: 4, AuthorID: ptr.Uint(test.UserGitOps.ID), ObserverCanRun: false} - teamGitOpsQuery := &fleet.Query{ID: 5, AuthorID: ptr.Uint(teamGitOps.ID), ObserverCanRun: false} + teamGitOpsQuery := &fleet.Query{ + ID: 5, AuthorID: ptr.Uint(teamGitOps.ID), + ObserverCanRun: false, + TeamID: ptr.Uint(1), + } + observerQueryOnTeam3 := &fleet.Query{ + ID: 6, + ObserverCanRun: true, + TeamID: ptr.Uint(3), + } + observerQueryOnTeam3TargetedToTeam3 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{3}}, + Query: observerQueryOnTeam3, + } + observerQueryOnTeam3TargetedToTeam2 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{2}}, + Query: observerQueryOnTeam3, + } + observerQueryOnTeam3TargetedToTeam1 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{1}}, + Query: observerQueryOnTeam3, + } + observerQueryOnTeam1 := &fleet.Query{ + ID: 7, + ObserverCanRun: true, + TeamID: ptr.Uint(1), + } + observerQueryOnTeam1TargetedToTeam1 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{1}}, + Query: observerQueryOnTeam1, + } + observerQueryOnTeam1TargetedToTeam2 := &fleet.TargetedQuery{ + HostTargets: fleet.HostTargets{TeamIDs: []uint{2}}, + Query: observerQueryOnTeam1, + } - runTestCases(t, []authTestCase{ - // No access - {user: nil, object: query, action: read, allow: false}, - {user: nil, object: query, action: write, allow: false}, - {user: nil, object: teamAdminQuery, action: write, allow: false}, - {user: nil, object: emptyTquery, action: run, allow: false}, - {user: nil, object: team1Query, action: run, allow: false}, - {user: nil, object: query, action: runNew, allow: false}, - {user: nil, object: observerQuery, action: read, allow: false}, - {user: nil, object: observerQuery, action: write, allow: false}, - {user: nil, object: emptyTobsQuery, action: run, allow: false}, - {user: nil, object: team1ObsQuery, action: run, allow: false}, - {user: nil, object: observerQuery, action: runNew, allow: false}, + runTestCasesGroups(t, []tcGroup{ + { + name: "no access", + testCases: []authTestCase{ + {user: nil, object: globalQuery, action: read, allow: false}, + {user: nil, object: globalQuery, action: write, allow: false}, + {user: nil, object: teamAdminQuery, action: write, allow: false}, + {user: nil, object: globalQueryNoTargets, action: run, allow: false}, + {user: nil, object: globalQueryTargetedToTeam1, action: run, allow: false}, + {user: nil, object: globalQuery, action: runNew, allow: false}, + {user: nil, object: globalObserverQuery, action: read, allow: false}, + {user: nil, object: globalObserverQuery, action: write, allow: false}, + {user: nil, object: globalObserverQueryEmptyTargets, action: run, allow: false}, + {user: nil, object: globalObserverQueryTargetedToTeam1, action: run, allow: false}, + {user: nil, object: globalObserverQuery, action: runNew, allow: false}, + }, + }, + { + name: "User with no roles cannot access queries", + testCases: []authTestCase{ + {user: test.UserNoRoles, object: globalQuery, action: read, allow: false}, + {user: test.UserNoRoles, object: globalQuery, action: write, allow: false}, + {user: test.UserNoRoles, object: teamAdminQuery, action: write, allow: false}, + {user: test.UserNoRoles, object: globalQueryNoTargets, action: run, allow: false}, + {user: test.UserNoRoles, object: globalQueryTargetedToTeam1, action: run, allow: false}, + {user: test.UserNoRoles, object: globalQuery, action: runNew, allow: false}, + {user: test.UserNoRoles, object: globalObserverQuery, action: read, allow: false}, + {user: test.UserNoRoles, object: globalObserverQuery, action: write, allow: false}, + {user: test.UserNoRoles, object: globalObserverQueryEmptyTargets, action: run, allow: false}, + {user: test.UserNoRoles, object: globalObserverQueryTargetedToTeam1, action: run, allow: false}, + {user: test.UserNoRoles, object: globalObserverQuery, action: runNew, allow: false}, + }, + }, + { + name: "Global observer can read", + testCases: []authTestCase{ + {user: test.UserObserver, object: globalQuery, action: read, allow: true}, + {user: test.UserObserver, object: globalQuery, action: write, allow: false}, + {user: test.UserObserver, object: teamAdminQuery, action: write, allow: false}, + {user: test.UserObserver, object: globalQueryNoTargets, action: run, allow: false}, + {user: test.UserObserver, object: globalQueryTargetedToTeam1, action: run, allow: false}, + {user: test.UserObserver, object: globalQuery, action: runNew, allow: false}, + {user: test.UserObserver, object: globalObserverQuery, action: read, allow: true}, + {user: test.UserObserver, object: globalObserverQuery, action: write, allow: false}, + {user: test.UserObserver, object: globalObserverQueryEmptyTargets, action: run, allow: true}, // can run observer query + {user: test.UserObserver, object: globalObserverQueryTargetedToTeam1, action: run, allow: true}, // can run observer query + {user: test.UserObserver, object: globalObserverQueryTargetedToTeam1AndTeam2, action: run, allow: true}, // can run observer query + {user: test.UserObserver, object: globalObserverQuery, action: runNew, allow: false}, - // User with no roles cannot access queries. - {user: test.UserNoRoles, object: query, action: read, allow: false}, - {user: test.UserNoRoles, object: query, action: write, allow: false}, - {user: test.UserNoRoles, object: teamAdminQuery, action: write, allow: false}, - {user: test.UserNoRoles, object: emptyTquery, action: run, allow: false}, - {user: test.UserNoRoles, object: team1Query, action: run, allow: false}, - {user: test.UserNoRoles, object: query, action: runNew, allow: false}, - {user: test.UserNoRoles, object: observerQuery, action: read, allow: false}, - {user: test.UserNoRoles, object: observerQuery, action: write, allow: false}, - {user: test.UserNoRoles, object: emptyTobsQuery, action: run, allow: false}, - {user: test.UserNoRoles, object: team1ObsQuery, action: run, allow: false}, - {user: test.UserNoRoles, object: observerQuery, action: runNew, allow: false}, + {user: test.UserObserver, object: observerQueryOnTeam3, action: read, allow: true}, + {user: test.UserObserver, object: observerQueryOnTeam3, action: write, allow: false}, + {user: test.UserObserver, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: true}, + {user: test.UserObserver, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: true}, + }, + }, + { + name: "Global observer+ can read all queries, not write them, and can run any query", + testCases: []authTestCase{ + {user: test.UserObserverPlus, object: globalQuery, action: read, allow: true}, + {user: test.UserObserverPlus, object: globalQuery, action: write, allow: false}, + {user: test.UserObserverPlus, object: teamAdminQuery, action: write, allow: false}, + {user: test.UserObserverPlus, object: globalQueryNoTargets, action: run, allow: true}, + {user: test.UserObserverPlus, object: globalQueryTargetedToTeam1, action: run, allow: true}, + {user: test.UserObserverPlus, object: globalQuery, action: runNew, allow: true}, + {user: test.UserObserverPlus, object: globalObserverQuery, action: read, allow: true}, + {user: test.UserObserverPlus, object: globalObserverQuery, action: write, allow: false}, + {user: test.UserObserverPlus, object: globalObserverQueryEmptyTargets, action: run, allow: true}, // can run observer query + {user: test.UserObserverPlus, object: globalObserverQueryTargetedToTeam1, action: run, allow: true}, // can run observer query + {user: test.UserObserverPlus, object: globalObserverQueryTargetedToTeam1AndTeam2, action: run, allow: true}, // can run observer query + {user: test.UserObserverPlus, object: globalObserverQuery, action: runNew, allow: true}, - // Global observer can read - {user: test.UserObserver, object: query, action: read, allow: true}, - {user: test.UserObserver, object: query, action: write, allow: false}, - {user: test.UserObserver, object: teamAdminQuery, action: write, allow: false}, - {user: test.UserObserver, object: emptyTquery, action: run, allow: false}, - {user: test.UserObserver, object: team1Query, action: run, allow: false}, - {user: test.UserObserver, object: query, action: runNew, allow: false}, - {user: test.UserObserver, object: observerQuery, action: read, allow: true}, - {user: test.UserObserver, object: observerQuery, action: write, allow: false}, - {user: test.UserObserver, object: emptyTobsQuery, action: run, allow: true}, // can run observer query - {user: test.UserObserver, object: team1ObsQuery, action: run, allow: true}, // can run observer query - {user: test.UserObserver, object: team12ObsQuery, action: run, allow: true}, // can run observer query - {user: test.UserObserver, object: observerQuery, action: runNew, allow: false}, + {user: test.UserObserverPlus, object: observerQueryOnTeam3, action: read, allow: true}, + {user: test.UserObserverPlus, object: observerQueryOnTeam3, action: write, allow: false}, + {user: test.UserObserverPlus, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: true}, + {user: test.UserObserverPlus, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: true}, + }, + }, + { + name: "Global maintainer can read/write/run any query", + testCases: []authTestCase{ + {user: test.UserMaintainer, object: globalQuery, action: read, allow: true}, + {user: test.UserMaintainer, object: globalQuery, action: write, allow: true}, + {user: test.UserMaintainer, object: teamMaintQuery, action: write, allow: true}, + {user: test.UserMaintainer, object: globalAdminQuery, action: write, allow: true}, + {user: test.UserMaintainer, object: globalQueryNoTargets, action: run, allow: true}, + {user: test.UserMaintainer, object: globalQueryTargetedToTeam1, action: run, allow: true}, + {user: test.UserMaintainer, object: globalQuery, action: runNew, allow: true}, + {user: test.UserMaintainer, object: globalObserverQuery, action: read, allow: true}, + {user: test.UserMaintainer, object: globalObserverQuery, action: write, allow: true}, + {user: test.UserMaintainer, object: globalObserverQueryEmptyTargets, action: run, allow: true}, + {user: test.UserMaintainer, object: globalObserverQueryTargetedToTeam1, action: run, allow: true}, + {user: test.UserMaintainer, object: globalObserverQuery, action: runNew, allow: true}, - // Global observer+ can read all queries, not write them, and can run any query. - {user: test.UserObserverPlus, object: query, action: read, allow: true}, - {user: test.UserObserverPlus, object: query, action: write, allow: false}, - {user: test.UserObserverPlus, object: teamAdminQuery, action: write, allow: false}, - {user: test.UserObserverPlus, object: emptyTquery, action: run, allow: true}, - {user: test.UserObserverPlus, object: team1Query, action: run, allow: true}, - {user: test.UserObserverPlus, object: query, action: runNew, allow: true}, - {user: test.UserObserverPlus, object: observerQuery, action: read, allow: true}, - {user: test.UserObserverPlus, object: observerQuery, action: write, allow: false}, - {user: test.UserObserverPlus, object: emptyTobsQuery, action: run, allow: true}, // can run observer query - {user: test.UserObserverPlus, object: team1ObsQuery, action: run, allow: true}, // can run observer query - {user: test.UserObserverPlus, object: team12ObsQuery, action: run, allow: true}, // can run observer query - {user: test.UserObserverPlus, object: observerQuery, action: runNew, allow: true}, + {user: test.UserMaintainer, object: observerQueryOnTeam3, action: read, allow: true}, + {user: test.UserMaintainer, object: observerQueryOnTeam3, action: write, allow: true}, + {user: test.UserMaintainer, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: true}, + {user: test.UserMaintainer, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: true}, + }, + }, + { + name: "Global admin can read/write/run any query (on its team)", + testCases: []authTestCase{ + {user: test.UserAdmin, object: globalQuery, action: read, allow: true}, + {user: test.UserAdmin, object: globalQuery, action: write, allow: true}, + {user: test.UserAdmin, object: teamMaintQuery, action: write, allow: true}, + {user: test.UserAdmin, object: globalAdminQuery, action: write, allow: true}, + {user: test.UserAdmin, object: globalQueryNoTargets, action: run, allow: true}, + {user: test.UserAdmin, object: globalQueryTargetedToTeam1, action: run, allow: true}, + {user: test.UserAdmin, object: globalQuery, action: runNew, allow: true}, + {user: test.UserAdmin, object: globalObserverQuery, action: read, allow: true}, + {user: test.UserAdmin, object: globalObserverQuery, action: write, allow: true}, + {user: test.UserAdmin, object: globalObserverQueryEmptyTargets, action: run, allow: true}, + {user: test.UserAdmin, object: globalObserverQueryTargetedToTeam1, action: run, allow: true}, + {user: test.UserAdmin, object: globalObserverQuery, action: runNew, allow: true}, - // Global maintainer can read/write (even not authored by them)/run any. - {user: test.UserMaintainer, object: query, action: read, allow: true}, - {user: test.UserMaintainer, object: query, action: write, allow: true}, - {user: test.UserMaintainer, object: teamMaintQuery, action: write, allow: true}, - {user: test.UserMaintainer, object: globalAdminQuery, action: write, allow: true}, - {user: test.UserMaintainer, object: emptyTquery, action: run, allow: true}, - {user: test.UserMaintainer, object: team1Query, action: run, allow: true}, - {user: test.UserMaintainer, object: query, action: runNew, allow: true}, - {user: test.UserMaintainer, object: observerQuery, action: read, allow: true}, - {user: test.UserMaintainer, object: observerQuery, action: write, allow: true}, - {user: test.UserMaintainer, object: emptyTobsQuery, action: run, allow: true}, - {user: test.UserMaintainer, object: team1ObsQuery, action: run, allow: true}, - {user: test.UserMaintainer, object: observerQuery, action: runNew, allow: true}, + {user: test.UserAdmin, object: observerQueryOnTeam3, action: read, allow: true}, + {user: test.UserAdmin, object: observerQueryOnTeam3, action: write, allow: true}, + {user: test.UserAdmin, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: true}, + {user: test.UserAdmin, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: true}, + }, + }, + { + name: "Global GitOps cannot read, or run any query, but can write", + testCases: []authTestCase{ + {user: test.UserGitOps, object: globalQuery, action: read, allow: false}, + {user: test.UserGitOps, object: globalQuery, action: write, allow: true}, + {user: test.UserGitOps, object: teamAdminQuery, action: write, allow: true}, + {user: test.UserGitOps, object: globalQueryNoTargets, action: run, allow: false}, + {user: test.UserGitOps, object: globalQueryTargetedToTeam1, action: run, allow: false}, + {user: test.UserGitOps, object: globalQuery, action: runNew, allow: false}, + {user: test.UserGitOps, object: globalObserverQuery, action: read, allow: false}, + {user: test.UserGitOps, object: globalObserverQuery, action: write, allow: true}, + {user: test.UserGitOps, object: globalObserverQueryEmptyTargets, action: run, allow: false}, + {user: test.UserGitOps, object: globalObserverQueryTargetedToTeam1, action: run, allow: false}, + {user: test.UserGitOps, object: globalObserverQueryTargetedToTeam1AndTeam2, action: run, allow: false}, + {user: test.UserGitOps, object: globalObserverQuery, action: runNew, allow: false}, + }, + }, + { + name: "Team observer can read and run observer_can_run only", + testCases: []authTestCase{ + {user: teamObserver, object: globalQuery, action: read, allow: true}, + {user: teamObserver, object: globalQuery, action: write, allow: false}, + {user: teamObserver, object: teamAdminQuery, action: write, allow: false}, + {user: teamObserver, object: globalQueryNoTargets, action: run, allow: false}, + {user: teamObserver, object: globalQueryTargetedToTeam1, action: run, allow: false}, + {user: teamObserver, object: globalQuery, action: runNew, allow: false}, + {user: teamObserver, object: globalObserverQuery, action: read, allow: true}, + {user: teamObserver, object: globalObserverQuery, action: write, allow: false}, + {user: teamObserver, object: globalObserverQueryEmptyTargets, action: run, allow: true}, // can run observer query with no targeted team + {user: teamObserver, object: globalObserverQueryTargetedToTeam1, action: run, allow: true}, // can run observer query filtered to observed team + {user: teamObserver, object: globalObserverQueryTargetedToTeam1AndTeam2, action: run, allow: false}, // not filtered only to observed teams + {user: teamObserver, object: globalObserverQueryTargetedToTeam2, action: run, allow: false}, // not filtered only to observed teams + {user: teamObserver, object: globalObserverQuery, action: runNew, allow: false}, - // Global admin can read/write (even not authored by them)/run any - {user: test.UserAdmin, object: query, action: read, allow: true}, - {user: test.UserAdmin, object: query, action: write, allow: true}, - {user: test.UserAdmin, object: teamMaintQuery, action: write, allow: true}, - {user: test.UserAdmin, object: globalAdminQuery, action: write, allow: true}, - {user: test.UserAdmin, object: emptyTquery, action: run, allow: true}, - {user: test.UserAdmin, object: team1Query, action: run, allow: true}, - {user: test.UserAdmin, object: query, action: runNew, allow: true}, - {user: test.UserAdmin, object: observerQuery, action: read, allow: true}, - {user: test.UserAdmin, object: observerQuery, action: write, allow: true}, - {user: test.UserAdmin, object: emptyTobsQuery, action: run, allow: true}, - {user: test.UserAdmin, object: team1ObsQuery, action: run, allow: true}, - {user: test.UserAdmin, object: observerQuery, action: runNew, allow: true}, + {user: teamObserver, object: observerQueryOnTeam3, action: read, allow: false}, + {user: teamObserver, object: observerQueryOnTeam3, action: write, allow: false}, + {user: teamObserver, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: false}, + {user: teamObserver, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: false}, + {user: teamObserver, object: observerQueryOnTeam3TargetedToTeam1, action: run, allow: false}, + {user: teamObserver, object: observerQueryOnTeam1TargetedToTeam1, action: run, allow: true}, + }, + }, + { + name: "Team observer+ can read all queries, not write them, and can run any query", + testCases: []authTestCase{ + {user: teamObserverPlus, object: globalQuery, action: read, allow: true}, + {user: teamObserverPlus, object: globalQuery, action: write, allow: false}, + {user: teamObserverPlus, object: teamAdminQuery, action: write, allow: false}, + {user: teamObserverPlus, object: globalQueryNoTargets, action: run, allow: true}, + {user: teamObserverPlus, object: globalQueryTargetedToTeam1, action: run, allow: true}, + {user: teamObserverPlus, object: globalQuery, action: runNew, allow: true}, + {user: teamObserverPlus, object: globalObserverQuery, action: read, allow: true}, + {user: teamObserverPlus, object: globalObserverQuery, action: write, allow: false}, + {user: teamObserverPlus, object: globalObserverQueryEmptyTargets, action: run, allow: true}, // can run observer query with no targeted team + {user: teamObserverPlus, object: globalObserverQueryTargetedToTeam1, action: run, allow: true}, // can run observer query filtered to observed team + {user: teamObserverPlus, object: globalObserverQueryTargetedToTeam1AndTeam2, action: run, allow: false}, // not filtered only to observed teams + {user: teamObserverPlus, object: globalObserverQueryTargetedToTeam2, action: run, allow: false}, // not filtered only to observed teams + {user: teamObserverPlus, object: globalObserverQuery, action: runNew, allow: true}, - // Global GitOps cannot read, or run any query, but can write. - {user: test.UserGitOps, object: query, action: read, allow: false}, - {user: test.UserGitOps, object: query, action: write, allow: true}, - {user: test.UserGitOps, object: teamAdminQuery, action: write, allow: true}, - {user: test.UserGitOps, object: emptyTquery, action: run, allow: false}, - {user: test.UserGitOps, object: team1Query, action: run, allow: false}, - {user: test.UserGitOps, object: query, action: runNew, allow: false}, - {user: test.UserGitOps, object: observerQuery, action: read, allow: false}, - {user: test.UserGitOps, object: observerQuery, action: write, allow: true}, - {user: test.UserGitOps, object: emptyTobsQuery, action: run, allow: false}, - {user: test.UserGitOps, object: team1ObsQuery, action: run, allow: false}, - {user: test.UserGitOps, object: team12ObsQuery, action: run, allow: false}, - {user: test.UserGitOps, object: observerQuery, action: runNew, allow: false}, + {user: teamObserverPlus, object: observerQueryOnTeam3, action: read, allow: false}, + {user: teamObserverPlus, object: observerQueryOnTeam3, action: write, allow: false}, + {user: teamObserverPlus, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: false}, + {user: teamObserverPlus, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: false}, + {user: teamObserverPlus, object: observerQueryOnTeam3TargetedToTeam1, action: run, allow: false}, + {user: teamObserverPlus, object: observerQueryOnTeam1TargetedToTeam1, action: run, allow: true}, + }, + }, + { + name: "Team maintainer can read/write/run queries filtered on their team(s)", + testCases: []authTestCase{ + {user: teamMaintainer, object: globalQuery, action: read, allow: true}, + {user: teamMaintainer, object: globalQuery, action: write, allow: false}, // query belongs to global domain. + {user: teamMaintainer, object: teamMaintQuery, action: write, allow: true}, + {user: teamMaintainer, object: teamAdminQuery, action: write, allow: true}, + {user: teamMaintainer, object: globalQueryNoTargets, action: run, allow: true}, + {user: teamMaintainer, object: globalQueryTargetedToTeam1, action: run, allow: true}, + {user: teamMaintainer, object: globalQueryTargetedToTeam1AndTeam2, action: run, allow: false}, + {user: teamMaintainer, object: globalQueryTargetedToTeam2, action: run, allow: false}, + {user: teamMaintainer, object: globalQuery, action: runNew, allow: true}, + {user: teamMaintainer, object: globalObserverQuery, action: read, allow: true}, + {user: teamMaintainer, object: globalObserverQuery, action: write, allow: false}, // query belongs to global domain. + {user: teamMaintainer, object: globalObserverQueryEmptyTargets, action: run, allow: true}, + {user: teamMaintainer, object: globalObserverQueryTargetedToTeam1, action: run, allow: true}, + {user: teamMaintainer, object: globalObserverQueryTargetedToTeam1AndTeam2, action: run, allow: false}, + {user: teamMaintainer, object: globalObserverQueryTargetedToTeam2, action: run, allow: false}, + {user: teamMaintainer, object: globalObserverQuery, action: runNew, allow: true}, - // Team observer can read and run observer_can_run only - {user: teamObserver, object: query, action: read, allow: true}, - {user: teamObserver, object: query, action: write, allow: false}, - {user: teamObserver, object: teamAdminQuery, action: write, allow: false}, - {user: teamObserver, object: emptyTquery, action: run, allow: false}, - {user: teamObserver, object: team1Query, action: run, allow: false}, - {user: teamObserver, object: query, action: runNew, allow: false}, - {user: teamObserver, object: observerQuery, action: read, allow: true}, - {user: teamObserver, object: observerQuery, action: write, allow: false}, - {user: teamObserver, object: emptyTobsQuery, action: run, allow: true}, // can run observer query with no targeted team - {user: teamObserver, object: team1ObsQuery, action: run, allow: true}, // can run observer query filtered to observed team - {user: teamObserver, object: team12ObsQuery, action: run, allow: false}, // not filtered only to observed teams - {user: teamObserver, object: team2ObsQuery, action: run, allow: false}, // not filtered only to observed teams - {user: teamObserver, object: observerQuery, action: runNew, allow: false}, + {user: teamMaintainer, object: observerQueryOnTeam3, action: read, allow: false}, + {user: teamMaintainer, object: observerQueryOnTeam3, action: write, allow: false}, + {user: teamMaintainer, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: false}, + {user: teamMaintainer, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: false}, + {user: teamMaintainer, object: observerQueryOnTeam3TargetedToTeam1, action: run, allow: false}, + {user: teamMaintainer, object: observerQueryOnTeam1TargetedToTeam1, action: run, allow: true}, + }, + }, + { + name: "Team admin can read/write their own queries/run queries filtered on their team(s)", + testCases: []authTestCase{ + {user: teamAdmin, object: globalQuery, action: read, allow: true}, + {user: teamAdmin, object: globalQuery, action: write, allow: false}, // query belongs to global domain. + {user: teamAdmin, object: teamAdminQuery, action: write, allow: true}, + {user: teamAdmin, object: teamMaintQuery, action: write, allow: true}, + {user: teamAdmin, object: globalAdminQuery, action: write, allow: false}, // query belongs to global domain. + {user: teamAdmin, object: globalQueryNoTargets, action: run, allow: true}, + {user: teamAdmin, object: globalQueryTargetedToTeam1, action: run, allow: true}, + {user: teamAdmin, object: globalQueryTargetedToTeam1AndTeam2, action: run, allow: false}, + {user: teamAdmin, object: globalQueryTargetedToTeam2, action: run, allow: false}, + {user: teamAdmin, object: globalQuery, action: runNew, allow: true}, + {user: teamAdmin, object: globalObserverQuery, action: read, allow: true}, + {user: teamAdmin, object: globalObserverQuery, action: write, allow: false}, // observerQuery belongs to global domain. + {user: teamAdmin, object: globalObserverQueryEmptyTargets, action: run, allow: true}, + {user: teamAdmin, object: globalObserverQueryTargetedToTeam1, action: run, allow: true}, + {user: teamAdmin, object: globalObserverQueryTargetedToTeam1AndTeam2, action: run, allow: false}, + {user: teamAdmin, object: globalObserverQueryTargetedToTeam2, action: run, allow: false}, + {user: teamAdmin, object: globalObserverQuery, action: runNew, allow: true}, - // Team observer+ can read all queries, not write them, and can run any query. - {user: teamObserverPlus, object: query, action: read, allow: true}, - {user: teamObserverPlus, object: query, action: write, allow: false}, - {user: teamObserverPlus, object: teamAdminQuery, action: write, allow: false}, - {user: teamObserverPlus, object: emptyTquery, action: run, allow: true}, - {user: teamObserverPlus, object: team1Query, action: run, allow: true}, - {user: teamObserverPlus, object: query, action: runNew, allow: true}, - {user: teamObserverPlus, object: observerQuery, action: read, allow: true}, - {user: teamObserverPlus, object: observerQuery, action: write, allow: false}, - {user: teamObserverPlus, object: emptyTobsQuery, action: run, allow: true}, // can run observer query with no targeted team - {user: teamObserverPlus, object: team1ObsQuery, action: run, allow: true}, // can run observer query filtered to observed team - {user: teamObserverPlus, object: team12ObsQuery, action: run, allow: false}, // not filtered only to observed teams - {user: teamObserverPlus, object: team2ObsQuery, action: run, allow: false}, // not filtered only to observed teams - {user: teamObserverPlus, object: observerQuery, action: runNew, allow: true}, + {user: teamAdmin, object: observerQueryOnTeam3, action: read, allow: false}, + {user: teamAdmin, object: observerQueryOnTeam3, action: write, allow: false}, + {user: teamAdmin, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: false}, + {user: teamAdmin, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: false}, + {user: teamAdmin, object: observerQueryOnTeam3TargetedToTeam1, action: run, allow: false}, + {user: teamAdmin, object: observerQueryOnTeam1TargetedToTeam1, action: run, allow: true}, + }, + }, + { + name: "Team GitOps cannot read or run any query, but can create new or edit (write) queries authored by it.", + testCases: []authTestCase{ + {user: teamGitOps, object: globalQuery, action: read, allow: false}, + {user: teamGitOps, object: globalQuery, action: write, allow: false}, // cannot create a global query + {user: teamGitOps, object: teamAdminQuery, action: write, allow: true}, + {user: teamGitOps, object: teamGitOpsQuery, action: write, allow: true}, + {user: teamGitOps, object: globalGitOpsQuery, action: write, allow: false}, // cannot write a global query + {user: teamGitOps, object: globalQueryNoTargets, action: run, allow: false}, + {user: teamGitOps, object: globalQueryTargetedToTeam1, action: run, allow: false}, + {user: teamGitOps, object: globalQuery, action: runNew, allow: false}, + {user: teamGitOps, object: globalObserverQueryEmptyTargets, action: run, allow: false}, + {user: teamGitOps, object: globalObserverQueryTargetedToTeam1, action: run, allow: false}, + {user: teamGitOps, object: globalObserverQueryTargetedToTeam1AndTeam2, action: run, allow: false}, + {user: teamGitOps, object: globalObserverQueryTargetedToTeam2, action: run, allow: false}, + {user: teamGitOps, object: globalObserverQuery, action: runNew, allow: false}, - // Team maintainer can read/write their own queries/run queries filtered on their team(s) - {user: teamMaintainer, object: query, action: read, allow: true}, - {user: teamMaintainer, object: query, action: write, allow: true}, - {user: teamMaintainer, object: teamMaintQuery, action: write, allow: true}, - {user: teamMaintainer, object: teamAdminQuery, action: write, allow: false}, - {user: teamMaintainer, object: emptyTquery, action: run, allow: true}, - {user: teamMaintainer, object: team1Query, action: run, allow: true}, - {user: teamMaintainer, object: team12Query, action: run, allow: false}, - {user: teamMaintainer, object: team2Query, action: run, allow: false}, - {user: teamMaintainer, object: query, action: runNew, allow: true}, - {user: teamMaintainer, object: observerQuery, action: read, allow: true}, - {user: teamMaintainer, object: observerQuery, action: write, allow: true}, - {user: teamMaintainer, object: emptyTobsQuery, action: run, allow: true}, - {user: teamMaintainer, object: team1ObsQuery, action: run, allow: true}, - {user: teamMaintainer, object: team12ObsQuery, action: run, allow: false}, - {user: teamMaintainer, object: team2ObsQuery, action: run, allow: false}, - {user: teamMaintainer, object: observerQuery, action: runNew, allow: true}, + {user: teamGitOps, object: observerQueryOnTeam3, action: read, allow: false}, + {user: teamGitOps, object: observerQueryOnTeam3, action: write, allow: false}, + {user: teamGitOps, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: false}, + {user: teamGitOps, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: false}, + {user: teamGitOps, object: observerQueryOnTeam3TargetedToTeam1, action: run, allow: false}, + {user: teamGitOps, object: observerQueryOnTeam1TargetedToTeam1, action: run, allow: false}, + }, + }, + { + name: "User admin on team 1, observer on team 2", + testCases: []authTestCase{ + {user: twoTeamsAdminObs, object: globalQuery, action: read, allow: true}, + {user: twoTeamsAdminObs, object: globalQuery, action: write, allow: false}, // cannot write a global query + {user: twoTeamsAdminObs, object: teamAdminQuery, action: write, allow: true}, + {user: twoTeamsAdminObs, object: teamMaintQuery, action: write, allow: true}, + {user: twoTeamsAdminObs, object: globalAdminQuery, action: write, allow: false}, // cannot write a global query + {user: twoTeamsAdminObs, object: globalQueryNoTargets, action: run, allow: true}, + {user: twoTeamsAdminObs, object: globalQueryTargetedToTeam1, action: run, allow: true}, + {user: twoTeamsAdminObs, object: globalQueryTargetedToTeam1AndTeam2, action: run, allow: false}, // user is only observer on team 2 + {user: twoTeamsAdminObs, object: globalQueryTargetedToTeam2, action: run, allow: false}, + {user: twoTeamsAdminObs, object: globalQueryTargetedToTeam1AndTeam2AndTeam3, action: run, allow: false}, + {user: twoTeamsAdminObs, object: globalQuery, action: runNew, allow: true}, + {user: twoTeamsAdminObs, object: globalObserverQuery, action: read, allow: true}, + {user: twoTeamsAdminObs, object: globalObserverQuery, action: write, allow: false}, // cannot write a global query + {user: twoTeamsAdminObs, object: globalObserverQueryEmptyTargets, action: run, allow: true}, + {user: twoTeamsAdminObs, object: globalObserverQueryTargetedToTeam1, action: run, allow: true}, + {user: twoTeamsAdminObs, object: globalObserverQueryTargetedToTeam1AndTeam2, action: run, allow: true}, // user is at least observer on both teams + {user: twoTeamsAdminObs, object: globalObserverQueryTargetedToTeam2, action: run, allow: true}, + {user: twoTeamsAdminObs, object: globalObserverQueryTargetedToTeam1AndTeam2AndTeam3, action: run, allow: false}, // not member of team 3 + {user: twoTeamsAdminObs, object: globalObserverQuery, action: runNew, allow: true}, - // Team admin can read/write their own queries/run queries filtered on their team(s) - {user: teamAdmin, object: query, action: read, allow: true}, - {user: teamAdmin, object: query, action: write, allow: true}, - {user: teamAdmin, object: teamAdminQuery, action: write, allow: true}, - {user: teamAdmin, object: teamMaintQuery, action: write, allow: false}, - {user: teamAdmin, object: globalAdminQuery, action: write, allow: false}, - {user: teamAdmin, object: emptyTquery, action: run, allow: true}, - {user: teamAdmin, object: team1Query, action: run, allow: true}, - {user: teamAdmin, object: team12Query, action: run, allow: false}, - {user: teamAdmin, object: team2Query, action: run, allow: false}, - {user: teamAdmin, object: query, action: runNew, allow: true}, - {user: teamAdmin, object: observerQuery, action: read, allow: true}, - {user: teamAdmin, object: observerQuery, action: write, allow: true}, - {user: teamAdmin, object: emptyTobsQuery, action: run, allow: true}, - {user: teamAdmin, object: team1ObsQuery, action: run, allow: true}, - {user: teamAdmin, object: team12ObsQuery, action: run, allow: false}, - {user: teamAdmin, object: team2ObsQuery, action: run, allow: false}, - {user: teamAdmin, object: observerQuery, action: runNew, allow: true}, - - // Team GitOps cannot read or run any query, but can create new or edit (write) queries authored by it. - {user: teamGitOps, object: query, action: read, allow: false}, - {user: teamGitOps, object: query, action: write, allow: true}, // create new - {user: teamGitOps, object: teamAdminQuery, action: write, allow: false}, // not the author - {user: teamGitOps, object: teamGitOpsQuery, action: write, allow: true}, // author - {user: teamGitOps, object: globalGitOpsQuery, action: write, allow: false}, // not the author - {user: teamGitOps, object: emptyTquery, action: run, allow: false}, - {user: teamGitOps, object: team1Query, action: run, allow: false}, - {user: teamGitOps, object: query, action: runNew, allow: false}, - {user: teamGitOps, object: emptyTobsQuery, action: run, allow: false}, - {user: teamGitOps, object: team1ObsQuery, action: run, allow: false}, - {user: teamGitOps, object: team12ObsQuery, action: run, allow: false}, - {user: teamGitOps, object: team2ObsQuery, action: run, allow: false}, - {user: teamGitOps, object: observerQuery, action: runNew, allow: false}, - - // User admin on team 1, observer on team 2 - {user: twoTeamsAdminObs, object: query, action: read, allow: true}, - {user: twoTeamsAdminObs, object: query, action: write, allow: true}, - {user: twoTeamsAdminObs, object: teamAdminQuery, action: write, allow: false}, - {user: twoTeamsAdminObs, object: teamMaintQuery, action: write, allow: false}, - {user: twoTeamsAdminObs, object: globalAdminQuery, action: write, allow: false}, - {user: twoTeamsAdminObs, object: emptyTquery, action: run, allow: true}, - {user: twoTeamsAdminObs, object: team1Query, action: run, allow: true}, - {user: twoTeamsAdminObs, object: team12Query, action: run, allow: false}, // user is only observer on team 2 - {user: twoTeamsAdminObs, object: team2Query, action: run, allow: false}, - {user: twoTeamsAdminObs, object: team123Query, action: run, allow: false}, - {user: twoTeamsAdminObs, object: query, action: runNew, allow: true}, - {user: twoTeamsAdminObs, object: observerQuery, action: read, allow: true}, - {user: twoTeamsAdminObs, object: observerQuery, action: write, allow: true}, - {user: twoTeamsAdminObs, object: emptyTobsQuery, action: run, allow: true}, - {user: twoTeamsAdminObs, object: team1ObsQuery, action: run, allow: true}, - {user: twoTeamsAdminObs, object: team12ObsQuery, action: run, allow: true}, // user is at least observer on both teams - {user: twoTeamsAdminObs, object: team2ObsQuery, action: run, allow: true}, - {user: twoTeamsAdminObs, object: team123ObsQuery, action: run, allow: false}, // not member of team 3 - {user: twoTeamsAdminObs, object: observerQuery, action: runNew, allow: true}, + {user: twoTeamsAdminObs, object: observerQueryOnTeam3, action: read, allow: false}, + {user: twoTeamsAdminObs, object: observerQueryOnTeam3, action: write, allow: false}, + {user: twoTeamsAdminObs, object: observerQueryOnTeam3TargetedToTeam3, action: run, allow: false}, + {user: twoTeamsAdminObs, object: observerQueryOnTeam3TargetedToTeam2, action: run, allow: false}, + {user: twoTeamsAdminObs, object: observerQueryOnTeam3TargetedToTeam1, action: run, allow: false}, + {user: twoTeamsAdminObs, object: observerQueryOnTeam1TargetedToTeam1, action: run, allow: true}, + {user: twoTeamsAdminObs, object: observerQueryOnTeam1TargetedToTeam2, action: run, allow: true}, + }, + }, }) } @@ -973,109 +1153,6 @@ func TestAuthorizeUserCreatedPack(t *testing.T) { }) } -func TestAuthorizeGlobalPack(t *testing.T) { - t.Parallel() - - globalPack := &fleet.Pack{ - // Type "global" is the type for the one global pack. - Type: ptr.String("global"), - } - runTestCases(t, []authTestCase{ - {user: nil, object: globalPack, action: read, allow: false}, - {user: nil, object: globalPack, action: write, allow: false}, - - {user: test.UserNoRoles, object: globalPack, action: read, allow: false}, - {user: test.UserNoRoles, object: globalPack, action: write, allow: false}, - - {user: test.UserAdmin, object: globalPack, action: read, allow: true}, - {user: test.UserAdmin, object: globalPack, action: write, allow: true}, - - {user: test.UserMaintainer, object: globalPack, action: read, allow: true}, - {user: test.UserMaintainer, object: globalPack, action: write, allow: true}, - - {user: test.UserObserver, object: globalPack, action: read, allow: true}, - {user: test.UserObserver, object: globalPack, action: write, allow: false}, - - {user: test.UserObserverPlus, object: globalPack, action: read, allow: true}, - {user: test.UserObserverPlus, object: globalPack, action: write, allow: false}, - - // This is one exception to the "write only" nature of gitops. To be able to create - // and edit packs currently it needs read access too. - {user: test.UserGitOps, object: globalPack, action: read, allow: true}, - {user: test.UserGitOps, object: globalPack, action: write, allow: true}, - - {user: test.UserTeamAdminTeam1, object: globalPack, action: read, allow: true}, - {user: test.UserTeamAdminTeam1, object: globalPack, action: write, allow: false}, - - {user: test.UserTeamMaintainerTeam1, object: globalPack, action: read, allow: true}, - {user: test.UserTeamMaintainerTeam1, object: globalPack, action: write, allow: false}, - - {user: test.UserTeamObserverTeam1, object: globalPack, action: read, allow: true}, - {user: test.UserTeamObserverTeam1, object: globalPack, action: write, allow: false}, - - {user: test.UserTeamObserverPlusTeam1, object: globalPack, action: read, allow: true}, - {user: test.UserTeamObserverPlusTeam1, object: globalPack, action: write, allow: false}, - - {user: test.UserTeamGitOpsTeam1, object: globalPack, action: read, allow: false}, - {user: test.UserTeamGitOpsTeam1, object: globalPack, action: write, allow: false}, - }) -} - -func TestAuthorizeTeamPack(t *testing.T) { - t.Parallel() - - team1Pack := &fleet.Pack{Type: ptr.String("team-1")} - team2Pack := &fleet.Pack{Type: ptr.String("team-2")} - runTestCases(t, []authTestCase{ - {user: test.UserAdmin, object: team1Pack, action: read, allow: true}, - {user: test.UserAdmin, object: team1Pack, action: write, allow: true}, - - {user: test.UserMaintainer, object: team1Pack, action: read, allow: true}, - {user: test.UserMaintainer, object: team1Pack, action: write, allow: true}, - - {user: test.UserObserver, object: team1Pack, action: read, allow: false}, - {user: test.UserObserver, object: team1Pack, action: write, allow: false}, - - {user: test.UserObserverPlus, object: team1Pack, action: read, allow: false}, - {user: test.UserObserverPlus, object: team1Pack, action: write, allow: false}, - - // This is one exception to the "write only" nature of gitops. To be able to create - // and edit packs currently it needs read access too. - {user: test.UserGitOps, object: team1Pack, action: read, allow: true}, - {user: test.UserGitOps, object: team1Pack, action: write, allow: true}, - - {user: test.UserTeamAdminTeam1, object: team1Pack, action: read, allow: true}, - {user: test.UserTeamAdminTeam1, object: team1Pack, action: write, allow: true}, - - {user: test.UserTeamMaintainerTeam1, object: team1Pack, action: read, allow: true}, - {user: test.UserTeamMaintainerTeam1, object: team1Pack, action: read, allow: true}, - - {user: test.UserTeamObserverTeam1, object: team1Pack, action: read, allow: true}, - {user: test.UserTeamObserverTeam1, object: team1Pack, action: write, allow: false}, - - {user: test.UserTeamObserverPlusTeam1, object: team1Pack, action: read, allow: true}, - {user: test.UserTeamObserverPlusTeam1, object: team1Pack, action: write, allow: false}, - - {user: test.UserTeamGitOpsTeam1, object: team1Pack, action: read, allow: false}, - {user: test.UserTeamGitOpsTeam1, object: team1Pack, action: write, allow: true}, - - {user: test.UserTeamAdminTeam1, object: team2Pack, action: read, allow: false}, - {user: test.UserTeamAdminTeam1, object: team2Pack, action: write, allow: false}, - - {user: test.UserTeamMaintainerTeam1, object: team2Pack, action: read, allow: false}, - {user: test.UserTeamMaintainerTeam1, object: team2Pack, action: write, allow: false}, - - {user: test.UserTeamObserverTeam1, object: team2Pack, action: read, allow: false}, - {user: test.UserTeamObserverTeam1, object: team2Pack, action: write, allow: false}, - - {user: test.UserTeamObserverPlusTeam1, object: team2Pack, action: read, allow: false}, - {user: test.UserTeamObserverPlusTeam1, object: team2Pack, action: write, allow: false}, - - {user: test.UserTeamGitOpsTeam1, object: team2Pack, action: read, allow: false}, - {user: test.UserTeamGitOpsTeam1, object: team2Pack, action: write, allow: false}, - }) -} - func TestAuthorizeCarve(t *testing.T) { t.Parallel() @@ -1584,45 +1661,64 @@ func assertUnauthorized(t *testing.T, user *fleet.User, object, action interface assert.Error(t, auth.Authorize(test.UserContext(context.Background(), user), object, action), "should be unauthorized\n%s", string(b)) } +type tcGroup struct { + name string + testCases []authTestCase +} + func runTestCases(t *testing.T, testCases []authTestCase) { + runTestCasesGroups(t, []tcGroup{ + { + name: "all", + testCases: testCases, + }, + }) +} + +func runTestCasesGroups(t *testing.T, testCaseGroups []tcGroup) { t.Helper() - for _, tt := range testCases { - tt := tt + for _, gg := range testCaseGroups { + gg := gg + t.Run(gg.name, func(t *testing.T) { + for _, tt := range gg.testCases { + tt := tt - // build a useful test name from user role, object, action and expected result - action := tt.action - role := "none" - if tt.user != nil { - if tt.user.GlobalRole != nil { - role = "g:" + *tt.user.GlobalRole - } else if len(tt.user.Teams) > 0 { - role = "" - for _, tm := range tt.user.Teams { - if role != "" { - role += "," + // build a useful test name from user role, object, action and expected result + action := tt.action + role := "none" + if tt.user != nil { + if tt.user.GlobalRole != nil { + role = "g:" + *tt.user.GlobalRole + } else if len(tt.user.Teams) > 0 { + role = "" + for _, tm := range tt.user.Teams { + if role != "" { + role += "," + } + role += tm.Role + } } - role += tm.Role } - } - } - obj := fmt.Sprintf("%T", tt.object) - if at, ok := tt.object.(AuthzTyper); ok { - obj = at.AuthzType() - } + obj := fmt.Sprintf("%T", tt.object) + if at, ok := tt.object.(AuthzTyper); ok { + obj = at.AuthzType() + } - result := "allow" - if !tt.allow { - result = "deny" - } + result := "allow" + if !tt.allow { + result = "deny" + } - t.Run(action+"_"+obj+"_"+role+"_"+result, func(t *testing.T) { - t.Parallel() - if tt.allow { - assertAuthorized(t, tt.user, tt.object, tt.action) - } else { - assertUnauthorized(t, tt.user, tt.object, tt.action) + t.Run(action+"_"+obj+"_"+role+"_"+result, func(t *testing.T) { + t.Parallel() + if tt.allow { + assertAuthorized(t, tt.user, tt.object, tt.action) + } else { + assertUnauthorized(t, tt.user, tt.object, tt.action) + } + }) } }) } diff --git a/server/datastore/mysql/aggregated_stats.go b/server/datastore/mysql/aggregated_stats.go index 2c81c9e78c..04622e070d 100644 --- a/server/datastore/mysql/aggregated_stats.go +++ b/server/datastore/mysql/aggregated_stats.go @@ -13,7 +13,6 @@ import ( type aggregatedStatsType string const ( - aggregatedStatsTypeQuery = "query" aggregatedStatsTypeScheduledQuery = "scheduled_query" aggregatedStatsTypeMunkiVersions = "munki_versions" aggregatedStatsTypeMunkiIssues = "munki_issues" @@ -48,38 +47,14 @@ FROM ( ) AS t2 WHERE t1.row_number_value = floor(total_rows * %s) + 1;` -const queryPercentileQuery = ` -SELECT - coalesce((t1.%s / t1.executions), 0) -FROM ( - SELECT @rownum := @rownum + 1 AS row_number_value, mm.* FROM ( - SELECT d.scheduled_query_id, d.%s, d.executions - FROM scheduled_query_stats d - JOIN scheduled_queries sq ON (sq.id=d.scheduled_query_id) - WHERE sq.query_id=? - ORDER BY (d.%s / d.executions) ASC - ) AS mm -) AS t1, -(SELECT @rownum := 0) AS r, -( - SELECT count(*) AS total_rows - FROM scheduled_query_stats d - JOIN scheduled_queries sq ON (sq.id=d.scheduled_query_id) - WHERE sq.query_id=? -) AS t2 -WHERE t1.row_number_value = floor(total_rows * %s) + 1;` - const ( scheduledQueryTotalExecutions = `SELECT coalesce(sum(executions), 0) FROM scheduled_query_stats WHERE scheduled_query_id=?` - queryTotalExecutions = `SELECT coalesce(sum(executions), 0) FROM scheduled_query_stats sqs JOIN scheduled_queries sq ON (sqs.scheduled_query_id=sq.id) JOIN queries q ON (q.id=sq.query_id) WHERE sq.query_id=?` ) func getPercentileQuery(aggregate aggregatedStatsType, time string, percentile string) string { switch aggregate { case aggregatedStatsTypeScheduledQuery: return fmt.Sprintf(scheduledQueryPercentileQuery, time, time, time, percentile) - case aggregatedStatsTypeQuery: - return fmt.Sprintf(queryPercentileQuery, time, time, time, percentile) } return "" } @@ -107,23 +82,12 @@ func setP50AndP95Map(ctx context.Context, tx sqlx.QueryerContext, aggregate aggr return nil } -func (ds *Datastore) UpdateScheduledQueryAggregatedStats(ctx context.Context) error { - err := walkIdsInTable(ctx, ds.reader(ctx), "scheduled_queries", func(id uint) error { +func (ds *Datastore) UpdateQueryAggregatedStats(ctx context.Context) error { + err := walkIdsInTable(ctx, ds.reader(ctx), "queries", func(id uint) error { return calculatePercentiles(ctx, ds.writer(ctx), aggregatedStatsTypeScheduledQuery, id) }) if err != nil { - return ctxerr.Wrap(ctx, err, "looping through ids") - } - - return nil -} - -func (ds *Datastore) UpdateQueryAggregatedStats(ctx context.Context) error { - err := walkIdsInTable(ctx, ds.reader(ctx), "queries", func(id uint) error { - return calculatePercentiles(ctx, ds.writer(ctx), aggregatedStatsTypeQuery, id) - }) - if err != nil { - return ctxerr.Wrap(ctx, err, "looping through ids") + return ctxerr.Wrap(ctx, err, "looping through query ids") } return nil @@ -174,8 +138,6 @@ func getTotalExecutionsQuery(aggregate aggregatedStatsType) string { switch aggregate { case aggregatedStatsTypeScheduledQuery: return scheduledQueryTotalExecutions - case aggregatedStatsTypeQuery: - return queryTotalExecutions } return "" } diff --git a/server/datastore/mysql/aggregated_stats_test.go b/server/datastore/mysql/aggregated_stats_test.go index 56ed113d2f..488652a282 100644 --- a/server/datastore/mysql/aggregated_stats_test.go +++ b/server/datastore/mysql/aggregated_stats_test.go @@ -14,14 +14,9 @@ import ( "github.com/stretchr/testify/require" ) -func slowStats(t *testing.T, ds *Datastore, id uint, percentile int, table, column string) float64 { - scheduledQueriesSQL := fmt.Sprintf(`SELECT d.%s / d.executions FROM scheduled_query_stats d WHERE d.scheduled_query_id=? ORDER BY (d.%s / d.executions) ASC`, column, column) - queriesSQL := fmt.Sprintf(`SELECT d.%s / d.executions FROM scheduled_query_stats d JOIN scheduled_queries sq ON (sq.id=d.scheduled_query_id) WHERE sq.query_id=? ORDER BY (d.%s / d.executions) ASC`, column, column) - queryToRun := scheduledQueriesSQL - if table == "queries" { - queryToRun = queriesSQL - } - rows, err := ds.writer(context.Background()).Queryx(queryToRun, id) +func slowStats(t *testing.T, ds *Datastore, id uint, percentile int, column string) float64 { + queriesSQL := fmt.Sprintf(`SELECT d.%s / d.executions FROM scheduled_query_stats d JOIN queries q ON (d.scheduled_query_id=q.id) WHERE q.id=? ORDER BY (d.%s / d.executions) ASC`, column, column) + rows, err := ds.writer(context.Background()).Queryx(queriesSQL, id) require.NoError(t, err) defer rows.Close() @@ -58,7 +53,7 @@ func TestAggregatedStats(t *testing.T) { require.NoError(t, err) } for i := 0; i < scheduledQueryCount; i++ { - _, err := ds.writer(context.Background()).Exec(`INSERT INTO scheduled_queries(query_id,name,query_name) VALUES (?,?,?)`, rand.Intn(queryCount)+1, fmt.Sprint(i), fmt.Sprint(i)) + _, err := ds.writer(context.Background()).Exec(`INSERT INTO scheduled_queries(query_id, name, query_name) VALUES (?,?,?)`, rand.Intn(queryCount)+1, fmt.Sprint(i), fmt.Sprint(i)) require.NoError(t, err) } insertScheduledQuerySQL := `INSERT IGNORE INTO scheduled_query_stats(host_id, scheduled_query_id, system_time, user_time, executions) VALUES %s` @@ -70,7 +65,7 @@ func TestAggregatedStats(t *testing.T) { require.NoError(t, err) args = []interface{}{} } - args = append(args, rand.Intn(hostCount)+1, rand.Intn(scheduledQueryCount)+1, rand.Intn(10000)+100, rand.Intn(10000)+100, rand.Intn(10000)+100) + args = append(args, rand.Intn(hostCount)+1, rand.Intn(queryCount)+1, rand.Intn(10000)+100, rand.Intn(10000)+100, rand.Intn(10000)+100) } if len(args) > 0 { values := strings.TrimSuffix(strings.Repeat("(?,?,?,?,?),", len(args)/5), ",") @@ -84,7 +79,7 @@ func TestAggregatedStats(t *testing.T) { require.NoError(t, err) } for i := scheduledQueryCount; i < scheduledQueryCount+4; i++ { - _, err := ds.writer(context.Background()).Exec(`INSERT INTO scheduled_queries(query_id,name,query_name) VALUES (?,?,?)`, rand.Intn(queryCount)+1, fmt.Sprint(i), fmt.Sprint(i)) + _, err := ds.writer(context.Background()).Exec(`INSERT INTO scheduled_queries(query_id, name, query_name) VALUES (?,?,?)`, rand.Intn(queryCount)+1, fmt.Sprint(i), fmt.Sprint(i)) require.NoError(t, err) } @@ -95,8 +90,7 @@ func TestAggregatedStats(t *testing.T) { aggregate aggregatedStatsType aggFunc func(ctx context.Context) error }{ - {"scheduled_queries", aggregatedStatsTypeScheduledQuery, ds.UpdateScheduledQueryAggregatedStats}, - {"queries", aggregatedStatsTypeQuery, ds.UpdateQueryAggregatedStats}, + {"queries", aggregatedStatsTypeScheduledQuery, ds.UpdateQueryAggregatedStats}, } for _, tt := range testcases { t.Run(tt.table, func(t *testing.T) { @@ -114,7 +108,7 @@ func TestAggregatedStats(t *testing.T) { ` select id, - global_stats, + global_stats, JSON_EXTRACT(json_value, '$.user_time_p50') as user_time_p50, JSON_EXTRACT(json_value, '$.user_time_p95') as user_time_p95, JSON_EXTRACT(json_value, '$.system_time_p50') as system_time_p50, @@ -125,10 +119,10 @@ from aggregated_stats where type=?`, tt.aggregate)) require.True(t, len(stats) > 0) for _, stat := range stats { require.False(t, stat.GlobalStats) - checkAgainstSlowStats(t, ds, stat.ID, 50, tt.table, "user_time", stat.UserTimeP50) - checkAgainstSlowStats(t, ds, stat.ID, 95, tt.table, "user_time", stat.UserTimeP95) - checkAgainstSlowStats(t, ds, stat.ID, 50, tt.table, "system_time", stat.SystemTimeP50) - checkAgainstSlowStats(t, ds, stat.ID, 95, tt.table, "system_time", stat.SystemTimeP95) + checkAgainstSlowStats(t, ds, stat.ID, 50, "user_time", stat.UserTimeP50) + checkAgainstSlowStats(t, ds, stat.ID, 95, "user_time", stat.UserTimeP95) + checkAgainstSlowStats(t, ds, stat.ID, 50, "system_time", stat.SystemTimeP50) + checkAgainstSlowStats(t, ds, stat.ID, 95, "system_time", stat.SystemTimeP95) require.NotNil(t, stat.TotalExecutions) assert.True(t, *stat.TotalExecutions >= 0) } @@ -136,8 +130,8 @@ from aggregated_stats where type=?`, tt.aggregate)) } } -func checkAgainstSlowStats(t *testing.T, ds *Datastore, id uint, percentile int, table, column string, against *float64) { - slowp := slowStats(t, ds, id, percentile, table, column) +func checkAgainstSlowStats(t *testing.T, ds *Datastore, id uint, percentile int, column string, against *float64) { + slowp := slowStats(t, ds, id, percentile, column) if against != nil { assert.Equal(t, slowp, *against) } else { diff --git a/server/datastore/mysql/campaigns_test.go b/server/datastore/mysql/campaigns_test.go index 87d9316b6a..863fd8531c 100644 --- a/server/datastore/mysql/campaigns_test.go +++ b/server/datastore/mysql/campaigns_test.go @@ -35,7 +35,7 @@ func TestCampaigns(t *testing.T) { func testCampaignsDistributedQuery(t *testing.T, ds *Datastore) { user := test.NewUser(t, ds, "Zach", "zwass@fleet.co", true) mockClock := clock.NewMockClock() - query := test.NewQuery(t, ds, "test", "select * from time", user.ID, false) + query := test.NewQuery(t, ds, nil, "test", "select * from time", user.ID, false) campaign := test.NewCampaign(t, ds, query.ID, fleet.QueryRunning, mockClock.Now()) { @@ -84,7 +84,7 @@ func testCampaignsCleanupDistributedQuery(t *testing.T, ds *Datastore) { mockClock := clock.NewMockClock() - query := test.NewQuery(t, ds, "test", "select * from time", user.ID, false) + query := test.NewQuery(t, ds, nil, "test", "select * from time", user.ID, false) c1 := test.NewCampaign(t, ds, query.ID, fleet.QueryWaiting, mockClock.Now()) c2 := test.NewCampaign(t, ds, query.ID, fleet.QueryRunning, mockClock.Now()) @@ -158,7 +158,7 @@ func testCampaignsSaveDistributedQuery(t *testing.T, ds *Datastore) { mockClock := clock.NewMockClock() - query := test.NewQuery(t, ds, t.Name()+"test", "select * from time", user.ID, false) + query := test.NewQuery(t, ds, nil, t.Name()+"test", "select * from time", user.ID, false) c1 := test.NewCampaign(t, ds, query.ID, fleet.QueryWaiting, mockClock.Now()) gotC, err := ds.DistributedQueryCampaign(context.Background(), c1.ID) diff --git a/server/datastore/mysql/delete_test.go b/server/datastore/mysql/delete_test.go index 80fa6d37b1..b2b253ccac 100644 --- a/server/datastore/mysql/delete_test.go +++ b/server/datastore/mysql/delete_test.go @@ -58,7 +58,7 @@ func testDeleteEntity(t *testing.T, ds *Datastore) { func testDeleteEntityByName(t *testing.T, ds *Datastore) { defer TruncateTables(t, ds) - query1 := test.NewQuery(t, ds, t.Name()+"time", "select * from time", 0, true) + query1 := test.NewQuery(t, ds, nil, t.Name()+"time", "select * from time", 0, true) require.NoError(t, ds.deleteEntityByName(context.Background(), queriesTable, query1.Name)) @@ -70,9 +70,9 @@ func testDeleteEntityByName(t *testing.T, ds *Datastore) { func testDeleteEntities(t *testing.T, ds *Datastore) { defer TruncateTables(t, ds) - query1 := test.NewQuery(t, ds, t.Name()+"time1", "select * from time", 0, true) - query2 := test.NewQuery(t, ds, t.Name()+"time2", "select * from time", 0, true) - query3 := test.NewQuery(t, ds, t.Name()+"time3", "select * from time", 0, true) + query1 := test.NewQuery(t, ds, nil, t.Name()+"time1", "select * from time", 0, true) + query2 := test.NewQuery(t, ds, nil, t.Name()+"time2", "select * from time", 0, true) + query3 := test.NewQuery(t, ds, nil, t.Name()+"time3", "select * from time", 0, true) count, err := ds.deleteEntities(context.Background(), queriesTable, []uint{query1.ID, query2.ID}) require.NoError(t, err) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index a00b339a31..cd6c4ce41c 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -136,44 +136,78 @@ func (ds *Datastore) SerialUpdateHost(ctx context.Context, host *fleet.Host) err } } -func (ds *Datastore) SaveHostPackStats(ctx context.Context, hostID uint, stats []fleet.PackStats) error { - return saveHostPackStatsDB(ctx, ds.writer(ctx), hostID, stats) +func (ds *Datastore) SaveHostPackStats(ctx context.Context, teamID *uint, hostID uint, stats []fleet.PackStats) error { + return saveHostPackStatsDB(ctx, ds.writer(ctx), teamID, hostID, stats) } -func saveHostPackStatsDB(ctx context.Context, db sqlx.ExecerContext, hostID uint, stats []fleet.PackStats) error { +func saveHostPackStatsDB(ctx context.Context, db *sqlx.DB, teamID *uint, hostID uint, stats []fleet.PackStats) error { // NOTE: this implementation must be kept in sync with the async/batch version // in AsyncBatchSaveHostsScheduledQueryStats (in scheduled_queries.go) - that is, // the behaviour per host must be the same. - var args []interface{} - queryCount := 0 - for _, pack := range stats { - for _, query := range pack.QueryStats { - queryCount++ + var ( + userPacksArgs []interface{} + userPacksQueryCount = 0 + scheduledQueriesArgs []interface{} + scheduledQueriesQueryCount = 0 + ) - args = append(args, - query.PackName, - query.ScheduledQueryName, - hostID, - query.AverageMemory, - query.Denylisted, - query.Executions, - query.Interval, - query.LastExecuted, - query.OutputSize, - query.SystemTime, - query.UserTime, - query.WallTime, - ) + for _, pack := range stats { + if pack.PackName == "Global" || (teamID != nil && pack.PackName == fmt.Sprintf("team-%d", *teamID)) { + for _, query := range pack.QueryStats { + scheduledQueriesQueryCount++ + + teamIDArg := uint(0) + if pack.PackName != "Global" { + teamIDArg = *teamID + } + scheduledQueriesArgs = append(scheduledQueriesArgs, + teamIDArg, + query.QueryName, + + hostID, + query.AverageMemory, + query.Denylisted, + query.Executions, + query.Interval, + query.LastExecuted, + query.OutputSize, + query.SystemTime, + query.UserTime, + query.WallTime, + ) + } + } else { // User 2017 packs + for _, query := range pack.QueryStats { + userPacksQueryCount++ + + userPacksArgs = append(userPacksArgs, + query.PackName, + query.ScheduledQueryName, + + hostID, + query.AverageMemory, + query.Denylisted, + query.Executions, + query.Interval, + query.LastExecuted, + query.OutputSize, + query.SystemTime, + query.UserTime, + query.WallTime, + ) + } } } - if queryCount == 0 { + if userPacksQueryCount == 0 && scheduledQueriesQueryCount == 0 { return nil } - values := strings.TrimSuffix(strings.Repeat("((SELECT sq.id FROM scheduled_queries sq JOIN packs p ON (sq.pack_id = p.id) WHERE p.name = ? AND sq.name = ?),?,?,?,?,?,?,?,?,?,?),", queryCount), ",") - sql := fmt.Sprintf(` + if scheduledQueriesQueryCount > 0 { + // This query will import stats for queries (new format). + values := strings.TrimSuffix(strings.Repeat("((SELECT q.id FROM queries q WHERE COALESCE(q.team_id, 0) = ? AND q.name = ?),?,?,?,?,?,?,?,?,?,?),", scheduledQueriesQueryCount), ",") + sql := fmt.Sprintf(` INSERT IGNORE INTO scheduled_query_stats ( scheduled_query_id, host_id, @@ -200,9 +234,47 @@ func saveHostPackStatsDB(ctx context.Context, db sqlx.ExecerContext, hostID uint user_time = VALUES(user_time), wall_time = VALUES(wall_time) `, values) - if _, err := db.ExecContext(ctx, sql, args...); err != nil { - return ctxerr.Wrap(ctx, err, "insert pack stats") + if _, err := db.ExecContext(ctx, sql, scheduledQueriesArgs...); err != nil { + return ctxerr.Wrap(ctx, err, "insert query schedule stats") + } } + + if userPacksQueryCount > 0 { + // This query will import stats for 2017 packs. + // NOTE(lucas): If more than one scheduled query reference the same query then only one of the stats will be written. + values := strings.TrimSuffix(strings.Repeat("((SELECT sq.query_id FROM scheduled_queries sq JOIN packs p ON (sq.pack_id = p.id) WHERE p.pack_type IS NULL AND p.name = ? AND sq.name = ?),?,?,?,?,?,?,?,?,?,?),", userPacksQueryCount), ",") + sql := fmt.Sprintf(` + INSERT IGNORE INTO scheduled_query_stats ( + scheduled_query_id, + host_id, + average_memory, + denylisted, + executions, + schedule_interval, + last_executed, + output_size, + system_time, + user_time, + wall_time + ) + VALUES %s ON DUPLICATE KEY UPDATE + scheduled_query_id = VALUES(scheduled_query_id), + host_id = VALUES(host_id), + average_memory = VALUES(average_memory), + denylisted = VALUES(denylisted), + executions = VALUES(executions), + schedule_interval = VALUES(schedule_interval), + last_executed = VALUES(last_executed), + output_size = VALUES(output_size), + system_time = VALUES(system_time), + user_time = VALUES(user_time), + wall_time = VALUES(wall_time) + `, values) + if _, err := db.ExecContext(ctx, sql, userPacksArgs...); err != nil { + return ctxerr.Wrap(ctx, err, "insert pack stats") + } + } + return nil } @@ -214,7 +286,7 @@ func saveHostPackStatsDB(ctx context.Context, db sqlx.ExecerContext, hostID uint // ScheduledQueryStats.LastExecuted. var pastDate = "2000-01-01T00:00:00Z" -// loadhostPacksStatsDB will load all the pack stats for the given host. The scheduled +// loadhostPacksStatsDB will load all the "2017 pack" stats for the given host. The scheduled // queries that haven't run yet are returned with zero values. func loadHostPackStatsDB(ctx context.Context, db sqlx.QueryerContext, hid uint, hostPlatform string) ([]fleet.PackStats, error) { packs, err := listPacksForHost(ctx, db, hid) @@ -254,12 +326,12 @@ func loadHostPackStatsDB(ctx context.Context, db sqlx.QueryerContext, hid uint, goqu.On(goqu.I("sq.pack_id").Eq(goqu.I("p.id"))), ).Join( goqu.I("queries").As("q"), - goqu.On(goqu.I("sq.query_name").Eq(goqu.I("q.name"))), + goqu.On(goqu.I("sq.query_id").Eq(goqu.I("q.id"))), ).LeftJoin( dialect.From("scheduled_query_stats").As("sqs").Where( goqu.I("host_id").Eq(hid), ), - goqu.On(goqu.I("sqs.scheduled_query_id").Eq(goqu.I("sq.id"))), + goqu.On(goqu.I("sqs.scheduled_query_id").Eq(goqu.I("sq.query_id"))), ).Where( goqu.Or( // sq.platform empty or NULL means the scheduled query is set to @@ -295,6 +367,60 @@ func loadHostPackStatsDB(ctx context.Context, db sqlx.QueryerContext, hid uint, return ps, nil } +func loadHostScheduledQueryStatsDB(ctx context.Context, db sqlx.QueryerContext, hid uint, hostPlatform string, teamID *uint) ([]fleet.QueryStats, error) { + var teamID_ uint + if teamID != nil { + teamID_ = *teamID + } + ds := dialect.From(goqu.I("queries").As("q")).Select( + goqu.I("q.id"), + goqu.I("q.name"), + goqu.I("q.description"), + goqu.I("q.team_id"), + goqu.I("q.schedule_interval").As("schedule_interval"), + goqu.COALESCE(goqu.I("sqs.average_memory"), 0).As("average_memory"), + goqu.COALESCE(goqu.I("sqs.denylisted"), false).As("denylisted"), + goqu.COALESCE(goqu.I("sqs.executions"), 0).As("executions"), + goqu.COALESCE(goqu.I("sqs.last_executed"), goqu.L("timestamp(?)", pastDate)).As("last_executed"), + goqu.COALESCE(goqu.I("sqs.output_size"), 0).As("output_size"), + goqu.COALESCE(goqu.I("sqs.system_time"), 0).As("system_time"), + goqu.COALESCE(goqu.I("sqs.user_time"), 0).As("user_time"), + goqu.COALESCE(goqu.I("sqs.wall_time"), 0).As("wall_time"), + ).LeftJoin( + dialect.From("scheduled_query_stats").As("sqs").Where( + goqu.I("host_id").Eq(hid), + ), + goqu.On(goqu.I("sqs.scheduled_query_id").Eq(goqu.I("q.id"))), + ).Where( + goqu.And( + goqu.Or( + // sq.platform empty or NULL means the scheduled query is set to + // run on all hosts. + goqu.I("q.platform").Eq(""), + goqu.I("q.platform").IsNull(), + // scheduled_queries.platform can be a comma-separated list of + // platforms, e.g. "darwin,windows". + goqu.L("FIND_IN_SET(?, q.platform)", fleet.PlatformFromHost(hostPlatform)).Neq(0), + ), + goqu.I("q.schedule_interval").Gt(0), + goqu.I("q.automations_enabled").IsTrue(), + goqu.Or( + goqu.I("q.team_id").IsNull(), + goqu.I("q.team_id").Eq(teamID_), + ), + ), + ) + sql, args, err := ds.ToSQL() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "sql build") + } + var stats []fleet.QueryStats + if err := sqlx.SelectContext(ctx, db, &stats, sql, args...); err != nil { + return nil, ctxerr.Wrap(ctx, err, "load query stats") + } + return stats, nil +} + func getPackTypeFromDBField(t *string) string { if t == nil { return "pack" @@ -497,6 +623,39 @@ LIMIT return nil, err } host.PackStats = packStats + queriesStats, err := loadHostScheduledQueryStatsDB(ctx, ds.reader(ctx), host.ID, host.Platform, host.TeamID) + if err != nil { + return nil, err + } + var ( + globalQueriesStats []fleet.QueryStats + hostTeamQueriesStats []fleet.QueryStats + ) + for _, queryStats := range queriesStats { + if queryStats.TeamID == nil { + globalQueriesStats = append(globalQueriesStats, queryStats) + } else { + hostTeamQueriesStats = append(hostTeamQueriesStats, queryStats) + } + } + if len(globalQueriesStats) > 0 { + host.PackStats = append(host.PackStats, fleet.PackStats{ + PackName: "Global", + Type: "global", + QueryStats: queryStatsToScheduledQueryStats(globalQueriesStats, "Global"), + }) + } + if host.TeamID != nil && len(hostTeamQueriesStats) > 0 { + team, err := ds.Team(ctx, *host.TeamID) + if err != nil { + return nil, err + } + host.PackStats = append(host.PackStats, fleet.PackStats{ + PackName: "Team: " + team.Name, + Type: fmt.Sprintf("team-%d", team.ID), + QueryStats: queryStatsToScheduledQueryStats(hostTeamQueriesStats, "Team: "+team.Name), + }) + } users, err := loadHostUsersDB(ctx, ds.reader(ctx), host.ID) if err != nil { @@ -507,6 +666,29 @@ LIMIT return &host, nil } +func queryStatsToScheduledQueryStats(queriesStats []fleet.QueryStats, packName string) []fleet.ScheduledQueryStats { + scheduledQueriesStats := make([]fleet.ScheduledQueryStats, 0, len(queriesStats)) + for _, queryStats := range queriesStats { + scheduledQueriesStats = append(scheduledQueriesStats, fleet.ScheduledQueryStats{ + ScheduledQueryName: queryStats.Name, + ScheduledQueryID: queryStats.ID, + QueryName: queryStats.Name, + Description: queryStats.Description, + PackName: packName, + AverageMemory: queryStats.AverageMemory, + Denylisted: queryStats.Denylisted, + Executions: queryStats.Executions, + Interval: queryStats.Interval, + LastExecuted: queryStats.LastExecuted, + OutputSize: queryStats.OutputSize, + SystemTime: queryStats.SystemTime, + UserTime: queryStats.UserTime, + WallTime: queryStats.WallTime, + }) + } + return scheduledQueriesStats +} + // hostMDMSelect is the SQL fragment used to construct the JSON object // of MDM host data. It assumes that hostMDMJoin is included in the query. const hostMDMSelect = `, diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 04f326933a..ecba75d79e 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -295,24 +295,25 @@ func testSaveHostPackStatsDB(t *testing.T, ds *Datastore) { HostIDs: []uint{host.ID}, }) require.NoError(t, err) - query1 := test.NewQuery(t, ds, "time", "select * from time", 0, true) + query1 := test.NewQuery(t, ds, nil, "time", "select * from time", 0, true) squery1 := test.NewScheduledQuery(t, ds, pack1.ID, query1.ID, 30, true, true, "time-scheduled") stats1 := []fleet.ScheduledQueryStats{ { - ScheduledQueryName: squery1.Name, - ScheduledQueryID: squery1.ID, - QueryName: query1.Name, PackName: pack1.Name, - PackID: pack1.ID, - AverageMemory: 8000, - Denylisted: false, - Executions: 164, - Interval: 30, - LastExecuted: time.Unix(1620325191, 0).UTC(), - OutputSize: 1337, - SystemTime: 150, - UserTime: 180, - WallTime: 0, + ScheduledQueryName: squery1.Name, + + ScheduledQueryID: squery1.ID, + QueryName: query1.Name, + PackID: pack1.ID, + AverageMemory: 8000, + Denylisted: false, + Executions: 164, + Interval: 30, + LastExecuted: time.Unix(1620325191, 0).UTC(), + OutputSize: 1337, + SystemTime: 150, + UserTime: 180, + WallTime: 0, }, } @@ -322,40 +323,42 @@ func testSaveHostPackStatsDB(t *testing.T, ds *Datastore) { }) require.NoError(t, err) squery2 := test.NewScheduledQuery(t, ds, pack2.ID, query1.ID, 30, true, true, "time-scheduled") - query2 := test.NewQuery(t, ds, "processes", "select * from processes", 0, true) + query2 := test.NewQuery(t, ds, nil, "processes", "select * from processes", 0, true) squery3 := test.NewScheduledQuery(t, ds, pack2.ID, query2.ID, 30, true, true, "processes") stats2 := []fleet.ScheduledQueryStats{ { - ScheduledQueryName: squery2.Name, - ScheduledQueryID: squery2.ID, - QueryName: query1.Name, PackName: pack2.Name, - PackID: pack2.ID, - AverageMemory: 431, - Denylisted: true, - Executions: 1, - Interval: 30, - LastExecuted: time.Unix(980943843, 0).UTC(), - OutputSize: 134, - SystemTime: 1656, - UserTime: 18453, - WallTime: 10, + ScheduledQueryName: squery2.Name, + + ScheduledQueryID: squery2.ID, + QueryName: query1.Name, + PackID: pack2.ID, + AverageMemory: 431, + Denylisted: true, + Executions: 1, + Interval: 30, + LastExecuted: time.Unix(980943843, 0).UTC(), + OutputSize: 134, + SystemTime: 1656, + UserTime: 18453, + WallTime: 10, }, { ScheduledQueryName: squery3.Name, - ScheduledQueryID: squery3.ID, - QueryName: query2.Name, PackName: pack2.Name, - PackID: pack2.ID, - AverageMemory: 8000, - Denylisted: false, - Executions: 164, - Interval: 30, - LastExecuted: time.Unix(1620325191, 0).UTC(), - OutputSize: 1337, - SystemTime: 150, - UserTime: 180, - WallTime: 0, + + ScheduledQueryID: squery3.ID, + QueryName: query2.Name, + PackID: pack2.ID, + AverageMemory: 8000, + Denylisted: false, + Executions: 164, + Interval: 30, + LastExecuted: time.Unix(1620325191, 0).UTC(), + OutputSize: 1337, + SystemTime: 150, + UserTime: 180, + WallTime: 0, }, } @@ -373,7 +376,7 @@ func testSaveHostPackStatsDB(t *testing.T, ds *Datastore) { }, } - err = ds.SaveHostPackStats(context.Background(), host.ID, packStats) + err = ds.SaveHostPackStats(context.Background(), host.TeamID, host.ID, packStats) require.NoError(t, err) host, err = ds.Host(context.Background(), host.ID) @@ -383,12 +386,39 @@ func testSaveHostPackStatsDB(t *testing.T, ds *Datastore) { sort.Slice(host.PackStats, func(i, j int) bool { return host.PackStats[i].PackName < host.PackStats[j].PackName }) + assert.Equal(t, host.PackStats[0].PackName, "test1") - assert.ElementsMatch(t, host.PackStats[0].QueryStats, stats1) + // A new behavior is introduced with the new query model. If multiple scheduled queries + // with the same referenced query_id are executed in user packs, then only one of the results + // is gathered in Fleet. + assert.ElementsMatch(t, host.PackStats[0].QueryStats, []fleet.ScheduledQueryStats{ + { + PackName: pack1.Name, + ScheduledQueryName: squery1.Name, + + ScheduledQueryID: squery1.ID, + QueryName: query1.Name, + PackID: pack1.ID, + // + // These are the values for the same query1 in the second pack (it overrides the first schedule stats). + // + AverageMemory: 431, + Denylisted: true, + Executions: 1, + Interval: 30, + LastExecuted: time.Unix(980943843, 0).UTC(), + OutputSize: 134, + SystemTime: 1656, + UserTime: 18453, + WallTime: 10, + }, + }) assert.Equal(t, host.PackStats[1].PackName, "test2") assert.ElementsMatch(t, host.PackStats[1].QueryStats, stats2) } +// testHostsSavePackStatsOverwrites now behaves in a way that if two scheduled queries in a pack +// reference the same query_id, then their stat values are overriden. func testHostsSavePackStatsOverwrites(t *testing.T, ds *Datastore) { host, err := ds.NewHost(context.Background(), &fleet.Host{ DetailUpdatedAt: time.Now(), @@ -411,7 +441,7 @@ func testHostsSavePackStatsOverwrites(t *testing.T, ds *Datastore) { HostIDs: []uint{host.ID}, }) require.NoError(t, err) - query1 := test.NewQuery(t, ds, "time", "select * from time", 0, true) + query1 := test.NewQuery(t, ds, nil, "time", "select * from time", 0, true) squery1 := test.NewScheduledQuery(t, ds, pack1.ID, query1.ID, 30, true, true, "time-scheduled") pack2, err := ds.NewPack(context.Background(), &fleet.Pack{ Name: "test2", @@ -419,7 +449,7 @@ func testHostsSavePackStatsOverwrites(t *testing.T, ds *Datastore) { }) require.NoError(t, err) squery2 := test.NewScheduledQuery(t, ds, pack2.ID, query1.ID, 30, true, true, "time-scheduled") - query2 := test.NewQuery(t, ds, "processes", "select * from processes", 0, true) + query2 := test.NewQuery(t, ds, nil, "processes", "select * from processes", 0, true) execTime1 := time.Unix(1620325191, 0).UTC() @@ -468,7 +498,7 @@ func testHostsSavePackStatsOverwrites(t *testing.T, ds *Datastore) { }, } - err = ds.SaveHostPackStats(context.Background(), host.ID, packStats) + err = ds.SaveHostPackStats(context.Background(), host.TeamID, host.ID, packStats) require.NoError(t, err) host, err = ds.Host(context.Background(), host.ID) @@ -528,7 +558,7 @@ func testHostsSavePackStatsOverwrites(t *testing.T, ds *Datastore) { }, }, } - err = ds.SaveHostPackStats(context.Background(), host.ID, packStats) + err = ds.SaveHostPackStats(context.Background(), host.TeamID, host.ID, packStats) require.NoError(t, err) gotHost, err := ds.Host(context.Background(), host.ID) @@ -540,7 +570,7 @@ func testHostsSavePackStatsOverwrites(t *testing.T, ds *Datastore) { require.Len(t, gotHost.PackStats, 2) assert.Equal(t, gotHost.PackStats[0].PackName, "test1") - assert.Equal(t, execTime2, gotHost.PackStats[0].QueryStats[0].LastExecuted) + assert.Equal(t, execTime1, gotHost.PackStats[0].QueryStats[0].LastExecuted) } func testHostsWithTeamPackStats(t *testing.T, ds *Datastore) { @@ -564,10 +594,8 @@ func testHostsWithTeamPackStats(t *testing.T, ds *Datastore) { }) require.NoError(t, err) require.NoError(t, ds.AddHostsToTeam(context.Background(), &team.ID, []uint{host.ID})) - tp, err := ds.EnsureTeamPack(context.Background(), team.ID) - require.NoError(t, err) - tpQuery := test.NewQuery(t, ds, "tp-time", "select * from time", 0, true) - tpSquery := test.NewScheduledQuery(t, ds, tp.ID, tpQuery.ID, 30, true, true, "time-scheduled") + host.TeamID = &team.ID + tpQuery := test.NewQueryWithSchedule(t, ds, &team.ID, "tp-time", "select * from time", 0, true, 30, true) // Create a new pack and target to the host. // Pack and query must exist for stats to save successfully @@ -576,50 +604,50 @@ func testHostsWithTeamPackStats(t *testing.T, ds *Datastore) { HostIDs: []uint{host.ID}, }) require.NoError(t, err) - query1 := test.NewQuery(t, ds, "time", "select * from time", 0, true) + query1 := test.NewQuery(t, ds, nil, "time", "select * from time", 0, true) squery1 := test.NewScheduledQuery(t, ds, pack1.ID, query1.ID, 30, true, true, "time-scheduled") stats1 := []fleet.ScheduledQueryStats{ { - ScheduledQueryName: squery1.Name, - ScheduledQueryID: squery1.ID, - QueryName: query1.Name, PackName: pack1.Name, - PackID: pack1.ID, - AverageMemory: 8000, - Denylisted: false, - Executions: 164, - Interval: 30, - LastExecuted: time.Unix(1620325191, 0).UTC(), - OutputSize: 1337, - SystemTime: 150, - UserTime: 180, - WallTime: 0, + ScheduledQueryName: squery1.Name, + + QueryName: query1.Name, + PackID: pack1.ID, + AverageMemory: 8000, + Denylisted: false, + Executions: 164, + Interval: 30, + LastExecuted: time.Unix(1620325191, 0).UTC(), + OutputSize: 1337, + SystemTime: 150, + UserTime: 180, + WallTime: 0, }, } stats2 := []fleet.ScheduledQueryStats{ { - ScheduledQueryName: tpSquery.Name, - ScheduledQueryID: tpSquery.ID, - QueryName: tpQuery.Name, - PackName: tp.Name, - PackID: tp.ID, - AverageMemory: 8000, - Denylisted: false, - Executions: 164, - Interval: 30, - LastExecuted: time.Unix(1620325191, 0).UTC(), - OutputSize: 1337, - SystemTime: 150, - UserTime: 180, - WallTime: 0, + PackName: fmt.Sprintf("team-%d", team.ID), + ScheduledQueryName: tpQuery.Name, + + QueryName: tpQuery.Name, + PackID: 0, // pack_id will be 0 for stats of queries not in packs. + AverageMemory: 8000, + Denylisted: false, + Executions: 164, + Interval: 30, + LastExecuted: time.Unix(1620325191, 0).UTC(), + OutputSize: 1337, + SystemTime: 150, + UserTime: 180, + WallTime: 0, }, } packStats := []fleet.PackStats{ - {PackID: pack1.ID, PackName: pack1.Name, QueryStats: stats1}, - {PackID: tp.ID, PackName: teamScheduleName(team), QueryStats: stats2}, + {PackName: pack1.Name, QueryStats: stats1}, + {PackName: fmt.Sprintf("team-%d", team.ID), QueryStats: stats2}, } - err = ds.SaveHostPackStats(context.Background(), host.ID, packStats) + err = ds.SaveHostPackStats(context.Background(), host.TeamID, host.ID, packStats) require.NoError(t, err) host, err = ds.Host(context.Background(), host.ID) @@ -629,9 +657,11 @@ func testHostsWithTeamPackStats(t *testing.T, ds *Datastore) { sort.Sort(packStatsSlice(host.PackStats)) assert.Equal(t, host.PackStats[0].PackName, teamScheduleName(team)) + stats2[0].PackName = "Team: team1" + stats2[0].ScheduledQueryID = tpQuery.ID assert.ElementsMatch(t, host.PackStats[0].QueryStats, stats2) - assert.Equal(t, host.PackStats[1].PackName, pack1.Name) + stats1[0].ScheduledQueryID = squery1.ID assert.ElementsMatch(t, host.PackStats[1].QueryStats, stats1) } @@ -2586,7 +2616,7 @@ func testHostsListByPolicy(t *testing.T, ds *Datastore) { filter := fleet.TeamFilter{User: test.UserAdmin} - q := test.NewQuery(t, ds, "query1", "select 1", 0, true) + q := test.NewQuery(t, ds, nil, "query1", "select 1", 0, true) p, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{ QueryID: &q.ID, }) @@ -3040,8 +3070,8 @@ func testHostsListFailingPolicies(t *testing.T, ds *Datastore) { filter := fleet.TeamFilter{User: test.UserAdmin} - q := test.NewQuery(t, ds, "query1", "select 1", 0, true) - q2 := test.NewQuery(t, ds, "query2", "select 1", 0, true) + q := test.NewQuery(t, ds, nil, "query1", "select 1", 0, true) + q2 := test.NewQuery(t, ds, nil, "query2", "select 1", 0, true) p, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{ QueryID: &q.ID, }) @@ -3134,7 +3164,7 @@ func testHostsReadsLessRows(t *testing.T, ds *Datastore) { h1 := hosts[0] h2 := hosts[1] - q := test.NewQuery(t, ds, "query1", "select 1", 0, true) + q := test.NewQuery(t, ds, nil, "query1", "select 1", 0, true) p, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{ QueryID: &q.ID, }) @@ -3408,11 +3438,11 @@ func testHostsSavePackStatsConcurrent(t *testing.T, ds *Datastore) { require.NotNil(t, host2) pack1 := test.NewPack(t, ds, "test1") - query1 := test.NewQuery(t, ds, "time", "select * from time", 0, true) + query1 := test.NewQuery(t, ds, nil, "time", "select * from time", 0, true) squery1 := test.NewScheduledQuery(t, ds, pack1.ID, query1.ID, 30, true, true, "time-scheduled") pack2 := test.NewPack(t, ds, "test2") - query2 := test.NewQuery(t, ds, "time2", "select * from time", 0, true) + query2 := test.NewQuery(t, ds, nil, "time2", "select * from time", 0, true) squery2 := test.NewScheduledQuery(t, ds, pack2.ID, query2.ID, 30, true, true, "time-scheduled") ctx, cancelFunc := context.WithCancel(context.Background()) @@ -3463,7 +3493,7 @@ func testHostsSavePackStatsConcurrent(t *testing.T, ds *Datastore) { }, }, } - return ds.SaveHostPackStats(context.Background(), host.ID, packStats) + return ds.SaveHostPackStats(context.Background(), host.TeamID, host.ID, packStats) } errCh := make(chan error) @@ -3634,43 +3664,20 @@ func testHostsAllPackStats(t *testing.T, ds *Datastore) { require.NoError(t, err) require.NotNil(t, host) - // Create global pack (and one scheduled query in it). - test.AddAllHostsLabel(t, ds) // the global pack needs the "All Hosts" label. - labels, err := ds.ListLabels(context.Background(), fleet.TeamFilter{}, fleet.ListOptions{}) - require.NoError(t, err) - require.Len(t, labels, 1) - globalPack, err := ds.EnsureGlobalPack(context.Background()) - require.NoError(t, err) - globalQuery := test.NewQuery(t, ds, "global-time", "select * from time", 0, true) - globalSQuery := test.NewScheduledQuery(t, ds, globalPack.ID, globalQuery.ID, 30, true, true, "time-scheduled-global") - err = ds.AsyncBatchInsertLabelMembership(context.Background(), [][2]uint{{labels[0].ID, host.ID}}) - require.NoError(t, err) - - // Create a team and its pack (and one scheduled query in it). - team, err := ds.NewTeam(context.Background(), &fleet.Team{ - Name: "team1", - }) - require.NoError(t, err) - require.NoError(t, ds.AddHostsToTeam(context.Background(), &team.ID, []uint{host.ID})) - teamPack, err := ds.EnsureTeamPack(context.Background(), team.ID) - require.NoError(t, err) - teamQuery := test.NewQuery(t, ds, "team-time", "select * from time", 0, true) - teamSQuery := test.NewScheduledQuery(t, ds, teamPack.ID, teamQuery.ID, 31, true, true, "time-scheduled-team") - // Create a "user created" pack (and one scheduled query in it). userPack, err := ds.NewPack(context.Background(), &fleet.Pack{ Name: "test1", HostIDs: []uint{host.ID}, }) require.NoError(t, err) - userQuery := test.NewQuery(t, ds, "user-time", "select * from time", 0, true) + userQuery := test.NewQuery(t, ds, nil, "user-time", "select * from time", 0, true) userSQuery := test.NewScheduledQuery(t, ds, userPack.ID, userQuery.ID, 30, true, true, "time-scheduled-user") // Even if the scheduled queries didn't run, we get their pack stats (with zero values). host, err = ds.Host(context.Background(), host.ID) require.NoError(t, err) packStats := host.PackStats - require.Len(t, packStats, 3) + require.Len(t, packStats, 1) sort.Sort(packStatsSlice(packStats)) for _, tc := range []struct { expectedPack *fleet.Pack @@ -3678,23 +3685,11 @@ func testHostsAllPackStats(t *testing.T, ds *Datastore) { expectedSQuery *fleet.ScheduledQuery packStats fleet.PackStats }{ - { - expectedPack: globalPack, - expectedQuery: globalQuery, - expectedSQuery: globalSQuery, - packStats: packStats[0], - }, - { - expectedPack: teamPack, - expectedQuery: teamQuery, - expectedSQuery: teamSQuery, - packStats: packStats[1], - }, { expectedPack: userPack, expectedQuery: userQuery, expectedSQuery: userSQuery, - packStats: packStats[2], + packStats: packStats[0], }, } { require.Equal(t, tc.expectedPack.ID, tc.packStats.PackID) @@ -3718,38 +3713,6 @@ func testHostsAllPackStats(t *testing.T, ds *Datastore) { require.Zero(t, tc.packStats.QueryStats[0].WallTime) } - globalPackSQueryStats := []fleet.ScheduledQueryStats{{ - ScheduledQueryName: globalSQuery.Name, - ScheduledQueryID: globalSQuery.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, - AverageMemory: 8000, - Denylisted: false, - Executions: 164, - Interval: 30, - LastExecuted: time.Unix(1620325191, 0).UTC(), - OutputSize: 1337, - SystemTime: 150, - UserTime: 180, - WallTime: 0, - }} - teamPackSQueryStats := []fleet.ScheduledQueryStats{{ - ScheduledQueryName: teamSQuery.Name, - ScheduledQueryID: teamSQuery.ID, - QueryName: teamQuery.Name, - PackName: teamPack.Name, - PackID: teamPack.ID, - AverageMemory: 8001, - Denylisted: true, - Executions: 165, - Interval: 31, - LastExecuted: time.Unix(1620325190, 0).UTC(), - OutputSize: 1338, - SystemTime: 151, - UserTime: 181, - WallTime: 1, - }} userPackSQueryStats := []fleet.ScheduledQueryStats{{ ScheduledQueryName: userSQuery.Name, ScheduledQueryID: userSQuery.ID, @@ -3769,22 +3732,14 @@ func testHostsAllPackStats(t *testing.T, ds *Datastore) { // Reload the host and set the scheduled queries stats. host, err = ds.Host(context.Background(), host.ID) require.NoError(t, err) - hostPackStats := []fleet.PackStats{ - {PackID: globalPack.ID, PackName: globalPack.Name, QueryStats: globalPackSQueryStats}, - {PackID: teamPack.ID, PackName: teamPack.Name, QueryStats: teamPackSQueryStats}, - } - err = ds.SaveHostPackStats(context.Background(), host.ID, hostPackStats) - require.NoError(t, err) host, err = ds.Host(context.Background(), host.ID) require.NoError(t, err) packStats = host.PackStats - require.Len(t, packStats, 3) + require.Len(t, packStats, 1) sort.Sort(packStatsSlice(packStats)) - require.ElementsMatch(t, packStats[0].QueryStats, globalPackSQueryStats) - require.ElementsMatch(t, packStats[1].QueryStats, teamPackSQueryStats) - require.ElementsMatch(t, packStats[2].QueryStats, userPackSQueryStats) + require.ElementsMatch(t, packStats[0].QueryStats, userPackSQueryStats) } // See #2965. @@ -3805,6 +3760,7 @@ func testHostsPackStatsMultipleHosts(t *testing.T, ds *Datastore) { }) require.NoError(t, err) require.NotNil(t, host1) + osqueryHostID2, _ := server.GenerateRandomText(10) host2, err := ds.NewHost(context.Background(), &fleet.Host{ DetailUpdatedAt: time.Now(), @@ -3827,10 +3783,15 @@ func testHostsPackStatsMultipleHosts(t *testing.T, ds *Datastore) { labels, err := ds.ListLabels(context.Background(), fleet.TeamFilter{}, fleet.ListOptions{}) require.NoError(t, err) require.Len(t, labels, 1) - globalPack, err := ds.EnsureGlobalPack(context.Background()) + + userPack, err := ds.NewPack(context.Background(), &fleet.Pack{ + Name: "test1", + HostIDs: []uint{host1.ID, host2.ID}, + }) require.NoError(t, err) - globalQuery := test.NewQuery(t, ds, "global-time", "select * from time", 0, true) - globalSQuery := test.NewScheduledQuery(t, ds, globalPack.ID, globalQuery.ID, 30, true, true, "time-scheduled-global") + + userQuery := test.NewQuery(t, ds, nil, "global-time", "select * from time", 0, true) + userSQuery := test.NewScheduledQuery(t, ds, userPack.ID, userQuery.ID, 30, true, true, "time-scheduled-global") err = ds.AsyncBatchInsertLabelMembership(context.Background(), [][2]uint{ {labels[0].ID, host1.ID}, {labels[0].ID, host2.ID}, @@ -3838,11 +3799,11 @@ func testHostsPackStatsMultipleHosts(t *testing.T, ds *Datastore) { require.NoError(t, err) globalStatsHost1 := []fleet.ScheduledQueryStats{{ - ScheduledQueryName: globalSQuery.Name, - ScheduledQueryID: globalSQuery.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery.Name, + ScheduledQueryID: userSQuery.ID, + QueryName: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8000, Denylisted: false, Executions: 164, @@ -3854,11 +3815,11 @@ func testHostsPackStatsMultipleHosts(t *testing.T, ds *Datastore) { WallTime: 0, }} globalStatsHost2 := []fleet.ScheduledQueryStats{{ - ScheduledQueryName: globalSQuery.Name, - ScheduledQueryID: globalSQuery.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery.Name, + ScheduledQueryID: userSQuery.ID, + QueryName: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 9000, Denylisted: false, Executions: 165, @@ -3887,9 +3848,9 @@ func testHostsPackStatsMultipleHosts(t *testing.T, ds *Datastore) { host, err := ds.Host(context.Background(), tc.hostID) require.NoError(t, err) hostPackStats := []fleet.PackStats{ - {PackID: globalPack.ID, PackName: globalPack.Name, QueryStats: tc.globalStats}, + {PackID: userPack.ID, PackName: userPack.Name, QueryStats: tc.globalStats}, } - err = ds.SaveHostPackStats(context.Background(), host.ID, hostPackStats) + err = ds.SaveHostPackStats(context.Background(), host.TeamID, host.ID, hostPackStats) require.NoError(t, err) } @@ -3934,6 +3895,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { }) require.NoError(t, err) require.NotNil(t, host1) + osqueryHostID2, _ := server.GenerateRandomText(10) host2, err := ds.NewHost(context.Background(), &fleet.Host{ DetailUpdatedAt: time.Now(), @@ -3951,69 +3913,80 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { require.NoError(t, err) require.NotNil(t, host2) - // Create global pack (and one scheduled query in it). - test.AddAllHostsLabel(t, ds) // the global pack needs the "All Hosts" label. + test.AddAllHostsLabel(t, ds) labels, err := ds.ListLabels(context.Background(), fleet.TeamFilter{}, fleet.ListOptions{}) require.NoError(t, err) require.Len(t, labels, 1) - globalPack, err := ds.EnsureGlobalPack(context.Background()) + + userPack, err := ds.NewPack(context.Background(), &fleet.Pack{ + Name: "test1", + HostIDs: []uint{host1.ID, host2.ID}, + }) require.NoError(t, err) - globalQuery := test.NewQuery(t, ds, "global-time", "select * from time", 0, true) - globalSQuery1, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ + userQuery1 := test.NewQuery(t, ds, nil, "global-time", "select * from time", 0, true) + userQuery2 := test.NewQuery(t, ds, nil, "global-time-2", "select * from time", 0, true) + userQuery3 := test.NewQuery(t, ds, nil, "global-time-3", "select * from time", 0, true) + userQuery4 := test.NewQuery(t, ds, nil, "global-time-4", "select * from time", 0, true) + userQuery5 := test.NewQuery(t, ds, nil, "global-time-5", "select * from time", 0, true) + userSQuery1, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For Linux only", - PackID: globalPack.ID, - QueryID: globalQuery.ID, + PackID: userPack.ID, + QueryID: userQuery1.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), Platform: ptr.String("linux"), }) require.NoError(t, err) - require.NotZero(t, globalSQuery1.ID) - globalSQuery2, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ + require.NotZero(t, userSQuery1.ID) + + userSQuery2, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For Darwin only", - PackID: globalPack.ID, - QueryID: globalQuery.ID, + PackID: userPack.ID, + QueryID: userQuery2.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), Platform: ptr.String("darwin"), }) require.NoError(t, err) - require.NotZero(t, globalSQuery2.ID) - globalSQuery3, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ + require.NotZero(t, userSQuery2.ID) + + userSQuery3, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For Darwin and Linux", - PackID: globalPack.ID, - QueryID: globalQuery.ID, + PackID: userPack.ID, + QueryID: userQuery3.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), Platform: ptr.String("darwin,linux"), }) require.NoError(t, err) - require.NotZero(t, globalSQuery3.ID) - globalSQuery4, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ + require.NotZero(t, userSQuery3.ID) + + userSQuery4, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For All Platforms", - PackID: globalPack.ID, - QueryID: globalQuery.ID, + PackID: userPack.ID, + QueryID: userQuery4.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), Platform: ptr.String(""), }) require.NoError(t, err) - require.NotZero(t, globalSQuery4.ID) - globalSQuery5, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ + require.NotZero(t, userSQuery4.ID) + + userSQuery5, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For All Platforms v2", - PackID: globalPack.ID, - QueryID: globalQuery.ID, + PackID: userPack.ID, + QueryID: userQuery5.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), Platform: nil, }) require.NoError(t, err) - require.NotZero(t, globalSQuery5.ID) + require.NotZero(t, userSQuery5.ID) err = ds.AsyncBatchInsertLabelMembership(context.Background(), [][2]uint{ {labels[0].ID, host1.ID}, @@ -4023,11 +3996,11 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { globalStats := []fleet.ScheduledQueryStats{ { - ScheduledQueryName: globalSQuery2.Name, - ScheduledQueryID: globalSQuery2.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery2.Name, + ScheduledQueryID: userSQuery2.ID, + QueryName: userQuery2.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8001, Denylisted: false, Executions: 165, @@ -4039,11 +4012,11 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { WallTime: 1, }, { - ScheduledQueryName: globalSQuery3.Name, - ScheduledQueryID: globalSQuery3.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery3.Name, + ScheduledQueryID: userSQuery3.ID, + QueryName: userQuery3.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8002, Denylisted: false, Executions: 166, @@ -4055,11 +4028,11 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { WallTime: 2, }, { - ScheduledQueryName: globalSQuery4.Name, - ScheduledQueryID: globalSQuery4.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery4.Name, + ScheduledQueryID: userSQuery4.ID, + QueryName: userQuery4.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8003, Denylisted: false, Executions: 167, @@ -4071,11 +4044,11 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { WallTime: 3, }, { - ScheduledQueryName: globalSQuery5.Name, - ScheduledQueryID: globalSQuery5.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery5.Name, + ScheduledQueryID: userSQuery5.ID, + QueryName: userQuery5.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8003, Denylisted: false, Executions: 167, @@ -4096,11 +4069,11 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { stats[i] = globalStats[i] } stats = append(stats, fleet.ScheduledQueryStats{ - ScheduledQueryName: globalSQuery1.Name, - ScheduledQueryID: globalSQuery1.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery1.Name, + ScheduledQueryID: userSQuery1.ID, + QueryName: userQuery1.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8003, Denylisted: false, Executions: 167, @@ -4114,9 +4087,9 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { host, err := ds.Host(context.Background(), host1.ID) require.NoError(t, err) hostPackStats := []fleet.PackStats{ - {PackID: globalPack.ID, PackName: globalPack.Name, QueryStats: stats}, + {PackID: userPack.ID, PackName: userPack.Name, QueryStats: stats}, } - err = ds.SaveHostPackStats(context.Background(), host.ID, hostPackStats) + err = ds.SaveHostPackStats(context.Background(), host.TeamID, host.ID, hostPackStats) require.NoError(t, err) // host should only return scheduled query stats only for the scheduled queries @@ -4143,11 +4116,11 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { require.Len(t, packStats2[0].QueryStats, 4) zeroStats := []fleet.ScheduledQueryStats{ { - ScheduledQueryName: globalSQuery1.Name, - ScheduledQueryID: globalSQuery1.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery1.Name, + ScheduledQueryID: userSQuery1.ID, + QueryName: userQuery1.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 0, Denylisted: false, Executions: 0, @@ -4159,11 +4132,11 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { WallTime: 0, }, { - ScheduledQueryName: globalSQuery3.Name, - ScheduledQueryID: globalSQuery3.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery3.Name, + ScheduledQueryID: userSQuery3.ID, + QueryName: userQuery3.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 0, Denylisted: false, Executions: 0, @@ -4175,11 +4148,11 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { WallTime: 0, }, { - ScheduledQueryName: globalSQuery4.Name, - ScheduledQueryID: globalSQuery4.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery4.Name, + ScheduledQueryID: userSQuery4.ID, + QueryName: userQuery4.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 0, Denylisted: false, Executions: 0, @@ -4191,11 +4164,11 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { WallTime: 0, }, { - ScheduledQueryName: globalSQuery5.Name, - ScheduledQueryID: globalSQuery5.ID, - QueryName: globalQuery.Name, - PackName: globalPack.Name, - PackID: globalPack.ID, + ScheduledQueryName: userSQuery5.Name, + ScheduledQueryID: userSQuery5.ID, + QueryName: userQuery5.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 0, Denylisted: false, Executions: 0, @@ -5690,7 +5663,7 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) { HostIDs: []uint{host.ID}, }) require.NoError(t, err) - query := test.NewQuery(t, ds, "time", "select * from time", 0, true) + query := test.NewQuery(t, ds, nil, "time", "select * from time", 0, true) squery := test.NewScheduledQuery(t, ds, pack.ID, query.ID, 30, true, true, "time-scheduled") stats := []fleet.ScheduledQueryStats{ { @@ -5716,7 +5689,7 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) { QueryStats: stats, }, } - err = ds.SaveHostPackStats(context.Background(), host.ID, hostPackStats) + err = ds.SaveHostPackStats(context.Background(), host.TeamID, host.ID, hostPackStats) require.NoError(t, err) // Updates label_membership. @@ -6110,7 +6083,7 @@ func testFailingPoliciesCount(t *testing.T, ds *Datastore) { var policies []*fleet.Policy for i := 0; i < 10; i++ { - q := test.NewQuery(t, ds, fmt.Sprintf("query%d", i), "select 1", 0, true) + q := test.NewQuery(t, ds, nil, fmt.Sprintf("query%d", i), "select 1", 0, true) p, err := ds.NewGlobalPolicy(ctx, &u.ID, fleet.PolicyPayload{QueryID: &q.ID}) require.NoError(t, err) policies = append(policies, p) diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index 89ae14533e..86cedd44e9 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -929,8 +929,8 @@ func testListHostsInLabelFailingPolicies(t *testing.T, ds *Datastore) { filter := fleet.TeamFilter{User: test.UserAdmin} - q := test.NewQuery(t, ds, "query1", "select 1", 0, true) - q2 := test.NewQuery(t, ds, "query2", "select 1", 0, true) + q := test.NewQuery(t, ds, nil, "query1", "select 1", 0, true) + q2 := test.NewQuery(t, ds, nil, "query2", "select 1", 0, true) p, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{ QueryID: &q.ID, }) diff --git a/server/datastore/mysql/migrations/tables/20230721135421_QueriesSchemaChanges.go b/server/datastore/mysql/migrations/tables/20230721135421_QueriesSchemaChanges.go new file mode 100644 index 0000000000..5202b562bb --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230721135421_QueriesSchemaChanges.go @@ -0,0 +1,58 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20230721135421, Down_20230721135421) +} + +func Up_20230721135421(tx *sql.Tx) error { + // Drop FK constraint based on queries (name) - since the uniqueness constraint on the queries + // table changed. + if _, err := tx.Exec(` + ALTER TABLE scheduled_queries + ADD team_id_char CHAR(10) DEFAULT '' NOT NULL, + DROP FOREIGN KEY scheduled_queries_query_name; + `); err != nil { + return errors.Wrap(err, "removing FK on scheduled_queries") + } + + if _, err := tx.Exec(` + ALTER TABLE queries + DROP INDEX idx_query_unique_name, + DROP INDEX constraint_query_name_unique, + + ADD team_id INT(10) UNSIGNED DEFAULT NULL, + ADD team_id_char CHAR(10) DEFAULT '' NOT NULL, + + ADD platform VARCHAR(255) DEFAULT '' NOT NULL, + ADD min_osquery_version VARCHAR(255) DEFAULT '' NOT NULL, + + ADD schedule_interval INT(10) UNSIGNED DEFAULT 0 NOT NULL, + ADD automations_enabled TINYINT(1) UNSIGNED DEFAULT 0 NOT NULL, + ADD logging_type VARCHAR(255) DEFAULT 'snapshot' NOT NULL, + + ADD FOREIGN KEY fk_queries_team_id (team_id) REFERENCES teams (id) ON DELETE CASCADE, + ADD UNIQUE INDEX idx_team_id_name_unq (team_id_char, name); + `); err != nil { + return errors.Wrap(err, "updating queries schema") + } + + // Add new FK constraint to make sure all scheduled_queries exists as 'global' queries. + if _, err := tx.Exec(` + ALTER TABLE scheduled_queries + ADD FOREIGN KEY fk_scheduled_queries_queries (team_id_char, query_name) REFERENCES queries (team_id_char, name) ON DELETE CASCADE ON UPDATE CASCADE; + `); err != nil { + return errors.Wrap(err, "adding new FK on scheduled_queries") + } + + return nil +} + +func Down_20230721135421(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20230721161508_QueriesDataMigrator.go b/server/datastore/mysql/migrations/tables/20230721161508_QueriesDataMigrator.go new file mode 100644 index 0000000000..1060b64550 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230721161508_QueriesDataMigrator.go @@ -0,0 +1,467 @@ +package tables + +import ( + "database/sql" + "fmt" + "strconv" + "strings" + "time" +) + +func init() { + MigrationClient.AddMigration(Up_20230721161508, Down_20230721161508) +} + +// This is meant to future-proof this migration, this type is based on the fleet.Query type. +type _20230719152138_Query struct { + TeamID *uint + TeamIDChar string + ObserverCanRun bool + ScheduleInterval uint + Platform string + MinOsqueryVersion string + AutomationsEnabled bool + LoggingType string + Name string + Description string + Query string + Saved bool + AuthorID *uint + ScheduledQID uint + ScheduledQueryInterval uint + ScheduledQSnapshot *bool + ScheduledQRemoved *bool + ScheduledQueryPlatform string + ScheduledQueryVersion string + ScheduledQueryTimestamp time.Time + PackType string + TeamRoles string +} + +func _20230719152138_QueryName(q _20230719152138_Query) string { + return fmt.Sprintf("%s - %d - %s", q.Name, q.ScheduledQID, q.ScheduledQueryTimestamp.Format("Jan _2 15:04:05.000")) +} + +func _20230719152138_migrate_global_packs(tx *sql.Tx) error { + selectStmt := ` + SELECT DISTINCT q.name, + q.description, + q.query, + q.author_id, + q.saved, + q.observer_can_run, + sq.id AS scheduled_query_id, + sq.interval AS scheduled_query_interval, + sq.snapshot AS scheduled_query_snapshot, + sq.removed AS scheduled_query_removed, + sq.platform AS scheduled_query_platform, + sq.version AS scheduled_query_version, + sq.created_at AS scheduled_created_at, + p.pack_type AS pack_type + FROM queries q + INNER JOIN scheduled_queries sq ON q.name = sq.query_name + INNER JOIN packs p ON sq.pack_id = p.id + WHERE p.pack_type = 'global' AND q.team_id IS NULL` + rows, err := tx.Query(selectStmt) + if err != nil { + return fmt.Errorf("error executing 'Query' for scheduled queries from global packs: %s", err) + } + defer rows.Close() + + var args []interface{} + var nRows int + + for rows.Next() { + nRows += 1 + query := _20230719152138_Query{} + if err := rows.Scan( + &query.Name, + &query.Description, + &query.Query, + &query.AuthorID, + &query.Saved, + &query.ObserverCanRun, + &query.ScheduledQID, + &query.ScheduledQueryInterval, + &query.ScheduledQSnapshot, + &query.ScheduledQRemoved, + &query.ScheduledQueryPlatform, + &query.ScheduledQueryVersion, + &query.ScheduledQueryTimestamp, + &query.PackType, + ); err != nil { + return fmt.Errorf("error executing 'Scan' for scheduled queries from global packs: %s", err) + } + + var loggingType string + if query.ScheduledQSnapshot != nil && *query.ScheduledQSnapshot { + loggingType = "snapshot" + } + if loggingType == "" && query.ScheduledQRemoved != nil { + if *query.ScheduledQRemoved { + loggingType = "differential" + } else { + loggingType = "differential_ignore_removals" + } + } + + args = append(args, + _20230719152138_QueryName(query), + query.Description, + query.Query, + query.AuthorID, + query.Saved, + query.ObserverCanRun, + query.ScheduledQueryPlatform, + query.ScheduledQueryVersion, + query.ScheduledQueryInterval, + loggingType, + true, + ) + } + if err := rows.Err(); err != nil { + return fmt.Errorf("error on 'rows' for scheduled queries from global packs: %s", err) + } + if err := rows.Close(); err != nil { + return fmt.Errorf("error executing 'Close' for scheduled queries from global packs: %s", err) + } + + if len(args) == 0 { + return nil + } + + insertStmt := ` + INSERT INTO queries ( + name, + description, + query, + author_id, + saved, + observer_can_run, + platform, + min_osquery_version, + schedule_interval, + logging_type, + automations_enabled + ) VALUES %s` + + placeHolders := strings.TrimSuffix(strings.Repeat("( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ),", nRows), ",") + if _, err = tx.Exec(fmt.Sprintf(insertStmt, placeHolders), args...); err != nil { + return fmt.Errorf("error executing 'Exec' for scheduled queries from global packs: %s", err) + } + + return nil +} + +func _20230719152138_migrate_team_packs(tx *sql.Tx) error { + selectStmt := ` + SELECT DISTINCT q.name, + q.description, + q.query, + q.author_id, + q.saved, + q.observer_can_run, + sq.id AS scheduled_query_id, + sq.interval AS scheduled_query_interval, + sq.snapshot AS scheduled_query_snapshot, + sq.removed AS scheduled_query_removed, + sq.platform AS scheduled_query_platform, + sq.version AS scheduled_query_version, + sq.created_at AS scheduled_created_at, + p.pack_type AS pack_type + FROM queries q + INNER JOIN scheduled_queries sq ON q.team_id IS NULL AND q.name = sq.query_name + INNER JOIN packs p ON sq.pack_id = p.id + WHERE p.pack_type <> 'global' + AND p.pack_type IS NOT NULL` + + rows, err := tx.Query(selectStmt) + if err != nil { + return fmt.Errorf("error executing 'Query' for scheduled queries from team packs: %s", err) + } + defer rows.Close() + + var args []interface{} + var nRows int + + for rows.Next() { + nRows += 1 + var query _20230719152138_Query + if err := rows.Scan( + &query.Name, + &query.Description, + &query.Query, + &query.AuthorID, + &query.Saved, + &query.ObserverCanRun, + &query.ScheduledQID, + &query.ScheduledQueryInterval, + &query.ScheduledQSnapshot, + &query.ScheduledQRemoved, + &query.ScheduledQueryPlatform, + &query.ScheduledQueryVersion, + &query.ScheduledQueryTimestamp, + &query.PackType, + ); err != nil { + return fmt.Errorf("error executing 'Scan' for scheduled queries from team packs: %s", err) + } + + teamIDParts := strings.Split(query.PackType, "-") + if len(teamIDParts) != 2 { + return fmt.Errorf("invalid pack_type value %s", query.PackType) + } + teamID, err := strconv.Atoi(teamIDParts[1]) + if err != nil { + return fmt.Errorf("error parsing TeamID for scheduled queries from team packs: %s", err) + } + + var loggingType string + if query.ScheduledQSnapshot != nil && *query.ScheduledQSnapshot { + loggingType = "snapshot" + } + if loggingType == "" && query.ScheduledQRemoved != nil { + if *query.ScheduledQRemoved { + loggingType = "differential" + } else { + loggingType = "differential_ignore_removals" + } + } + + args = append(args, + teamID, + teamIDParts[1], + _20230719152138_QueryName(query), + query.Description, + query.Query, + query.AuthorID, + query.Saved, + query.ObserverCanRun, + query.ScheduledQueryPlatform, + query.ScheduledQueryVersion, + query.ScheduledQueryInterval, + loggingType, + true, + ) + } + if err := rows.Err(); err != nil { + return fmt.Errorf("error on 'rows' for scheduled queries from team packs: %s", err) + } + if err := rows.Close(); err != nil { + return fmt.Errorf("error closing reader from team packs: %s", err) + } + + if len(args) == 0 { + return nil + } + + insertStmt := ` + INSERT INTO queries ( + team_id, + team_id_char, + name, + description, + query, + author_id, + saved, + observer_can_run, + platform, + min_osquery_version, + schedule_interval, + logging_type, + automations_enabled + ) VALUES %s` + + placeHolders := strings.TrimSuffix(strings.Repeat("( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ),", nRows), ",") + if _, err = tx.Exec(fmt.Sprintf(insertStmt, placeHolders), args...); err != nil { + return fmt.Errorf("error executing 'Exec' for scheduled queries from team packs: %s", err) + } + + return nil +} + +func _20230719152138_migrate_non_scheduled(tx *sql.Tx) error { + // If the query is not scheduled, then it stays global except if it was created by a team user, + // in which case the query is duplicated as a team query iff the user is an admin or mantainer of the team. + selectStmt := ` + SELECT DISTINCT q.name, + q.description, + q.query, + q.author_id, + q.saved, + q.observer_can_run, + GROUP_CONCAT(CONCAT(ut.team_id, ':', ut.role)) AS team_roles + FROM queries q + LEFT JOIN scheduled_queries sq ON q.team_id IS NULL AND q.name = sq.query_name + INNER JOIN user_teams ut on q.author_id = ut.user_id + WHERE sq.id IS NULL + GROUP BY q.id` + + rows, err := tx.Query(selectStmt) + if err != nil { + return fmt.Errorf("error executing 'Query' for non-scheduled queries: %s", err) + } + defer rows.Close() + + var args []interface{} + var nRows int + + for rows.Next() { + query := _20230719152138_Query{} + if err := rows.Scan( + &query.Name, + &query.Description, + &query.Query, + &query.AuthorID, + &query.Saved, + &query.ObserverCanRun, + &query.TeamRoles, + ); err != nil { + return fmt.Errorf("error executing 'Scan' for non-scheduled queries: %s", err) + } + teamRoles := strings.Split(query.TeamRoles, ",") + for _, teamRole := range teamRoles { + teamRoleParts := strings.Split(teamRole, ":") + + role := teamRoleParts[1] + if role == "observer" || role == "observer_plus" { + continue + } + nRows += 1 + teamID, err := strconv.Atoi(teamRoleParts[0]) + if err != nil { + return fmt.Errorf("error parsing team ID on non-scheduled queries: %s", err) + } + args = append(args, + query.Name, + query.Description, + query.Query, + query.AuthorID, + query.Saved, + query.ObserverCanRun, + teamID, + teamRoleParts[0], + ) + } + } + if err := rows.Err(); err != nil { + return fmt.Errorf("error on 'rows' for non-scheduled queries: %s", err) + } + if err := rows.Close(); err != nil { + return fmt.Errorf("error executing 'Close' for non-scheduled queries: %s", err) + } + + if len(args) == 0 { + return nil + } + + insertStmt := ` + INSERT INTO queries ( + name, + description, + query, + author_id, + saved, + observer_can_run, + team_id, + team_id_char + ) VALUES %s` + + placeHolders := strings.TrimSuffix(strings.Repeat("( ?, ?, ?, ?, ?, ?, ?, ? ),", nRows), ",") + if _, err = tx.Exec(fmt.Sprintf(insertStmt, placeHolders), args...); err != nil { + return fmt.Errorf("error executing 'Exec' on non-scheduled queries: %s", err) + } + return nil +} + +func _20230719152138_clean_up(tx *sql.Tx) error { + // Remove query stats + if _, err := tx.Exec(`TRUNCATE scheduled_query_stats`); err != nil { + return fmt.Errorf("error truncating 'scheduled_query_stats': %s", err) + } + if _, err := tx.Exec(`DELETE FROM aggregated_stats WHERE type = 'query' OR type = 'scheduled_query'`); err != nil { + return fmt.Errorf("error removing aggregated_stats: %s", err) + } + + // Delete queries that only belong to 'global' and 'team' packs + if _, err := tx.Exec(` +DELETE +FROM queries +WHERE name IN (SELECT query_name + FROM (SELECT query_name + FROM scheduled_queries + INNER JOIN packs p on scheduled_queries.pack_id = p.id + WHERE p.pack_type = 'global' + UNION + SELECT query_name + FROM scheduled_queries + INNER JOIN packs p on scheduled_queries.pack_id = p.id + WHERE p.pack_type LIKE 'team-%') r + WHERE query_name NOT IN (SELECT query_name + FROM scheduled_queries + INNER JOIN packs p on scheduled_queries.pack_id = p.id + WHERE p.pack_type IS NULL)) + + `); err != nil { + return fmt.Errorf("error deleting queries that belong only to global / team packs: %s", err) + } + + // Remove 'global' and 'team' packs ... relevant rows in the 'scheduled_queries' table should be + // deleted because of the on cascade delete on the pack_id FK. + if _, err := tx.Exec(`DELETE FROM packs WHERE pack_type = 'global' OR pack_type LIKE 'team-%'`); err != nil { + return fmt.Errorf("error deleting packs: %s", err) + } + + return nil +} + +func Up_20230721161508(tx *sql.Tx) error { + // Migrates 'old' scheduled queries to the 'new' query schema. + // Queries can either be: + // 1 - Scheduled, which can either belong to: + // 1.1 - The global pack: + // For each scheduled query create a single global scheduled query named `$query.name - $scheduled.id`. + // 1.2 - Team pack: + // Create a new team query with the name of `$query.name - $scheduled.id`. + // 1.3 - A user pack (a.k.a 2017 pack): + // Do nothing. + // 2 - Not scheduled: + // 2.1 - If the author belongs to the global team, do nothing. + // 2.2 - Otherwise, for each team the author belongs to: + // Create a new team query with the name of `$query.name` iff the author can run the query. + // + + // ---------------------------------------------------------------------------- + // (2.2) Non scheduled queries, author belongs to one or more teams: + // ---------------------------------------------------------------------------- + if err := _20230719152138_migrate_non_scheduled(tx); err != nil { + return err + } + + // ------------------------------------- + // (1.1) Global pack scheduled queries + // ------------------------------------- + if err := _20230719152138_migrate_global_packs(tx); err != nil { + return err + } + + // ------------------------------------- + // (1.2) Team pack scheduled queries + // ------------------------------------- + if err := _20230719152138_migrate_team_packs(tx); err != nil { + return err + } + + //------------------------------------------------------- + // Remove stats, global packs and team packs and queries + // that are only used in global/team packs. + //------------------------------------------------------- + if err := _20230719152138_clean_up(tx); err != nil { + return err + } + + return nil +} + +func Down_20230721161508(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20230721161508_QueriesDataMigrator_test.go b/server/datastore/mysql/migrations/tables/20230721161508_QueriesDataMigrator_test.go new file mode 100644 index 0000000000..7dec3b414c --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230721161508_QueriesDataMigrator_test.go @@ -0,0 +1,293 @@ +package tables + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUp_20230721161508(t *testing.T) { + db := applyUpToPrev(t) + + dataStmts := ` + INSERT INTO users VALUES + (1,'2023-07-21 20:32:32','2023-07-21 20:32:32',_binary '$2a$12$n6hwsD7OU2bAXX94551DQOBcNNhfsEPS3Y6JEuLDjsLNvry3lgJjy','0fF81xRQIriYzm5fdXouk3V3tRwsZJhV','admin','admin@email.com',0,'','',0,'admin',0), + (2,'2023-07-21 20:33:13','2023-07-21 20:35:26',_binary '$2a$12$YxPPOd5TOmYhDlH5CfGIfuxBe4GJ78gbwvtxoBHTTw.symxpVcEZS','JPDLcBcv4j1QwIU+rHoRWBt3HVJC8hnf','User 1','user1@email.com',0,'','',0,NULL,0), + (3,'2023-07-21 20:33:31','2023-07-21 20:36:42',_binary '$2a$12$u3kuHl44jMojsols1NayLu0pPBwZvnWH6j6ZuDk6HsN4r0jgg7BRu','MoWlTEHH9zR7blcJ0l7/1c4EMnkh/dxq','User2','user2@email.com',0,'','',0,NULL,0); + + INSERT INTO teams VALUES + (1,'2023-07-21 20:32:42','Team 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, \"enable_disk_encryption\": false}}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": true}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"agent_options\": {\"config\": {\"options\": {\"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\": {}}, \"webhook_settings\": {\"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}}'), + (2,'2023-07-21 20:32:47','Team 2','','{\"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, \"enable_disk_encryption\": false}}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": true}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"agent_options\": {\"config\": {\"options\": {\"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\": {}}, \"webhook_settings\": {\"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}}'); + + INSERT INTO user_teams (user_id, team_id, role) VALUES + (2,1,'admin'), + (2,2,'admin'), + (3,2,'admin'), + (3,1,'observer'); + + INSERT INTO packs (id, created_at, updated_at, disabled, name, description, platform, pack_type) VALUES + (1,'2023-07-21 20:33:49','2023-07-21 20:33:49',0,'Global','Global pack','','global'), + (2,'2023-07-21 20:34:34','2023-07-21 20:34:34',0,'performance-metrics','','',NULL), + (3,'2023-07-21 20:36:03','2023-07-21 20:36:03',0,'Team: Team 1','Schedule additional queries for all hosts assigned to this team.','','team-1'), + (4,'2023-07-21 20:36:45','2023-07-21 20:36:45',0,'Team: Team 2','Schedule additional queries for all hosts assigned to this team.','','team-2'); + + INSERT INTO queries (id, created_at, updated_at, saved, name, description, query, author_id, observer_can_run, team_id, team_id_char, platform, min_osquery_version, schedule_interval, automations_enabled, logging_type) VALUES + (1,'2023-07-21 20:33:47','2023-07-21 20:33:47',1,'Admin Global Query','Admin desc','SELECT * FROM osquery_info;',1,1,NULL,'','','',0,0,''), + (2,'2023-07-21 20:34:34','2023-07-21 20:34:34',1,'per_query_perf','Records the CPU time and memory usage for each individual query. Helpful for identifying queries that may impact performance.','SELECT name, interval, executions, output_size, wall_time, (user_time/executions) AS avg_user_time, (system_time/executions) AS avg_system_time, average_memory FROM osquery_schedule;',1,0,NULL,'','','',0,0,''), + (3,'2023-07-21 20:34:34','2023-07-21 20:34:34',1,'runtime_perf','Track the amount of CPU time used by osquery.','SELECT ov.version AS os_version, ov.platform AS os_platform, ov.codename AS os_codename, i.*, p.resident_size, p.user_time, p.system_time, time.minutes AS counter, db.db_size_mb AS database_size FROM osquery_info i, os_version ov, processes p, time, (SELECT (sum(size) / 1024) / 1024.0 AS db_size_mb FROM (SELECT value FROM osquery_flags WHERE name = \'database_path\' LIMIT 1) flags, file WHERE path LIKE flags.value || \'%%\' AND type = \'regular\') db WHERE p.pid = i.pid;',1,0,NULL,'','','',0,0,''), + (4,'2023-07-21 20:34:34','2023-07-21 20:34:34',1,'endpoint_security_tool_perf','Track the percentage of total CPU time utilized by $endpoint_security_tool','SELECT ((tool_time*100)/(SUM(system_time) + SUM(user_time))) AS pct FROM processes, (SELECT (SUM(processes.system_time)+SUM(processes.user_time)) AS tool_time FROM processes WHERE name=\'endpoint_security_tool\');',1,0,NULL,'','','',0,0,''), + (5,'2023-07-21 20:34:34','2023-07-21 20:34:34',1,'backup_tool_perf','Track the percentage of total CPU time utilized by $backup_tool','SELECT ((backuptool_time*100)/(SUM(system_time) + SUM(user_time))) AS pct FROM processes, (SELECT (SUM(processes.system_time)+SUM(processes.user_time)) AS backuptool_time FROM processes WHERE name=\'backup_tool\');',1,0,NULL,'','','',0,0,''), + (6,'2023-07-21 20:35:37','2023-07-21 20:35:37',1,'User 1 Query','User 1 Query Desc','SELECT * FROM osquery_info;',2,0,NULL,'','','',0,0,''), + (7,'2023-07-21 20:36:02','2023-07-21 20:36:02',1,'User 1 Query 2','','SELECT * FROM osquery_info;',2,1,NULL,'','','',0,0,''), + (8,'2023-07-21 20:37:01','2023-07-21 20:37:01',1,'User 2 Query','Some desc','SELECT * FROM osquery_info;',3,1,NULL,'','','',0,0,''), + (9,'2023-07-21 20:37:01','2023-07-21 20:37:01',1,'User 2 Query 2','Some desc','SELECT * FROM osquery_info;',3,1,NULL,'','','',0,0,''); + + INSERT INTO scheduled_queries VALUES + -- Global pack + (1,'2023-07-21 20:33:54','2023-07-21 20:33:54',1,1,86400,1,0,'','',NULL,'Admin Global Query','Admin Global Query','',NULL,''), + (2,'2023-07-21 20:34:00','2023-07-21 20:34:00',1,1,3600,1,0,'','',NULL,'Admin Global Query','Admin Global Query-1','',NULL,''), + + -- 2017 pack + (3,'2023-07-21 20:34:34','2023-07-21 20:34:34',2,NULL,1800,1,NULL,NULL,NULL,NULL,'per_query_perf','per_query_perf','Records the CPU time and memory usage for each individual query. Helpful for identifying queries that may impact performance.',NULL,''), + (4,'2023-07-21 20:34:34','2023-07-21 20:34:34',2,NULL,1800,1,NULL,NULL,NULL,NULL,'runtime_perf','runtime_perf','Track the amount of CPU time used by osquery.',NULL,''), + (5,'2023-07-21 20:34:34','2023-07-21 20:34:34',2,NULL,1800,1,NULL,NULL,NULL,NULL,'endpoint_security_tool_perf','endpoint_security_tool_perf','Track the percentage of total CPU time utilized by $endpoint_security_tool',NULL,''), + (6,'2023-07-21 20:34:34','2023-07-21 20:34:34',2,NULL,1800,1,NULL,NULL,NULL,NULL,'backup_tool_perf','backup_tool_perf','Track the percentage of total CPU time utilized by $backup_tool',NULL,''), + + -- Global pack + (7,'2023-07-21 20:34:46','2023-07-21 20:34:46',1,2,86400,1,0,'','',NULL,'per_query_perf','per_query_perf','',NULL,''), + (8,'2023-07-21 20:34:51','2023-07-21 20:34:51',1,2,86400,1,0,'','',NULL,'per_query_perf','per_query_perf-1','',NULL,''), + + -- Team-1 pack + (9,'2023-07-21 20:36:08','2023-07-21 20:36:08',3,6,86400,1,0,'','',NULL,'User 1 Query','User 1 Query','',NULL,''), + (10,'2023-07-21 20:36:13','2023-07-21 20:36:13',3,6,86400,1,0,'','',NULL,'User 1 Query','User 1 Query-1','',NULL,''), + (11,'2023-07-21 20:36:25','2023-07-21 20:36:25',3,2,86400,1,0,'','',NULL,'per_query_perf','per_query_perf','',NULL,''), + + -- Team-2 pack + (12,'2023-07-21 20:36:50','2023-07-21 20:36:50',4,5,86400,1,0,'','',NULL,'backup_tool_perf','backup_tool_perf','',NULL,''); + ` + _, err := db.Exec(dataStmts) + require.NoError(t, err) + + // Apply current migration. + applyNext(t, db) + + // 'User 2 Query' is non-scheduled and was created by user#3, so it should exists in both the + // global team and on team#2 + stmt := "SELECT description, query, author_id, saved, observer_can_run, team_id, team_id_char FROM queries WHERE name = ?" + rows, err := db.Query(stmt, "User 2 Query") + require.NoError(t, err) + defer rows.Close() + + var nRows int + var teamIDs []uint + var teamIDStrs []string + + for rows.Next() { + nRows += 1 + var teamIDStr string + query := _20230719152138_Query{} + err := rows.Scan( + &query.Description, + &query.Query, + &query.AuthorID, + &query.Saved, + &query.ObserverCanRun, + &query.TeamID, + &teamIDStr, + ) + require.NoError(t, err) + require.Equal(t, query.Description, "Some desc") + require.Equal(t, query.Query, "SELECT * FROM osquery_info;") + require.Equal(t, *query.AuthorID, uint(3)) + require.Equal(t, query.Saved, true) + require.Equal(t, query.ObserverCanRun, true) + + teamIDStrs = append(teamIDStrs, teamIDStr) + if query.TeamID != nil { + teamIDs = append(teamIDs, *query.TeamID) + } + } + require.Equal(t, nRows, 2) + require.ElementsMatch(t, teamIDStrs, []string{"", "2"}) + require.Contains(t, teamIDs, uint(2)) + + // The global pack has 4 different schedules two targeting 'Admin Global Query' and the other + // two targeting 'per_query_perf' so I expect to see 6 queries here: + // 'Admin Global Query - 1 - $timestamp' <- For schedule with id 1 + // 'Admin Global Query - 2 - $timestamp' <- For schedule with id 2 + // 'per_query_perf' <- Original (kept because is referenced by an 2017 pack) + // 'per_query_perf - 7 - $timestamp' <- For schedule with id 7 + // 'per_query_perf - 8 - $timestamp' <- For schedule with id 8 + stmt = `SELECT + name, + description, + query, + author_id, + saved, + observer_can_run, + platform, + min_osquery_version, + schedule_interval, + logging_type, + automations_enabled + FROM queries WHERE name LIKE ? AND team_id IS NULL + ` + + rows, err = db.Query(stmt, "Admin Global Query%") + require.NoError(t, err) + defer rows.Close() + + nRows = 0 + var names []string + var scheduleIntervals []uint + var automationsEnabled []bool + var loggingTypes []string + + for rows.Next() { + nRows += 1 + query := _20230719152138_Query{} + err := rows.Scan( + &query.Name, + &query.Description, + &query.Query, + &query.AuthorID, + &query.Saved, + &query.ObserverCanRun, + &query.Platform, + &query.MinOsqueryVersion, + &query.ScheduleInterval, + &query.LoggingType, + &query.AutomationsEnabled, + ) + require.NoError(t, err) + + names = append(names, query.Name) + scheduleIntervals = append(scheduleIntervals, query.ScheduleInterval) + automationsEnabled = append(automationsEnabled, query.AutomationsEnabled) + loggingTypes = append(loggingTypes, query.LoggingType) + + require.Equal(t, query.Description, "Admin desc") + require.Equal(t, query.Query, "SELECT * FROM osquery_info;") + require.Equal(t, *query.AuthorID, uint(1)) + require.Equal(t, query.Saved, true) + require.Equal(t, query.ObserverCanRun, true) + } + require.ElementsMatch(t, names, []string{"Admin Global Query - 1 - Jul 21 20:33:54.000", "Admin Global Query - 2 - Jul 21 20:34:00.000"}) + require.ElementsMatch(t, scheduleIntervals, []uint{3600, 86400}) + require.ElementsMatch(t, automationsEnabled, []bool{true, true}) + require.ElementsMatch(t, loggingTypes, []string{"snapshot", "snapshot"}) + require.Equal(t, nRows, 2) + + rows, err = db.Query(stmt, "per_query_perf%") + require.NoError(t, err) + defer rows.Close() + + nRows = 0 + names = []string{} + scheduleIntervals = []uint{} + automationsEnabled = []bool{} + loggingTypes = []string{} + + for rows.Next() { + nRows += 1 + query := _20230719152138_Query{} + err := rows.Scan( + &query.Name, + &query.Description, + &query.Query, + &query.AuthorID, + &query.Saved, + &query.ObserverCanRun, + &query.Platform, + &query.MinOsqueryVersion, + &query.ScheduleInterval, + &query.LoggingType, + &query.AutomationsEnabled, + ) + require.NoError(t, err) + + names = append(names, query.Name) + scheduleIntervals = append(scheduleIntervals, query.ScheduleInterval) + automationsEnabled = append(automationsEnabled, query.AutomationsEnabled) + loggingTypes = append(loggingTypes, query.LoggingType) + + require.Equal(t, query.Description, "Records the CPU time and memory usage for each individual query. Helpful for identifying queries that may impact performance.") + require.Equal(t, query.Query, "SELECT name, interval, executions, output_size, wall_time, (user_time/executions) AS avg_user_time, (system_time/executions) AS avg_system_time, average_memory FROM osquery_schedule;") + require.Equal(t, *query.AuthorID, uint(1)) + require.Equal(t, query.Saved, true) + require.Equal(t, query.ObserverCanRun, false) + } + require.ElementsMatch(t, names, []string{"per_query_perf", "per_query_perf - 7 - Jul 21 20:34:46.000", "per_query_perf - 8 - Jul 21 20:34:51.000"}) + require.ElementsMatch(t, scheduleIntervals, []uint{0, 86400, 86400}) + require.ElementsMatch(t, automationsEnabled, []bool{false, true, true}) + require.ElementsMatch(t, loggingTypes, []string{"", "snapshot", "snapshot"}) + require.Equal(t, nRows, 3) + + // We have two team packs (Team-1, Team-2) + // For Team-1, we have three schedules, two of them reference 'User 1 Query', the last one + // 'per_query_perf', so I expect to see five different queries on team#1: + // - 'User 1 Query - 9 - $timestamp' for schedule#9 + // - 'User 1 Query - 10 - $timestamp' for schedule#10 + // - 'per_query_perf - 11 - $timestamp' for schedule#11 + // For Team-2, we only have one schedule on 'backup_tool_perf', so I expect to see on team#2: + // - 'backup_tool_perf - 12 - $timestamp' + stmt = `SELECT + name, + description, + query, + author_id, + saved, + observer_can_run, + platform, + min_osquery_version, + schedule_interval, + logging_type, + automations_enabled + FROM queries + WHERE name LIKE ? AND team_id = ? AND name <> 'User 1 Query 2'` + + rows, err = db.Query(stmt, "per_query_perf%", 1) + require.NoError(t, err) + defer rows.Close() + + nRows = 0 + names = []string{} + scheduleIntervals = []uint{} + automationsEnabled = []bool{} + loggingTypes = []string{} + + for rows.Next() { + nRows += 1 + query := _20230719152138_Query{} + err := rows.Scan( + &query.Name, + &query.Description, + &query.Query, + &query.AuthorID, + &query.Saved, + &query.ObserverCanRun, + &query.Platform, + &query.MinOsqueryVersion, + &query.ScheduleInterval, + &query.LoggingType, + &query.AutomationsEnabled, + ) + require.NoError(t, err) + + names = append(names, query.Name) + scheduleIntervals = append(scheduleIntervals, query.ScheduleInterval) + automationsEnabled = append(automationsEnabled, query.AutomationsEnabled) + loggingTypes = append(loggingTypes, query.LoggingType) + + require.Equal(t, query.Description, "Records the CPU time and memory usage for each individual query. Helpful for identifying queries that may impact performance.") + require.Equal(t, query.Query, "SELECT name, interval, executions, output_size, wall_time, (user_time/executions) AS avg_user_time, (system_time/executions) AS avg_system_time, average_memory FROM osquery_schedule;") + require.Equal(t, *query.AuthorID, uint(1)) + require.Equal(t, query.Saved, true) + require.Equal(t, query.ObserverCanRun, false) + } + require.ElementsMatch(t, names, []string{"per_query_perf - 11 - Jul 21 20:36:25.000"}) + require.ElementsMatch(t, scheduleIntervals, []uint{86400}) + require.ElementsMatch(t, automationsEnabled, []bool{true}) + require.ElementsMatch(t, loggingTypes, []string{"snapshot"}) + require.Equal(t, nRows, 1) +} diff --git a/server/datastore/mysql/migrations/tables/20230726115701_AddQueriesIndices.go b/server/datastore/mysql/migrations/tables/20230726115701_AddQueriesIndices.go new file mode 100644 index 0000000000..fff346e77e --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230726115701_AddQueriesIndices.go @@ -0,0 +1,26 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20230726115701, Down_20230726115701) +} + +func Up_20230726115701(tx *sql.Tx) error { + if _, err := tx.Exec(` + ALTER TABLE queries + ADD UNIQUE INDEX idx_name_team_id_unq (name, team_id_char), + ADD INDEX idx_team_id_saved_auto_interval (team_id, saved, automations_enabled, schedule_interval); + `); err != nil { + return errors.Wrap(err, "updating queries indices") + } + return nil +} + +func Down_20230726115701(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/packs.go b/server/datastore/mysql/packs.go index 8c03ce72a5..e93786304b 100644 --- a/server/datastore/mysql/packs.go +++ b/server/datastore/mysql/packs.go @@ -76,8 +76,17 @@ func applyPackSpecDB(ctx context.Context, tx sqlx.ExtContext, spec *fleet.PackSp q.Name = q.QueryName } _, err := tx.ExecContext(ctx, query, - packID, q.QueryName, q.Name, q.Description, q.Interval, - q.Snapshot, q.Removed, q.Shard, q.Platform, q.Version, q.Denylist, + packID, + q.QueryName, + q.Name, + q.Description, + q.Interval, + q.Snapshot, + q.Removed, + q.Shard, + q.Platform, + q.Version, + q.Denylist, ) switch { case isChildForeignKeyError(err): @@ -433,116 +442,10 @@ func packDB(ctx context.Context, q sqlx.QueryerContext, pid uint) (*fleet.Pack, return pack, nil } -// EnsureGlobalPack gets or inserts a pack with type global -func (ds *Datastore) EnsureGlobalPack(ctx context.Context) (*fleet.Pack, error) { - pack := &fleet.Pack{} - err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { - // read from primary as we will create the pack if it doesn't exist - err := sqlx.GetContext(ctx, tx, pack, `SELECT * FROM packs WHERE pack_type = 'global'`) - if err == sql.ErrNoRows { - pack, err = insertNewGlobalPackDB(ctx, tx) - return err - } else if err != nil { - return ctxerr.Wrap(ctx, err, "get pack") - } - - return loadPackTargetsDB(ctx, tx, pack) - }) - if err != nil { - return nil, err - } - return pack, nil -} - -func insertNewGlobalPackDB(ctx context.Context, q sqlx.ExtContext) (*fleet.Pack, error) { - var packID uint - res, err := q.ExecContext(ctx, - `INSERT INTO packs (name, description, platform, pack_type) VALUES ('Global', 'Global pack', '','global')`, - ) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "insert pack") - } - packId, err := res.LastInsertId() - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "last insert id") - } - packID = uint(packId) - if _, err := q.ExecContext(ctx, - `INSERT INTO pack_targets (pack_id, type, target_id) VALUES (?, ?, (SELECT id FROM labels WHERE name = ?))`, - packID, fleet.TargetLabel, "All Hosts", - ); err != nil { - return nil, ctxerr.Wrap(ctx, err, "adding label to pack") - } - - return packDB(ctx, q, packID) -} - -func (ds *Datastore) EnsureTeamPack(ctx context.Context, teamID uint) (*fleet.Pack, error) { - pack := &fleet.Pack{} - err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { - t, err := teamDB(ctx, tx, teamID) - if err != nil || t == nil { - return ctxerr.Wrap(ctx, err, "Error finding team") - } - - teamType := fmt.Sprintf("team-%d", teamID) - // read from primary as we will create the team pack if it doesn't exist - err = sqlx.GetContext(ctx, tx, pack, `SELECT * FROM packs WHERE pack_type = ?`, teamType) - if err == sql.ErrNoRows { - pack, err = insertNewTeamPackDB(ctx, tx, t) - return err - } else if err != nil { - return ctxerr.Wrap(ctx, err, "get pack") - } - - if err := loadPackTargetsDB(ctx, tx, pack); err != nil { - return err - } - - return nil - }) - if err != nil { - return nil, err - } - return pack, nil -} - func teamScheduleName(team *fleet.Team) string { return fmt.Sprintf("Team: %s", team.Name) } -func teamSchedulePackType(team *fleet.Team) string { - return teamSchedulePackTypeByID(team.ID) -} - -func teamSchedulePackTypeByID(teamID uint) string { - return fmt.Sprintf("team-%d", teamID) -} - -func insertNewTeamPackDB(ctx context.Context, q sqlx.ExtContext, team *fleet.Team) (*fleet.Pack, error) { - var packID uint - res, err := q.ExecContext(ctx, - `INSERT INTO packs (name, description, platform, pack_type) - VALUES (?, 'Schedule additional queries for all hosts assigned to this team.', '',?)`, - teamScheduleName(team), teamSchedulePackType(team), - ) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "insert team pack") - } - packId, err := res.LastInsertId() - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "last insert id") - } - packID = uint(packId) - if _, err := q.ExecContext(ctx, - `INSERT INTO pack_targets (pack_id, type, target_id) VALUES (?, ?, ?)`, - packID, fleet.TargetTeam, team.ID, - ); err != nil { - return nil, ctxerr.Wrap(ctx, err, "adding team id target to pack") - } - return packDB(ctx, q, packID) -} - // ListPacks returns all fleet.Pack records limited and sorted by fleet.ListOptions func (ds *Datastore) ListPacks(ctx context.Context, opt fleet.PackListOptions) ([]*fleet.Pack, error) { query := `SELECT * FROM packs WHERE pack_type IS NULL OR pack_type = ''` @@ -568,10 +471,10 @@ func (ds *Datastore) ListPacksForHost(ctx context.Context, hid uint) ([]*fleet.P return listPacksForHost(ctx, ds.reader(ctx), hid) } -// listPacksForHost returns all the packs that are configured to run on the given host. +// listPacksForHost returns all the "user packs" that are configured to run on the given host. func listPacksForHost(ctx context.Context, db sqlx.QueryerContext, hid uint) ([]*fleet.Pack, error) { query := ` -SELECT DISTINCT packs.* FROM ( + SELECT DISTINCT packs.* FROM ( ( SELECT p.* FROM packs p JOIN pack_targets pt @@ -581,26 +484,29 @@ SELECT DISTINCT packs.* FROM ( AND pt.target_id = lm.label_id AND pt.type = ? ) - WHERE lm.host_id = ? AND NOT p.disabled + WHERE lm.host_id = ? AND NOT p.disabled AND p.pack_type IS NULL ) UNION ALL ( SELECT p.* FROM packs p - JOIN pack_targets pt - ON (p.id = pt.pack_id AND pt.type = ? AND pt.target_id = ?) + JOIN pack_targets pt ON (p.id = pt.pack_id AND pt.type = ? AND pt.target_id = ?) + WHERE p.pack_type IS NULL ) UNION ALL ( SELECT p.* FROM packs p JOIN pack_targets pt - ON (p.id = pt.pack_id AND pt.type = ? AND pt.target_id = (SELECT team_id FROM hosts WHERE id = ?))) - ) packs` + ON (p.id = pt.pack_id AND pt.type = ? AND pt.target_id = (SELECT team_id FROM hosts WHERE id = ?)) + WHERE p.pack_type IS NULL + )) packs` + packs := []*fleet.Pack{} if err := sqlx.SelectContext(ctx, db, &packs, query, fleet.TargetLabel, hid, fleet.TargetHost, hid, fleet.TargetTeam, hid, ); err != nil && err != sql.ErrNoRows { return nil, ctxerr.Wrap(ctx, err, "listing hosts in pack") } + return packs, nil } diff --git a/server/datastore/mysql/packs_test.go b/server/datastore/mysql/packs_test.go index 21c91af67d..61da8f1c3c 100644 --- a/server/datastore/mysql/packs_test.go +++ b/server/datastore/mysql/packs_test.go @@ -2,7 +2,6 @@ package mysql import ( "context" - "fmt" "math/rand" "testing" "time" @@ -31,13 +30,10 @@ func TestPacks(t *testing.T) { {"ApplySpecMissingQueries", testPacksApplySpecMissingQueries}, {"ApplySpecMissingName", testPacksApplySpecMissingName}, {"ListForHost", testPacksListForHost}, - {"EnsureGlobal", testPacksEnsureGlobal}, - {"EnsureTeam", testPacksEnsureTeam}, - {"TeamNameChangesTeamSchedule", testPacksTeamNameChangesTeamSchedule}, - {"TeamScheduleNamesMigrateToNewFormat", testPacksTeamScheduleNamesMigrateToNewFormat}, {"ApplySpecFailsOnTargetIDNull", testPacksApplySpecFailsOnTargetIDNull}, {"ApplyStatsNotLocking", testPacksApplyStatsNotLocking}, {"ApplyStatsNotLockingTryTwo", testPacksApplyStatsNotLockingTryTwo}, + {"ListForHostIncludesOnlyUserPacks", testListForHostIncludesOnlyUserPacks}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -420,125 +416,6 @@ func testPacksListForHost(t *testing.T, ds *Datastore) { } } -func testPacksEnsureGlobal(t *testing.T, ds *Datastore) { - test.AddAllHostsLabel(t, ds) - - packs, err := ds.ListPacks(context.Background(), fleet.PackListOptions{IncludeSystemPacks: true}) - require.Nil(t, err) - assert.Len(t, packs, 0) - - gp, err := ds.EnsureGlobalPack(context.Background()) - require.Nil(t, err) - - packs, err = ds.ListPacks(context.Background(), fleet.PackListOptions{IncludeSystemPacks: true}) - require.Nil(t, err) - assert.Len(t, packs, 1) - assert.Equal(t, gp.ID, packs[0].ID) - assert.Equal(t, "global", *gp.Type) - - labels, err := ds.LabelIDsByName(context.Background(), []string{"All Hosts"}) - require.Nil(t, err) - - assert.Equal(t, []uint{labels[0]}, gp.LabelIDs) - - _, err = ds.EnsureGlobalPack(context.Background()) - require.Nil(t, err) - - packs, err = ds.ListPacks(context.Background(), fleet.PackListOptions{IncludeSystemPacks: true}) - require.Nil(t, err) - assert.Len(t, packs, 1) - assert.Equal(t, gp.ID, packs[0].ID) - assert.Equal(t, "global", *gp.Type) -} - -func testPacksEnsureTeam(t *testing.T, ds *Datastore) { - packs, err := ds.ListPacks(context.Background(), fleet.PackListOptions{IncludeSystemPacks: true}) - require.Nil(t, err) - assert.Len(t, packs, 0) - - _, err = ds.EnsureTeamPack(context.Background(), 12) - require.Error(t, err) - - team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"}) - require.NoError(t, err) - - tp, err := ds.EnsureTeamPack(context.Background(), team1.ID) - require.NoError(t, err) - - packs, err = ds.ListPacks(context.Background(), fleet.PackListOptions{IncludeSystemPacks: true}) - require.Nil(t, err) - assert.Len(t, packs, 1) - assert.Equal(t, tp.ID, packs[0].ID) - assert.Equal(t, teamScheduleName(team1), tp.Name) - assert.Equal(t, fmt.Sprintf("team-%d", team1.ID), *tp.Type) - assert.Equal(t, []uint{team1.ID}, tp.TeamIDs) - - _, err = ds.EnsureTeamPack(context.Background(), team1.ID) - require.NoError(t, err) - - packs, err = ds.ListPacks(context.Background(), fleet.PackListOptions{IncludeSystemPacks: true}) - require.Nil(t, err) - assert.Len(t, packs, 1) - assert.Equal(t, tp.ID, packs[0].ID) - - team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2"}) - require.NoError(t, err) - - tp2, err := ds.EnsureTeamPack(context.Background(), team2.ID) - require.NoError(t, err) - - packs, err = ds.ListPacks(context.Background(), fleet.PackListOptions{IncludeSystemPacks: true}) - require.Nil(t, err) - assert.Len(t, packs, 2) - assert.Equal(t, tp.ID, packs[0].ID) - assert.Equal(t, tp2.ID, packs[1].ID) - - assert.Equal(t, fmt.Sprintf("team-%d", team2.ID), *tp2.Type) - assert.Equal(t, []uint{team2.ID}, tp2.TeamIDs) -} - -func testPacksTeamNameChangesTeamSchedule(t *testing.T, ds *Datastore) { - team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"}) - require.NoError(t, err) - - tp, err := ds.EnsureTeamPack(context.Background(), team1.ID) - require.NoError(t, err) - firstName := teamScheduleName(team1) - assert.Equal(t, firstName, tp.Name) - - team1.Name = "new name!!" - team1, err = ds.SaveTeam(context.Background(), team1) - require.NoError(t, err) - - tp, err = ds.EnsureTeamPack(context.Background(), team1.ID) - require.NoError(t, err) - assert.NotEqual(t, firstName, tp.Name) - assert.Equal(t, teamScheduleName(team1), tp.Name) -} - -func testPacksTeamScheduleNamesMigrateToNewFormat(t *testing.T, ds *Datastore) { - team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"}) - require.NoError(t, err) - - // insert team pack by hand with the old naming scheme - _, err = ds.writer(context.Background()).Exec( - "INSERT INTO packs(name, description, platform, disabled, pack_type) VALUES (?, ?, ?, ?, ?)", - teamSchedulePackType(team1), "desc", "windows", false, teamSchedulePackType(team1), - ) - require.NoError(t, err) - - tp, err := ds.EnsureTeamPack(context.Background(), team1.ID) - require.NoError(t, err) - require.Equal(t, teamSchedulePackType(team1), tp.Name) - - require.NoError(t, ds.MigrateData(context.Background())) - - tp, err = ds.EnsureTeamPack(context.Background(), team1.ID) - require.NoError(t, err) - require.NotEqual(t, teamSchedulePackType(team1), tp.Name) - require.Equal(t, teamScheduleName(team1), tp.Name) -} - func testPacksApplySpecFailsOnTargetIDNull(t *testing.T, ds *Datastore) { // Do not define queries mentioned in spec specs := []*fleet.PackSpec{ @@ -621,7 +498,7 @@ func testPacksApplyStatsNotLocking(t *testing.T, ds *Datastore) { require.NoError(t, err) amount := rand.Intn(5000) - require.NoError(t, saveHostPackStatsDB(context.Background(), ds.writer(context.Background()), host.ID, randomPackStatsForHost(pack.ID, pack.Name, *pack.Type, schedQueries, amount))) + require.NoError(t, saveHostPackStatsDB(context.Background(), ds.writer(context.Background()), host.TeamID, host.ID, randomPackStatsForHost(pack.ID, pack.Name, *pack.Type, schedQueries, amount))) } } }() @@ -673,7 +550,7 @@ func testPacksApplyStatsNotLockingTryTwo(t *testing.T, ds *Datastore) { require.NoError(t, err) amount := rand.Intn(5000) - require.NoError(t, saveHostPackStatsDB(context.Background(), ds.writer(context.Background()), host.ID, randomPackStatsForHost(pack.ID, pack.Name, *pack.Type, schedQueries, amount))) + require.NoError(t, saveHostPackStatsDB(context.Background(), ds.writer(context.Background()), host.TeamID, host.ID, randomPackStatsForHost(pack.ID, pack.Name, *pack.Type, schedQueries, amount))) } } }() @@ -683,3 +560,33 @@ func testPacksApplyStatsNotLockingTryTwo(t *testing.T, ds *Datastore) { cancelFunc() } + +func testListForHostIncludesOnlyUserPacks(t *testing.T, ds *Datastore) { + mockClock := clock.NewMockClock() + h1 := test.NewHost(t, ds, "h1.local", "10.10.10.1", "1", "1", mockClock.Now()) + ctx := context.Background() + + label := &fleet.LabelSpec{ + ID: 1, + Name: "All Hosts", + } + require.NoError(t, ds.ApplyLabelSpecs(ctx, []*fleet.LabelSpec{label})) + + pack := &fleet.PackSpec{ + ID: 1, + Name: "foo_pack", + Targets: fleet.PackSpecTargets{ + Labels: []string{ + label.Name, + }, + }, + } + require.NoError(t, ds.ApplyPackSpecs(ctx, []*fleet.PackSpec{pack})) + require.NoError(t, ds.RecordLabelQueryExecutions(ctx, h1, map[uint]*bool{label.ID: ptr.Bool(true)}, mockClock.Now(), false)) + + packs, err := ds.ListPacksForHost(ctx, h1.ID) + require.Nil(t, err) + if assert.Len(t, packs, 1) { + assert.Equal(t, "foo_pack", packs[0].Name) + } +} diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go index 2f51dc26c5..1c1caee114 100644 --- a/server/datastore/mysql/policies_test.go +++ b/server/datastore/mysql/policies_test.go @@ -101,7 +101,7 @@ func testPoliciesNewGlobalPolicyLegacy(t *testing.T, ds *Datastore) { assert.Equal(t, user1.ID, *policies[1].AuthorID) // The original query can be removed as the policy owns it's own query. - require.NoError(t, ds.DeleteQuery(context.Background(), q.Name)) + require.NoError(t, ds.DeleteQuery(context.Background(), nil, q.Name)) _, err = ds.DeleteGlobalPolicies(context.Background(), []uint{policies[0].ID, policies[1].ID}) require.NoError(t, err) diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index f1f488f7ab..1ea22ef2bf 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -36,15 +36,29 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] query, author_id, saved, - observer_can_run - ) VALUES ( ?, ?, ?, ?, true, ? ) + observer_can_run, + team_id, + team_id_char, + platform, + min_osquery_version, + schedule_interval, + automations_enabled, + logging_type + ) VALUES ( ?, ?, ?, ?, true, ?, ?, ?, ?, ?, ?, ?, ? ) ON DUPLICATE KEY UPDATE name = VALUES(name), description = VALUES(description), query = VALUES(query), author_id = VALUES(author_id), saved = VALUES(saved), - observer_can_run = VALUES(observer_can_run) + observer_can_run = VALUES(observer_can_run), + team_id = VALUES(team_id), + team_id_char = VALUES(team_id_char), + platform = VALUES(platform), + min_osquery_version = VALUES(min_osquery_version), + schedule_interval = VALUES(schedule_interval), + automations_enabled = VALUES(automations_enabled), + logging_type = VALUES(logging_type) ` stmt, err := tx.PrepareContext(ctx, sql) if err != nil { @@ -53,10 +67,24 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] defer stmt.Close() for _, q := range queries { - if q.Name == "" { - return ctxerr.New(ctx, "query name must not be empty") + if err := q.Verify(); err != nil { + return ctxerr.Wrap(ctx, err) } - _, err := stmt.ExecContext(ctx, q.Name, q.Description, q.Query, authorID, q.ObserverCanRun) + _, err := stmt.ExecContext( + ctx, + q.Name, + q.Description, + q.Query, + authorID, + q.ObserverCanRun, + q.TeamID, + q.TeamIDStr(), + q.Platform, + q.MinOsqueryVersion, + q.Interval, + q.AutomationsEnabled, + q.Logging, + ) if err != nil { return ctxerr.Wrap(ctx, err, "exec ApplyQueries insert") } @@ -66,14 +94,42 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] return ctxerr.Wrap(ctx, err, "commit ApplyQueries transaction") } -func (ds *Datastore) QueryByName(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { - sqlStatement := ` - SELECT * - FROM queries - WHERE name = ? +func (ds *Datastore) QueryByName( + ctx context.Context, + teamID *uint, + name string, + opts ...fleet.OptionalArg, +) (*fleet.Query, error) { + stmt := ` + SELECT + id, + team_id, + name, + description, + query, + author_id, + saved, + observer_can_run, + schedule_interval, + platform, + min_osquery_version, + automations_enabled, + logging_type, + created_at, + updated_at + FROM queries + WHERE name = ? ` + args := []interface{}{name} + whereClause := " AND team_id_char = ''" + if teamID != nil { + args = append(args, fmt.Sprint(*teamID)) + whereClause = " AND team_id_char = ?" + } + + stmt += whereClause var query fleet.Query - err := sqlx.GetContext(ctx, ds.reader(ctx), &query, sqlStatement, name) + err := sqlx.GetContext(ctx, ds.reader(ctx), &query, stmt, args...) if err != nil { if err == sql.ErrNoRows { return nil, ctxerr.Wrap(ctx, notFound("Query").WithName(name)) @@ -89,7 +145,11 @@ func (ds *Datastore) QueryByName(ctx context.Context, name string, opts ...fleet } // NewQuery creates a New Query. -func (ds *Datastore) NewQuery(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) { +func (ds *Datastore) NewQuery( + ctx context.Context, + query *fleet.Query, + opts ...fleet.OptionalArg, +) (*fleet.Query, error) { sqlStatement := ` INSERT INTO queries ( name, @@ -97,10 +157,33 @@ func (ds *Datastore) NewQuery(ctx context.Context, query *fleet.Query, opts ...f query, saved, author_id, - observer_can_run - ) VALUES ( ?, ?, ?, ?, ?, ? ) + observer_can_run, + team_id, + team_id_char, + platform, + min_osquery_version, + schedule_interval, + automations_enabled, + logging_type + ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ` - result, err := ds.writer(ctx).ExecContext(ctx, sqlStatement, query.Name, query.Description, query.Query, query.Saved, query.AuthorID, query.ObserverCanRun) + result, err := ds.writer(ctx).ExecContext( + ctx, + sqlStatement, + query.Name, + query.Description, + query.Query, + query.Saved, + query.AuthorID, + query.ObserverCanRun, + query.TeamID, + query.TeamIDStr(), + query.Platform, + query.MinOsqueryVersion, + query.Interval, + query.AutomationsEnabled, + query.Logging, + ) if err != nil && isDuplicate(err) { return nil, ctxerr.Wrap(ctx, alreadyExists("Query", query.Name)) @@ -118,10 +201,38 @@ func (ds *Datastore) NewQuery(ctx context.Context, query *fleet.Query, opts ...f func (ds *Datastore) SaveQuery(ctx context.Context, q *fleet.Query) error { sql := ` UPDATE queries - SET name = ?, description = ?, query = ?, author_id = ?, saved = ?, observer_can_run = ? - WHERE id = ? + SET name = ?, + description = ?, + query = ?, + author_id = ?, + saved = ?, + observer_can_run = ?, + team_id = ?, + team_id_char = ?, + platform = ?, + min_osquery_version = ?, + schedule_interval = ?, + automations_enabled = ?, + logging_type = ? + WHERE id = ? ` - result, err := ds.writer(ctx).ExecContext(ctx, sql, q.Name, q.Description, q.Query, q.AuthorID, q.Saved, q.ObserverCanRun, q.ID) + result, err := ds.writer(ctx).ExecContext( + ctx, + sql, + q.Name, + q.Description, + q.Query, + q.AuthorID, + q.Saved, + q.ObserverCanRun, + q.TeamID, + q.TeamIDStr(), + q.Platform, + q.MinOsqueryVersion, + q.Interval, + q.AutomationsEnabled, + q.Logging, + q.ID) if err != nil { return ctxerr.Wrap(ctx, err, "updating query") } @@ -136,9 +247,33 @@ func (ds *Datastore) SaveQuery(ctx context.Context, q *fleet.Query) error { return nil } -// DeleteQuery deletes Query identified by Query.ID. -func (ds *Datastore) DeleteQuery(ctx context.Context, name string) error { - return ds.deleteEntityByName(ctx, queriesTable, name) +func (ds *Datastore) DeleteQuery( + ctx context.Context, + teamID *uint, + name string, +) error { + stmt := "DELETE FROM queries WHERE name = ?" + + args := []interface{}{name} + whereClause := " AND team_id_char = ''" + if teamID != nil { + args = append(args, fmt.Sprint(*teamID)) + whereClause = " AND team_id_char = ?" + } + stmt += whereClause + + result, err := ds.writer(ctx).ExecContext(ctx, stmt, args...) + if err != nil { + if isMySQLForeignKey(err) { + return ctxerr.Wrap(ctx, foreignKey("queries", name)) + } + return ctxerr.Wrap(ctx, err, "delete queries") + } + rows, _ := result.RowsAffected() + if rows != 1 { + return ctxerr.Wrap(ctx, notFound("queries").WithName(name)) + } + return nil } // DeleteQueries deletes the existing query objects with the provided IDs. The @@ -150,14 +285,38 @@ func (ds *Datastore) DeleteQueries(ctx context.Context, ids []uint) (uint, error // Query returns a single Query identified by id, if such exists. func (ds *Datastore) Query(ctx context.Context, id uint) (*fleet.Query, error) { sqlQuery := ` - SELECT q.*, COALESCE(NULLIF(u.name, ''), u.email, '') AS author_name, COALESCE(u.email, '') AS author_email + SELECT + q.id, + q.team_id, + q.name, + q.description, + q.query, + q.author_id, + q.saved, + q.observer_can_run, + q.schedule_interval, + q.platform, + q.min_osquery_version, + q.automations_enabled, + q.logging_type, + q.created_at, + q.updated_at, + 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, + JSON_EXTRACT(json_value, '$.user_time_p95') as user_time_p95, + JSON_EXTRACT(json_value, '$.system_time_p50') as system_time_p50, + JSON_EXTRACT(json_value, '$.system_time_p95') as system_time_p95, + JSON_EXTRACT(json_value, '$.total_executions') as total_executions FROM queries q LEFT JOIN users u ON q.author_id = u.id + LEFT JOIN aggregated_stats ag + ON (ag.id = q.id AND ag.global_stats = ? AND ag.type = ?) WHERE q.id = ? ` query := &fleet.Query{} - if err := sqlx.GetContext(ctx, ds.reader(ctx), query, sqlQuery, id); err != nil { + if err := sqlx.GetContext(ctx, ds.reader(ctx), query, sqlQuery, false, aggregatedStatsTypeScheduledQuery, id); err != nil { if err == sql.ErrNoRows { return nil, ctxerr.Wrap(ctx, notFound("Query").WithID(id)) } @@ -176,27 +335,60 @@ func (ds *Datastore) Query(ctx context.Context, id uint) (*fleet.Query, error) { func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { sql := ` SELECT - q.*, - COALESCE(u.name, '') AS author_name, - COALESCE(u.email, '') AS author_email, - JSON_EXTRACT(json_value, '$.user_time_p50') as user_time_p50, - JSON_EXTRACT(json_value, '$.user_time_p95') as user_time_p95, - JSON_EXTRACT(json_value, '$.system_time_p50') as system_time_p50, - JSON_EXTRACT(json_value, '$.system_time_p95') as system_time_p95, - JSON_EXTRACT(json_value, '$.total_executions') as total_executions + q.id, + q.team_id, + q.name, + q.description, + q.query, + q.author_id, + q.saved, + q.observer_can_run, + q.schedule_interval, + q.platform, + q.min_osquery_version, + q.automations_enabled, + q.logging_type, + q.created_at, + q.updated_at, + COALESCE(u.name, '') AS author_name, + COALESCE(u.email, '') AS author_email, + JSON_EXTRACT(json_value, '$.user_time_p50') as user_time_p50, + JSON_EXTRACT(json_value, '$.user_time_p95') as user_time_p95, + JSON_EXTRACT(json_value, '$.system_time_p50') as system_time_p50, + JSON_EXTRACT(json_value, '$.system_time_p95') as system_time_p95, + JSON_EXTRACT(json_value, '$.total_executions') as total_executions FROM queries q LEFT JOIN users u ON (q.author_id = u.id) LEFT JOIN aggregated_stats ag ON (ag.id = q.id AND ag.global_stats = ? AND ag.type = ?) - WHERE saved = true ` + + args := []interface{}{false, aggregatedStatsTypeScheduledQuery} + whereClauses := "WHERE saved = true" + if opt.OnlyObserverCanRun { - sql += " AND q.observer_can_run=true" + whereClauses += " AND q.observer_can_run=true" } + + if opt.TeamID != nil { + args = append(args, *opt.TeamID) + whereClauses += " AND team_id = ?" + } else { + whereClauses += " AND team_id IS NULL" + } + + if opt.IsScheduled != nil { + if *opt.IsScheduled { + whereClauses += " AND (q.schedule_interval>0 AND q.automations_enabled=1)" + } else { + whereClauses += " AND (q.schedule_interval=0 OR q.automations_enabled=0)" + } + } + + sql += whereClauses sql = appendListOptionsToSQL(sql, &opt.ListOptions) results := []*fleet.Query{} - - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, false, aggregatedStatsTypeQuery); err != nil { + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, args...); err != nil { return nil, ctxerr.Wrap(ctx, err, "listing queries") } @@ -207,18 +399,19 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions return results, nil } -// loadPacksForQueries loads the packs associated with the provided queries +// loadPacksForQueries loads the user packs (aka 2017 packs) associated with the provided queries. func (ds *Datastore) loadPacksForQueries(ctx context.Context, queries []*fleet.Query) error { if len(queries) == 0 { return nil } + // packs.pack_type is NULL for user created packs (aka 2017 packs). sql := ` SELECT p.*, sq.query_name AS query_name FROM packs p JOIN scheduled_queries sq ON p.id = sq.pack_id - WHERE query_name IN (?) + WHERE query_name IN (?) AND p.pack_type IS NULL ` // Used to map the results @@ -268,3 +461,35 @@ 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 := ` + SELECT + q.name, + q.query, + q.team_id, + q.schedule_interval, + q.platform, + q.min_osquery_version, + q.automations_enabled, + q.logging_type + FROM queries q + WHERE q.saved = true + AND (q.schedule_interval > 0 AND q.automations_enabled = 1) + ` + + args := []interface{}{} + if teamID != nil { + args = append(args, *teamID) + sql += " AND team_id = ?" + } else { + sql += " AND team_id IS NULL" + } + + results := []*fleet.Query{} + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, args...); err != nil { + return nil, ctxerr.Wrap(ctx, err, "list scheduled queries for agents") + } + + return results, nil +} diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index 5814c00388..d1e67446ef 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,6 +30,9 @@ func TestQueries(t *testing.T) { {"DuplicateNew", testQueriesDuplicateNew}, {"ListFiltersObservers", testQueriesListFiltersObservers}, {"ObserverCanRunQuery", testObserverCanRunQuery}, + {"ListQueriesFiltersByTeamID", testListQueriesFiltersByTeamID}, + {"ListQueriesFiltersByIsScheduled", testListQueriesFiltersByIsScheduled}, + {"ListScheduledQueriesForAgents", testListScheduledQueriesForAgents}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -43,63 +47,94 @@ func testQueriesApply(t *testing.T, ds *Datastore) { zwass := test.NewUser(t, ds, "Zach", "zwass@fleet.co", true) groob := test.NewUser(t, ds, "Victor", "victor@fleet.co", true) + expectedQueries := []*fleet.Query{ - {Name: "foo", Description: "get the foos", Query: "select * from foo", ObserverCanRun: true}, - {Name: "bar", Description: "do some bars", Query: "select baz from bar"}, + { + Name: "foo", + Description: "get the foos", + Query: "select * from foo", + ObserverCanRun: true, + Interval: 10, + Platform: "macos", + MinOsqueryVersion: "5.2.1", + AutomationsEnabled: true, + Logging: "differential", + }, + { + Name: "bar", + Description: "do some bars", + Query: "select baz from bar", + }, } // Zach creates some queries err := ds.ApplyQueries(context.Background(), zwass.ID, expectedQueries) - require.Nil(t, err) + require.NoError(t, err) queries, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) - require.Nil(t, err) + require.NoError(t, err) require.Len(t, queries, len(expectedQueries)) - for i, q := range queries { - comp := expectedQueries[i] - assert.Equal(t, comp.Name, q.Name) - assert.Equal(t, comp.Description, q.Description) - assert.Equal(t, comp.Query, q.Query) - assert.Equal(t, &zwass.ID, q.AuthorID) - assert.Equal(t, comp.ObserverCanRun, q.ObserverCanRun) + + test.QueryElementsMatch(t, expectedQueries, queries) + + // Check all queries were authored by zwass + for _, q := range queries { + require.Equal(t, &zwass.ID, q.AuthorID) + require.Equal(t, zwass.Email, q.AuthorEmail) + require.Equal(t, zwass.Name, q.AuthorName) + require.True(t, q.Saved) } // 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) - require.Nil(t, err) + require.NoError(t, err) queries, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) - require.Nil(t, err) + require.NoError(t, err) require.Len(t, queries, len(expectedQueries)) - for i, q := range queries { - comp := expectedQueries[i] - assert.Equal(t, comp.Name, q.Name) - assert.Equal(t, comp.Description, q.Description) - assert.Equal(t, comp.Query, q.Query) + + test.QueryElementsMatch(t, expectedQueries, queries) + + // Check queries were authored by groob + for _, q := range queries { assert.Equal(t, &groob.ID, q.AuthorID) + require.Equal(t, groob.Email, q.AuthorEmail) + require.Equal(t, groob.Name, q.AuthorName) + require.True(t, q.Saved) } // Zach adds a third query (but does not re-apply the others) expectedQueries = append(expectedQueries, - &fleet.Query{Name: "trouble", Description: "Look out!", Query: "select * from time"}, + &fleet.Query{ + Name: "trouble", + Description: "Look out!", + Query: "select * from time", + }, ) err = ds.ApplyQueries(context.Background(), zwass.ID, []*fleet.Query{expectedQueries[2]}) - require.Nil(t, err) + require.NoError(t, err) queries, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) - require.Nil(t, err) + require.NoError(t, err) require.Len(t, queries, len(expectedQueries)) - for i, q := range queries { - comp := expectedQueries[i] - assert.Equal(t, comp.Name, q.Name) - assert.Equal(t, comp.Description, q.Description) - assert.Equal(t, comp.Query, q.Query) + + test.QueryElementsMatch(t, expectedQueries, queries) + + for _, q := range queries { + require.True(t, q.Saved) + switch q.Name { + case "foo", "bar": + require.Equal(t, &groob.ID, q.AuthorID) + require.Equal(t, groob.Email, q.AuthorEmail) + require.Equal(t, groob.Name, q.AuthorName) + default: + require.Equal(t, &zwass.ID, q.AuthorID) + require.Equal(t, zwass.Email, q.AuthorEmail) + require.Equal(t, zwass.Name, q.AuthorName) + } } - assert.Equal(t, &groob.ID, queries[0].AuthorID) - assert.Equal(t, &groob.ID, queries[1].AuthorID) - assert.Equal(t, &zwass.ID, queries[2].AuthorID) } func testQueriesDelete(t *testing.T, ds *Datastore) { @@ -111,38 +146,62 @@ func testQueriesDelete(t *testing.T, ds *Datastore) { AuthorID: &user.ID, } query, err := ds.NewQuery(context.Background(), query) - require.Nil(t, err) + require.NoError(t, err) require.NotNil(t, query) assert.NotEqual(t, query.ID, 0) - err = ds.DeleteQuery(context.Background(), query.Name) - require.Nil(t, err) + err = ds.DeleteQuery(context.Background(), query.TeamID, query.Name) + require.NoError(t, err) - assert.NotEqual(t, query.ID, 0) + require.NotEqual(t, query.ID, 0) _, err = ds.Query(context.Background(), query.ID) - assert.NotNil(t, err) + require.Error(t, err) + require.True(t, fleet.IsNotFound(err)) } func testQueriesGetByName(t *testing.T, ds *Datastore) { user := test.NewUser(t, ds, "Zach", "zwass@fleet.co", true) - test.NewQuery(t, ds, "q1", "select * from time", user.ID, true) - actual, err := ds.QueryByName(context.Background(), "q1") - require.Nil(t, err) - assert.Equal(t, "q1", actual.Name) - assert.Equal(t, "select * from time", actual.Query) - actual, err = ds.QueryByName(context.Background(), "xxx") - assert.Error(t, err) - assert.True(t, fleet.IsNotFound(err)) + // Test we can get global queries by name + globalQ := test.NewQuery(t, ds, nil, "q1", "select * from time", user.ID, true) + + actual, err := ds.QueryByName(context.Background(), nil, globalQ.Name) + require.NoError(t, err) + require.Nil(t, actual.TeamID) + require.Equal(t, "q1", actual.Name) + require.Equal(t, "select * from time", actual.Query) + + actual, err = ds.QueryByName(context.Background(), nil, "xxx") + require.Error(t, err) + require.True(t, fleet.IsNotFound(err)) + + // Test we can get queries in a team + teamRocket, err := ds.NewTeam(context.Background(), &fleet.Team{ + Name: "Team Rocket", + Description: "Something cheesy", + }) + require.NoError(t, err) + + teamRocketQ := test.NewQuery(t, ds, &teamRocket.ID, "q1", "select * from time", user.ID, true) + + actual, err = ds.QueryByName(context.Background(), &teamRocket.ID, teamRocketQ.Name) + require.NoError(t, err) + require.Equal(t, "q1", actual.Name) + require.Equal(t, teamRocket.ID, *actual.TeamID) + require.Equal(t, "select * from time", actual.Query) + + actual, err = ds.QueryByName(context.Background(), &teamRocket.ID, "xxx") + require.Error(t, err) + require.True(t, fleet.IsNotFound(err)) } func testQueriesDeleteMany(t *testing.T, ds *Datastore) { user := test.NewUser(t, ds, "Zach", "zwass@fleet.co", true) - q1 := test.NewQuery(t, ds, "q1", "select * from time", user.ID, true) - q2 := test.NewQuery(t, ds, "q2", "select * from processes", user.ID, true) - q3 := test.NewQuery(t, ds, "q3", "select 1", user.ID, true) - q4 := test.NewQuery(t, ds, "q4", "select * from osquery_info", user.ID, true) + q1 := test.NewQuery(t, ds, nil, "q1", "select * from time", user.ID, true) + q2 := test.NewQuery(t, ds, nil, "q2", "select * from processes", user.ID, true) + q3 := test.NewQuery(t, ds, nil, "q3", "select 1", user.ID, true) + q4 := test.NewQuery(t, ds, nil, "q4", "select * from osquery_info", user.ID, true) queries, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.Nil(t, err) @@ -182,23 +241,37 @@ func testQueriesSave(t *testing.T, ds *Datastore) { AuthorID: &user.ID, } query, err := ds.NewQuery(context.Background(), query) - require.Nil(t, err) + require.NoError(t, err) require.NotNil(t, query) - assert.NotEqual(t, 0, query.ID) + require.NotEqual(t, 0, query.ID) + + team, err := ds.NewTeam(context.Background(), &fleet.Team{ + Name: "some kind of nature", + Description: "some kind of goal", + }) + require.NoError(t, err) query.Query = "baz" query.ObserverCanRun = true + query.TeamID = &team.ID + query.Interval = 10 + query.Platform = "macos" + query.MinOsqueryVersion = "5.2.1" + query.AutomationsEnabled = true + query.Logging = "differential" + err = ds.SaveQuery(context.Background(), query) + require.NoError(t, err) - require.Nil(t, err) + actual, err := ds.Query(context.Background(), query.ID) + require.NoError(t, err) + require.NotNil(t, actual) - queryVerify, err := ds.Query(context.Background(), query.ID) - require.Nil(t, err) - require.NotNil(t, queryVerify) - assert.Equal(t, "baz", queryVerify.Query) - assert.Equal(t, "Zach", queryVerify.AuthorName) - assert.Equal(t, "zwass@fleet.co", queryVerify.AuthorEmail) - assert.True(t, queryVerify.ObserverCanRun) + test.QueriesMatch(t, actual, query) + + require.Equal(t, "baz", actual.Query) + require.Equal(t, "Zach", actual.AuthorName) + require.Equal(t, "zwass@fleet.co", actual.AuthorEmail) } func testQueriesList(t *testing.T, ds *Datastore) { @@ -227,20 +300,20 @@ func testQueriesList(t *testing.T, ds *Datastore) { results, err := ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 10, len(results)) - assert.Equal(t, "Zach", results[0].AuthorName) - assert.Equal(t, "zwass@fleet.co", results[0].AuthorEmail) + require.Equal(t, "Zach", results[0].AuthorName) + require.Equal(t, "zwass@fleet.co", results[0].AuthorEmail) idWithAgg := results[0].ID _, err = ds.writer(context.Background()).Exec( `INSERT INTO aggregated_stats(id,global_stats,type,json_value) VALUES (?,?,?,?)`, - idWithAgg, false, aggregatedStatsTypeQuery, `{"user_time_p50": 10.5777, "user_time_p95": 111.7308, "system_time_p50": 0.6936, "system_time_p95": 95.8654, "total_executions": 5038}`, + idWithAgg, false, aggregatedStatsTypeScheduledQuery, `{"user_time_p50": 10.5777, "user_time_p95": 111.7308, "system_time_p50": 0.6936, "system_time_p95": 95.8654, "total_executions": 5038}`, ) require.NoError(t, err) results, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) - assert.Equal(t, 10, len(results)) + require.Equal(t, 10, len(results)) foundAgg := false for _, q := range results { @@ -262,7 +335,7 @@ func testQueriesLoadPacksForQueries(t *testing.T, ds *Datastore) { {Name: "q2", Query: "select * from osquery_info"}, } err := ds.ApplyQueries(context.Background(), zwass.ID, queries) - require.Nil(t, err) + require.NoError(t, err) specs := []*fleet.PackSpec{ {Name: "p1"}, @@ -272,11 +345,11 @@ func testQueriesLoadPacksForQueries(t *testing.T, ds *Datastore) { err = ds.ApplyPackSpecs(context.Background(), specs) require.Nil(t, err) - q0, err := ds.QueryByName(context.Background(), queries[0].Name) + q0, err := ds.QueryByName(context.Background(), nil, queries[0].Name) require.Nil(t, err) assert.Empty(t, q0.Packs) - q1, err := ds.QueryByName(context.Background(), queries[1].Name) + q1, err := ds.QueryByName(context.Background(), nil, queries[1].Name) require.Nil(t, err) assert.Empty(t, q1.Packs) @@ -295,13 +368,13 @@ func testQueriesLoadPacksForQueries(t *testing.T, ds *Datastore) { err = ds.ApplyPackSpecs(context.Background(), specs) require.Nil(t, err) - q0, err = ds.QueryByName(context.Background(), queries[0].Name) + q0, err = ds.QueryByName(context.Background(), nil, queries[0].Name) require.Nil(t, err) if assert.Len(t, q0.Packs, 1) { assert.Equal(t, "p2", q0.Packs[0].Name) } - q1, err = ds.QueryByName(context.Background(), queries[1].Name) + q1, err = ds.QueryByName(context.Background(), nil, queries[1].Name) require.Nil(t, err) assert.Empty(t, q1.Packs) @@ -328,13 +401,13 @@ func testQueriesLoadPacksForQueries(t *testing.T, ds *Datastore) { err = ds.ApplyPackSpecs(context.Background(), specs) require.Nil(t, err) - q0, err = ds.QueryByName(context.Background(), queries[0].Name) + q0, err = ds.QueryByName(context.Background(), nil, queries[0].Name) require.Nil(t, err) if assert.Len(t, q0.Packs, 1) { assert.Equal(t, "p2", q0.Packs[0].Name) } - q1, err = ds.QueryByName(context.Background(), queries[1].Name) + q1, err = ds.QueryByName(context.Background(), nil, queries[1].Name) require.Nil(t, err) if assert.Len(t, q1.Packs, 2) { sort.Slice(q1.Packs, func(i, j int) bool { return q1.Packs[i].Name < q1.Packs[j].Name }) @@ -362,7 +435,7 @@ func testQueriesLoadPacksForQueries(t *testing.T, ds *Datastore) { err = ds.ApplyPackSpecs(context.Background(), specs) require.Nil(t, err) - q0, err = ds.QueryByName(context.Background(), queries[0].Name) + q0, err = ds.QueryByName(context.Background(), nil, queries[0].Name) require.Nil(t, err) if assert.Len(t, q0.Packs, 2) { sort.Slice(q0.Packs, func(i, j int) bool { return q0.Packs[i].Name < q0.Packs[j].Name }) @@ -370,7 +443,7 @@ func testQueriesLoadPacksForQueries(t *testing.T, ds *Datastore) { assert.Equal(t, "p3", q0.Packs[1].Name) } - q1, err = ds.QueryByName(context.Background(), queries[1].Name) + q1, err = ds.QueryByName(context.Background(), nil, queries[1].Name) require.Nil(t, err) if assert.Len(t, q1.Packs, 2) { sort.Slice(q1.Packs, func(i, j int) bool { return q1.Packs[i].Name < q1.Packs[j].Name }) @@ -381,22 +454,41 @@ func testQueriesLoadPacksForQueries(t *testing.T, ds *Datastore) { func testQueriesDuplicateNew(t *testing.T, ds *Datastore) { user := test.NewUser(t, ds, "Mike Arpaia", "mike@fleet.co", true) - q1, err := ds.NewQuery(context.Background(), &fleet.Query{ + + // The uniqueness of 'global' queries should be based on their name alone. + globalQ1, err := ds.NewQuery(context.Background(), &fleet.Query{ Name: "foo", Query: "select * from time;", AuthorID: &user.ID, }) - require.Nil(t, err) - assert.NotZero(t, q1.ID) - + require.NoError(t, err) + require.NotZero(t, globalQ1.ID) _, err = ds.NewQuery(context.Background(), &fleet.Query{ Name: "foo", Query: "select * from osquery_info;", }) + require.Contains(t, err.Error(), "already exists") - // Note that we can't do the actual type assertion here because existsError - // is private to the individual datastore implementations - assert.Contains(t, err.Error(), "already exists") + // Check uniqueness constraint on queries that belong to a team + team, err := ds.NewTeam(context.Background(), &fleet.Team{ + Name: "some kind of nature", + Description: "some kind of goal", + }) + require.NoError(t, err) + + _, err = ds.NewQuery(context.Background(), &fleet.Query{ + Name: "foo", + Query: "select * from osquery_info;", + TeamID: &team.ID, + }) + require.NoError(t, err) + + _, err = ds.NewQuery(context.Background(), &fleet.Query{ + Name: "foo", + Query: "select * from osquery_info;", + TeamID: &team.ID, + }) + require.Contains(t, err.Error(), "already exists") } func testQueriesListFiltersObservers(t *testing.T, ds *Datastore) { @@ -430,7 +522,7 @@ func testQueriesListFiltersObservers(t *testing.T, ds *Datastore) { ) require.NoError(t, err) require.Len(t, queries, 1) - assert.Equal(t, query3.ID, queries[0].ID) + require.Equal(t, query3.ID, queries[0].ID) } func testObserverCanRunQuery(t *testing.T, ds *Datastore) { @@ -463,3 +555,174 @@ func testObserverCanRunQuery(t *testing.T, ds *Datastore) { require.Equal(t, q.ObserverCanRun, canRun) } } + +func testListQueriesFiltersByTeamID(t *testing.T, ds *Datastore) { + globalQ1, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query1", + Query: "select 1;", + Saved: true, + }) + require.NoError(t, err) + globalQ2, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query2", + Query: "select 1;", + Saved: true, + }) + require.NoError(t, err) + globalQ3, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query3", + Query: "select 1;", + Saved: true, + }) + require.NoError(t, err) + + queries, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + require.NoError(t, err) + test.QueryElementsMatch(t, queries, []*fleet.Query{globalQ1, globalQ2, globalQ3}) + + team, err := ds.NewTeam(context.Background(), &fleet.Team{ + Name: "some kind of nature", + Description: "some kind of goal", + }) + require.NoError(t, err) + + teamQ1, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query1", + Query: "select 1;", + Saved: true, + TeamID: &team.ID, + }) + require.NoError(t, err) + teamQ2, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query2", + Query: "select 1;", + Saved: true, + TeamID: &team.ID, + }) + require.NoError(t, err) + teamQ3, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query3", + Query: "select 1;", + Saved: true, + TeamID: &team.ID, + }) + require.NoError(t, err) + + queries, err = ds.ListQueries( + context.Background(), + fleet.ListQueryOptions{ + TeamID: &team.ID, + }, + ) + require.NoError(t, err) + test.QueryElementsMatch(t, queries, []*fleet.Query{teamQ1, teamQ2, teamQ3}) +} + +func testListQueriesFiltersByIsScheduled(t *testing.T, ds *Datastore) { + q1, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query1", + Query: "select 1;", + Saved: true, + Interval: 0, + }) + require.NoError(t, err) + q2, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query2", + Query: "select 1;", + Saved: true, + Interval: 10, + AutomationsEnabled: false, + }) + require.NoError(t, err) + q3, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query3", + Query: "select 1;", + Saved: true, + Interval: 20, + AutomationsEnabled: true, + }) + require.NoError(t, err) + + testCases := []struct { + opts fleet.ListQueryOptions + expected []*fleet.Query + }{ + { + opts: fleet.ListQueryOptions{}, + expected: []*fleet.Query{q1, q2, q3}, + }, + { + opts: fleet.ListQueryOptions{IsScheduled: ptr.Bool(true)}, + expected: []*fleet.Query{q3}, + }, + { + opts: fleet.ListQueryOptions{IsScheduled: ptr.Bool(false)}, + expected: []*fleet.Query{q1, q2}, + }, + } + + for i, tCase := range testCases { + queries, err := ds.ListQueries( + context.Background(), + tCase.opts, + ) + require.NoError(t, err) + test.QueryElementsMatch(t, queries, tCase.expected, i) + } +} + +func testListScheduledQueriesForAgents(t *testing.T, ds *Datastore) { + ctx := context.Background() + + team, err := ds.NewTeam(context.Background(), &fleet.Team{ + Name: "Team 1", + Description: "Team 1", + }) + require.NoError(t, err) + + for i, teamID := range []*uint{nil, &team.ID} { + var teamIDStr string + 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) + _, err = ds.NewQuery(context.Background(), &fleet.Query{ + Name: fmt.Sprintf("%s query2", teamIDStr), + Query: "select 1;", + Saved: false, + Interval: 10, + AutomationsEnabled: false, + TeamID: teamID, + }) + require.NoError(t, err) + q3, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: fmt.Sprintf("%s query3", teamIDStr), + Query: "select 1;", + Saved: true, + Interval: 20, + AutomationsEnabled: true, + TeamID: teamID, + }) + require.NoError(t, err) + _, err = ds.NewQuery(context.Background(), &fleet.Query{ + Name: fmt.Sprintf("%s query4", teamIDStr), + Query: "select 1;", + Saved: true, + Interval: 0, + AutomationsEnabled: true, + TeamID: teamID, + }) + require.NoError(t, err) + + result, err := ds.ListScheduledQueriesForAgents(ctx, teamID) + require.NoError(t, err) + test.QueryElementsMatch(t, result, []*fleet.Query{q3}, i) + } +} diff --git a/server/datastore/mysql/scheduled_queries.go b/server/datastore/mysql/scheduled_queries.go index a6860ddd4b..c4cb5dd89d 100644 --- a/server/datastore/mysql/scheduled_queries.go +++ b/server/datastore/mysql/scheduled_queries.go @@ -37,7 +37,7 @@ func (ds *Datastore) ListScheduledQueriesInPackWithStats(ctx context.Context, id JSON_EXTRACT(ag.json_value, '$.system_time_p95') as system_time_p95, JSON_EXTRACT(ag.json_value, '$.total_executions') as total_executions FROM scheduled_queries sq - JOIN queries q ON (sq.query_name = q.name) + JOIN (SELECT * FROM queries WHERE team_id IS NULL) q ON (sq.query_name = q.name) LEFT JOIN aggregated_stats ag ON (ag.id = sq.id AND ag.global_stats = ? AND ag.type = ?) WHERE sq.pack_id = ? ` @@ -275,37 +275,33 @@ func (ds *Datastore) AsyncBatchSaveHostsScheduledQueryStats(ctx context.Context, // in SaveHostPackStats (in hosts.go) - that is, the behaviour per host must // be the same. - const ( - stmt = ` - INSERT INTO scheduled_query_stats ( - host_id, - scheduled_query_id, - average_memory, - denylisted, - executions, - schedule_interval, - last_executed, - output_size, - system_time, - user_time, - wall_time - ) - VALUES %s - ON DUPLICATE KEY UPDATE - average_memory = VALUES(average_memory), - denylisted = VALUES(denylisted), - executions = VALUES(executions), - schedule_interval = VALUES(schedule_interval), - last_executed = VALUES(last_executed), - output_size = VALUES(output_size), - system_time = VALUES(system_time), - user_time = VALUES(user_time), - wall_time = VALUES(wall_time) -` - - values = `(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),` - ) - + stmt := ` + INSERT IGNORE INTO scheduled_query_stats ( + scheduled_query_id, + host_id, + average_memory, + denylisted, + executions, + schedule_interval, + last_executed, + output_size, + system_time, + user_time, + wall_time + ) + VALUES %s ON DUPLICATE KEY UPDATE + scheduled_query_id = VALUES(scheduled_query_id), + host_id = VALUES(host_id), + average_memory = VALUES(average_memory), + denylisted = VALUES(denylisted), + executions = VALUES(executions), + schedule_interval = VALUES(schedule_interval), + last_executed = VALUES(last_executed), + output_size = VALUES(output_size), + system_time = VALUES(system_time), + user_time = VALUES(user_time), + wall_time = VALUES(wall_time); + ` var countExecs int // inserting sorted by host id (the first key in the PK) apparently helps @@ -318,40 +314,143 @@ func (ds *Datastore) AsyncBatchSaveHostsScheduledQueryStats(ctx context.Context, return hostIDs[i] < hostIDs[j] }) - var batchArgs []interface{} var batchCount int + + var ( + userPacksArgs []interface{} + userPacksQueryCount = 0 + scheduledQueriesArgs []interface{} + scheduledQueriesQueryCount = 0 + ) + for _, hostID := range hostIDs { hostStats := stats[hostID] for _, stat := range hostStats { - batchArgs = append(batchArgs, - hostID, - stat.ScheduledQueryID, - stat.AverageMemory, - stat.Denylisted, - stat.Executions, - stat.Interval, - stat.LastExecuted, - stat.OutputSize, - stat.SystemTime, - stat.UserTime, - stat.WallTime) - batchCount++ + // Stats for 'new' query structure + if stat.PackName == "Global" || strings.HasPrefix(stat.PackName, "team-") { + scheduledQueriesQueryCount++ + // Get the team id embedded in the pack name + var teamID int + statTeamID, err := stat.TeamID() + if err != nil { + return 0, err + } + if statTeamID != nil { + teamID = *statTeamID + } + + scheduledQueriesArgs = append(scheduledQueriesArgs, + teamID, + stat.QueryName, + hostID, + stat.AverageMemory, + stat.Denylisted, + stat.Executions, + stat.Interval, + stat.LastExecuted, + stat.OutputSize, + stat.SystemTime, + stat.UserTime, + stat.WallTime, + ) + } else { // stats for a 2017 pack + userPacksQueryCount++ + + userPacksArgs = append(userPacksArgs, + stat.PackName, + stat.ScheduledQueryName, + hostID, + stat.AverageMemory, + stat.Denylisted, + stat.Executions, + stat.Interval, + stat.LastExecuted, + stat.OutputSize, + stat.SystemTime, + stat.UserTime, + stat.WallTime, + ) + } + + batchCount++ if batchCount >= batchSize { - stmt := fmt.Sprintf(stmt, strings.TrimSuffix(strings.Repeat(values, batchCount), ",")) + var values []string + batchArgs := make([]interface{}, 0, scheduledQueriesQueryCount+userPacksQueryCount) + + if scheduledQueriesQueryCount > 0 { + values = append(values, + strings.TrimSuffix( + strings.Repeat( + "((SELECT q.id FROM queries q WHERE COALESCE(q.team_id, 0) = ? AND q.name = ?),?,?,?,?,?,?,?,?,?,?),", + scheduledQueriesQueryCount, + ), + ",", + ), + ) + batchArgs = append(batchArgs, scheduledQueriesArgs...) + } + if userPacksQueryCount > 0 { + values = append(values, + strings.TrimSuffix( + strings.Repeat( + "((SELECT sq.query_id FROM scheduled_queries sq JOIN packs p ON (sq.pack_id = p.id) WHERE p.pack_type IS NULL AND p.name = ? AND sq.name = ?),?,?,?,?,?,?,?,?,?,?),", + userPacksQueryCount, + ), + ",", + ), + ) + batchArgs = append(batchArgs, userPacksArgs...) + } + stmt := fmt.Sprintf(stmt, strings.Join(values, ",")) + if _, err := ds.writer(ctx).ExecContext(ctx, stmt, batchArgs...); err != nil { return countExecs, ctxerr.Wrap(ctx, err, "insert batch of scheduled query stats") } + countExecs++ - batchArgs = batchArgs[:0] + + scheduledQueriesArgs = scheduledQueriesArgs[:0] + userPacksArgs = userPacksArgs[:0] + batchCount = 0 + scheduledQueriesQueryCount = 0 + userPacksQueryCount = 0 } } } if batchCount > 0 { - stmt := fmt.Sprintf(stmt, strings.TrimSuffix(strings.Repeat(values, batchCount), ",")) + var values []string + batchArgs := make([]interface{}, 0, scheduledQueriesQueryCount+userPacksQueryCount) + + if scheduledQueriesQueryCount > 0 { + values = append(values, + strings.TrimSuffix( + strings.Repeat( + "((SELECT q.id FROM queries q WHERE COALESCE(q.team_id, 0) = ? AND q.name = ?),?,?,?,?,?,?,?,?,?,?),", + scheduledQueriesQueryCount, + ), + ",", + ), + ) + batchArgs = append(batchArgs, scheduledQueriesArgs...) + } + if userPacksQueryCount > 0 { + values = append(values, + strings.TrimSuffix( + strings.Repeat( + "((SELECT sq.query_id FROM scheduled_queries sq JOIN packs p ON (sq.pack_id = p.id) WHERE p.pack_type IS NULL AND p.name = ? AND sq.name = ?),?,?,?,?,?,?,?,?,?,?),", + userPacksQueryCount, + ), + ",", + ), + ) + batchArgs = append(batchArgs, userPacksArgs...) + } + stmt := fmt.Sprintf(stmt, strings.Join(values, ",")) + if _, err := ds.writer(ctx).ExecContext(ctx, stmt, batchArgs...); err != nil { return countExecs, ctxerr.Wrap(ctx, err, "insert batch of scheduled query stats") } diff --git a/server/datastore/mysql/scheduled_queries_test.go b/server/datastore/mysql/scheduled_queries_test.go index 6bff759664..9231587f99 100644 --- a/server/datastore/mysql/scheduled_queries_test.go +++ b/server/datastore/mysql/scheduled_queries_test.go @@ -266,7 +266,7 @@ func testScheduledQueriesListInPack(t *testing.T, ds *Datastore) { func testScheduledQueriesNew(t *testing.T, ds *Datastore) { u1 := test.NewUser(t, ds, "Admin", "admin@fleet.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) + q1 := test.NewQuery(t, ds, nil, "foo", "select * from time;", u1.ID, true) p1 := test.NewPack(t, ds, "baz") query, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ @@ -282,7 +282,7 @@ func testScheduledQueriesNew(t *testing.T, ds *Datastore) { func testScheduledQueriesGet(t *testing.T, ds *Datastore) { u1 := test.NewUser(t, ds, "Admin", "admin@fleet.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) + q1 := test.NewQuery(t, ds, nil, "foo", "select * from time;", u1.ID, true) p1 := test.NewPack(t, ds, "baz") sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false, "") @@ -306,7 +306,7 @@ func testScheduledQueriesGet(t *testing.T, ds *Datastore) { func testScheduledQueriesDelete(t *testing.T, ds *Datastore) { u1 := test.NewUser(t, ds, "Admin", "admin@fleet.co", true) - q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) + q1 := test.NewQuery(t, ds, nil, "foo", "select * from time;", u1.ID, true) p1 := test.NewPack(t, ds, "baz") sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false, "") @@ -362,7 +362,7 @@ func testScheduledQueriesCascadingDelete(t *testing.T, ds *Datastore) { require.Nil(t, err) require.Len(t, gotQueries, 3) - err = ds.DeleteQuery(context.Background(), queries[1].Name) + err = ds.DeleteQuery(context.Background(), nil, queries[1].Name) require.Nil(t, err) gotQueries, err = ds.ListScheduledQueriesInPackWithStats(context.Background(), 1, fleet.ListOptions{}) @@ -492,10 +492,10 @@ func testScheduledQueriesAsyncBatchSaveStats(t *testing.T, ds *Datastore) { p2 := test.NewPack(t, ds, "p2") p3 := test.NewPack(t, ds, "p3") - q1 := test.NewQuery(t, ds, "q1", "select 1", user.ID, true) - q2 := test.NewQuery(t, ds, "q2", "select 2", user.ID, true) - q3 := test.NewQuery(t, ds, "q3", "select 3", user.ID, true) - q4 := test.NewQuery(t, ds, "q4", "select 4", user.ID, true) + q1 := test.NewQuery(t, ds, nil, "q1", "select 1", user.ID, true) + q2 := test.NewQuery(t, ds, nil, "q2", "select 2", user.ID, true) + q3 := test.NewQuery(t, ds, nil, "q3", "select 3", user.ID, true) + q4 := test.NewQuery(t, ds, nil, "q4", "select 4", user.ID, true) sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false, "sq1") sq2 := test.NewScheduledQuery(t, ds, p2.ID, q2.ID, 60, false, false, "sq2") @@ -529,7 +529,13 @@ func testScheduledQueriesAsyncBatchSaveStats(t *testing.T, ds *Datastore) { // single host, single stat m := map[uint][]fleet.ScheduledQueryStats{ h1.ID: { - {ScheduledQueryID: sq1.ID, Executions: 1, LastExecuted: lastExec}, + { + ScheduledQueryID: sq1.ID, + Executions: 1, + LastExecuted: lastExec, + PackName: p1.Name, + ScheduledQueryName: sq1.Name, + }, }, } execs, err = ds.AsyncBatchSaveHostsScheduledQueryStats(ctx, m, batchSize) @@ -540,8 +546,20 @@ func testScheduledQueriesAsyncBatchSaveStats(t *testing.T, ds *Datastore) { // single host, stats == batch size m = map[uint][]fleet.ScheduledQueryStats{ h1.ID: { - {ScheduledQueryID: sq1.ID, Executions: 2, LastExecuted: lastExec}, - {ScheduledQueryID: sq2.ID, Executions: 3, LastExecuted: lastExec}, + { + ScheduledQueryID: sq1.ID, + Executions: 2, + LastExecuted: lastExec, + PackName: p1.Name, + ScheduledQueryName: sq1.Name, + }, + { + ScheduledQueryID: sq2.ID, + Executions: 3, + LastExecuted: lastExec, + PackName: p2.Name, + ScheduledQueryName: sq2.Name, + }, }, } execs, err = ds.AsyncBatchSaveHostsScheduledQueryStats(ctx, m, batchSize) @@ -552,9 +570,27 @@ func testScheduledQueriesAsyncBatchSaveStats(t *testing.T, ds *Datastore) { // single host, stats > batch size m = map[uint][]fleet.ScheduledQueryStats{ h1.ID: { - {ScheduledQueryID: sq1.ID, Executions: 4, LastExecuted: lastExec}, - {ScheduledQueryID: sq2.ID, Executions: 5, LastExecuted: lastExec}, - {ScheduledQueryID: sq3.ID, Executions: 6, LastExecuted: lastExec}, + { + ScheduledQueryID: sq1.ID, + Executions: 4, + LastExecuted: lastExec, + PackName: p1.Name, + ScheduledQueryName: sq1.Name, + }, + { + ScheduledQueryID: sq2.ID, + Executions: 5, + LastExecuted: lastExec, + PackName: p2.Name, + ScheduledQueryName: sq2.Name, + }, + { + ScheduledQueryID: sq3.ID, + Executions: 6, + LastExecuted: lastExec, + PackName: p3.Name, + ScheduledQueryName: sq3.Name, + }, }, } execs, err = ds.AsyncBatchSaveHostsScheduledQueryStats(ctx, m, batchSize) @@ -565,10 +601,22 @@ func testScheduledQueriesAsyncBatchSaveStats(t *testing.T, ds *Datastore) { // multi host, stats == batch size m = map[uint][]fleet.ScheduledQueryStats{ h1.ID: { - {ScheduledQueryID: sq1.ID, Executions: 7, LastExecuted: lastExec}, + { + ScheduledQueryID: sq1.ID, + Executions: 7, + LastExecuted: lastExec, + PackName: p1.Name, + ScheduledQueryName: sq1.Name, + }, }, h2.ID: { - {ScheduledQueryID: sq2.ID, Executions: 8, LastExecuted: lastExec}, + { + ScheduledQueryID: sq2.ID, + Executions: 8, + LastExecuted: lastExec, + PackName: p2.Name, + ScheduledQueryName: sq2.Name, + }, }, } execs, err = ds.AsyncBatchSaveHostsScheduledQueryStats(ctx, m, batchSize) @@ -579,11 +627,29 @@ func testScheduledQueriesAsyncBatchSaveStats(t *testing.T, ds *Datastore) { // multi host, stats > batch size m = map[uint][]fleet.ScheduledQueryStats{ h1.ID: { - {ScheduledQueryID: sq1.ID, Executions: 9, LastExecuted: lastExec}, + { + ScheduledQueryID: sq1.ID, + Executions: 9, + LastExecuted: lastExec, + PackName: p1.Name, + ScheduledQueryName: sq1.Name, + }, }, h2.ID: { - {ScheduledQueryID: sq2.ID, Executions: 10, LastExecuted: lastExec}, - {ScheduledQueryID: sq3.ID, Executions: 11, LastExecuted: lastExec}, + { + ScheduledQueryID: sq2.ID, + Executions: 10, + LastExecuted: lastExec, + PackName: p2.Name, + ScheduledQueryName: sq2.Name, + }, + { + ScheduledQueryID: sq3.ID, + Executions: 11, + LastExecuted: lastExec, + PackName: p3.Name, + ScheduledQueryName: sq3.Name, + }, }, } execs, err = ds.AsyncBatchSaveHostsScheduledQueryStats(ctx, m, batchSize) @@ -594,17 +660,59 @@ func testScheduledQueriesAsyncBatchSaveStats(t *testing.T, ds *Datastore) { // multi host, stats > (N * batch size) m = map[uint][]fleet.ScheduledQueryStats{ h1.ID: { - {ScheduledQueryID: sq1.ID, Executions: 12, LastExecuted: lastExec}, - {ScheduledQueryID: sq2.ID, Executions: 13, LastExecuted: lastExec}, + { + ScheduledQueryID: sq1.ID, + Executions: 12, + LastExecuted: lastExec, + PackName: p1.Name, + ScheduledQueryName: sq1.Name, + }, + { + ScheduledQueryID: sq2.ID, + Executions: 13, + LastExecuted: lastExec, + PackName: p2.Name, + ScheduledQueryName: sq2.Name, + }, }, h2.ID: { - {ScheduledQueryID: sq2.ID, Executions: 14, LastExecuted: lastExec}, - {ScheduledQueryID: sq4.ID, Executions: 15, LastExecuted: lastExec}, + { + ScheduledQueryID: sq2.ID, + Executions: 14, + LastExecuted: lastExec, + PackName: p2.Name, + ScheduledQueryName: sq2.Name, + }, + { + ScheduledQueryID: sq4.ID, + Executions: 15, + LastExecuted: lastExec, + PackName: p3.Name, + ScheduledQueryName: sq4.Name, + }, }, h3.ID: { - {ScheduledQueryID: sq1.ID, Executions: 16, LastExecuted: lastExec}, - {ScheduledQueryID: sq2.ID, Executions: 17, LastExecuted: lastExec}, - {ScheduledQueryID: sq3.ID, Executions: 18, LastExecuted: lastExec}, + { + ScheduledQueryID: sq1.ID, + Executions: 16, + LastExecuted: lastExec, + PackName: p1.Name, + ScheduledQueryName: sq1.Name, + }, + { + ScheduledQueryID: sq2.ID, + Executions: 17, + LastExecuted: lastExec, + PackName: p2.Name, + ScheduledQueryName: sq2.Name, + }, + { + ScheduledQueryID: sq3.ID, + Executions: 18, + LastExecuted: lastExec, + PackName: p3.Name, + ScheduledQueryName: sq3.Name, + }, }, } execs, err = ds.AsyncBatchSaveHostsScheduledQueryStats(ctx, m, batchSize) diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 863960ac41..289ddd33a2 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -661,9 +661,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=198 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=201 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'); +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'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( @@ -1017,11 +1017,20 @@ CREATE TABLE `queries` ( `query` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, `author_id` int(10) unsigned DEFAULT NULL, `observer_can_run` tinyint(1) NOT NULL DEFAULT '0', + `team_id` int(10) unsigned DEFAULT NULL, + `team_id_char` char(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `platform` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `min_osquery_version` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `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', PRIMARY KEY (`id`), - UNIQUE KEY `idx_query_unique_name` (`name`), - UNIQUE KEY `constraint_query_name_unique` (`name`), + UNIQUE KEY `idx_team_id_name_unq` (`team_id_char`,`name`), + UNIQUE KEY `idx_name_team_id_unq` (`name`,`team_id_char`), KEY `author_id` (`author_id`), - CONSTRAINT `queries_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL + KEY `idx_team_id_saved_auto_interval` (`team_id`,`saved`,`automations_enabled`,`schedule_interval`), + CONSTRAINT `queries_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, + CONSTRAINT `queries_ibfk_2` FOREIGN KEY (`team_id`) REFERENCES `teams` (`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 */; @@ -1065,12 +1074,14 @@ CREATE TABLE `scheduled_queries` ( `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `description` varchar(1023) COLLATE utf8mb4_unicode_ci DEFAULT '', `denylist` tinyint(1) DEFAULT NULL, + `team_id_char` char(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `unique_names_in_packs` (`name`,`pack_id`), KEY `scheduled_queries_pack_id` (`pack_id`), KEY `scheduled_queries_query_name` (`query_name`), - CONSTRAINT `scheduled_queries_pack_id` FOREIGN KEY (`pack_id`) REFERENCES `packs` (`id`) ON DELETE CASCADE, - CONSTRAINT `scheduled_queries_query_name` FOREIGN KEY (`query_name`) REFERENCES `queries` (`name`) ON DELETE CASCADE ON UPDATE CASCADE + KEY `fk_scheduled_queries_queries` (`team_id_char`,`query_name`), + CONSTRAINT `scheduled_queries_ibfk_1` FOREIGN KEY (`team_id_char`, `query_name`) REFERENCES `queries` (`team_id_char`, `name`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `scheduled_queries_pack_id` FOREIGN KEY (`pack_id`) REFERENCES `packs` (`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 */; diff --git a/server/datastore/mysql/teams.go b/server/datastore/mysql/teams.go index 1c34899c79..d4ab38ff74 100644 --- a/server/datastore/mysql/teams.go +++ b/server/datastore/mysql/teams.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "encoding/json" - "errors" "fmt" "strings" @@ -102,11 +101,6 @@ func (ds *Datastore) DeleteTeam(ctx context.Context, tid uint) error { return ctxerr.Wrapf(ctx, err, "deleting pack_targets for team %d", tid) } - _, err = tx.ExecContext(ctx, `DELETE FROM packs WHERE pack_type=?`, teamSchedulePackTypeByID(tid)) - if err != nil { - return ctxerr.Wrapf(ctx, err, "deleting team global packs for team %d", tid) - } - _, err = tx.ExecContext(ctx, `DELETE FROM mdm_apple_configuration_profiles WHERE team_id=?`, tid) if err != nil { return ctxerr.Wrapf(ctx, err, "deleting mdm_apple_configuration_profiles for team %d", tid) @@ -124,7 +118,7 @@ func (ds *Datastore) TeamByName(ctx context.Context, name string) (*fleet.Team, team := &fleet.Team{} if err := sqlx.GetContext(ctx, ds.reader(ctx), team, stmt, name); err != nil { - if errors.Is(err, sql.ErrNoRows) { + if err == sql.ErrNoRows { return nil, ctxerr.Wrap(ctx, notFound("Team").WithName(name)) } return nil, ctxerr.Wrap(ctx, err, "select team") @@ -233,8 +227,7 @@ WHERE if err := saveUsersForTeamDB(ctx, tx, team); err != nil { return err } - - return updateTeamScheduleDB(ctx, tx, team) + return nil }) if err != nil { return nil, err @@ -242,13 +235,6 @@ WHERE return team, nil } -func updateTeamScheduleDB(ctx context.Context, exec sqlx.ExecerContext, team *fleet.Team) error { - _, err := exec.ExecContext(ctx, - `UPDATE packs SET name = ? WHERE pack_type = ?`, teamScheduleName(team), teamSchedulePackType(team), - ) - return ctxerr.Wrap(ctx, err, "update packs") -} - // ListTeams lists all teams with limit, sort and offset passed in with // fleet.ListOptions func (ds *Datastore) ListTeams(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) { diff --git a/server/datastore/mysql/teams_test.go b/server/datastore/mysql/teams_test.go index 7b46c80b51..47273c9ea3 100644 --- a/server/datastore/mysql/teams_test.go +++ b/server/datastore/mysql/teams_test.go @@ -31,7 +31,6 @@ func TestTeams(t *testing.T) { {"Search", testTeamsSearch}, {"EnrollSecrets", testTeamsEnrollSecrets}, {"TeamAgentOptions", testTeamsAgentOptions}, - {"TeamsDeleteRename", testTeamsDeleteRename}, {"DeleteIntegrationsFromTeams", testTeamsDeleteIntegrationsFromTeams}, {"TeamsFeatures", testTeamsFeatures}, {"TeamsMDMConfig", testTeamsMDMConfig}, @@ -106,35 +105,6 @@ func testTeamsGetSetDelete(t *testing.T, ds *Datastore) { } } -func testTeamsDeleteRename(t *testing.T, ds *Datastore) { - team, err := ds.NewTeam(context.Background(), &fleet.Team{ - Name: t.Name(), - Description: t.Name() + "desc", - }) - require.NoError(t, err) - assert.NotZero(t, team.ID) - - team2, err := ds.NewTeam(context.Background(), &fleet.Team{ - Name: t.Name() + "2", - Description: t.Name() + "desc 2", - }) - require.NoError(t, err) - assert.NotZero(t, team2.ID) - - _, err = ds.EnsureTeamPack(context.Background(), team.ID) - require.NoError(t, err) - - err = ds.DeleteTeam(context.Background(), team.ID) - require.NoError(t, err) - - team2.Name = t.Name() - _, err = ds.SaveTeam(context.Background(), team2) - require.NoError(t, err) - - _, err = ds.EnsureTeamPack(context.Background(), team2.ID) - require.NoError(t, err) -} - func testTeamsUsers(t *testing.T, ds *Datastore) { users := createTestUsers(t, ds) user1 := fleet.User{Name: users[0].Name, Email: users[0].Email, ID: users[0].ID} diff --git a/server/datastore/mysql/testing_utils.go b/server/datastore/mysql/testing_utils.go index 23f6b85734..feb1722fe1 100644 --- a/server/datastore/mysql/testing_utils.go +++ b/server/datastore/mysql/testing_utils.go @@ -103,6 +103,11 @@ func setupReadReplica(t testing.TB, testName string, ds *Datastore, opts *Datast for _, fk := range fks { stmt := fmt.Sprintf(`ALTER TABLE %s.%s DROP FOREIGN KEY %s`, replicaDB, fk.TableName, fk.ConstraintName) _, err := replica.ExecContext(ctx, stmt) + // If the FK was already removed do nothing + if err != nil && strings.Contains(err.Error(), "check that column/key exists") { + continue + } + require.NoError(t, err) } diff --git a/server/fleet/app.go b/server/fleet/app.go index c73ff70778..6a77190739 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -762,6 +762,11 @@ func (l ListOptions) UsesCursorPagination() bool { type ListQueryOptions struct { ListOptions + // TeamID which team the queries belong to. If teamID is nil, then it is assumed the 'global' + // team. + TeamID *uint + // IsScheduled filters queries that are meant to run at a set interval. + IsScheduled *bool OnlyObserverCanRun bool } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 4c62c316d5..70639dcbb3 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -67,13 +67,13 @@ 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 - // 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 - // DeleteQuery deletes an existing query object. - DeleteQuery(ctx context.Context, name string) 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 // DeleteQueries deletes the existing query objects with the provided IDs. The number of deleted queries is returned // along with any error. DeleteQueries(ctx context.Context, ids []uint) (uint, error) @@ -82,8 +82,12 @@ type Datastore interface { // ListQueries returns a list of queries with the provided sorting and paging options. Associated packs should also // be loaded. ListQueries(ctx context.Context, opt ListQueryOptions) ([]*Query, error) - // QueryByName looks up a query by name. - QueryByName(ctx context.Context, name string, opts ...OptionalArg) (*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) + // 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) @@ -139,15 +143,9 @@ type Datastore interface { // PackByName fetches pack if it exists, if the pack exists the bool return value is true PackByName(ctx context.Context, name string, opts ...OptionalArg) (*Pack, bool, error) - // ListPacksForHost lists the packs that a host should execute. + // ListPacksForHost lists the "user packs" that a host should execute. ListPacksForHost(ctx context.Context, hid uint) (packs []*Pack, err error) - // EnsureGlobalPack gets or inserts a pack with type global - EnsureGlobalPack(ctx context.Context) (*Pack, error) - - // EnsureTeamPack gets or inserts a pack with type global - EnsureTeamPack(ctx context.Context, teamID uint) (*Pack, error) - /////////////////////////////////////////////////////////////////////////////// // LabelStore @@ -586,7 +584,6 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // Aggregated Stats - UpdateScheduledQueryAggregatedStats(ctx context.Context) error UpdateQueryAggregatedStats(ctx context.Context) error /////////////////////////////////////////////////////////////////////////////// @@ -625,7 +622,7 @@ type Datastore interface { TeamMDMConfig(ctx context.Context, teamID uint) (*TeamMDM, error) // SaveHostPackStats stores (and updates) the pack's scheduled queries stats of a host. - SaveHostPackStats(ctx context.Context, hostID uint, stats []PackStats) error + SaveHostPackStats(ctx context.Context, teamID *uint, hostID uint, stats []PackStats) error // AsyncBatchSaveHostsScheduledQueryStats efficiently saves a batch of hosts' // pack stats of scheduled queries. It is the async and batch version of // SaveHostPackStats. It returns the number of INSERT-ON DUPLICATE UPDATE diff --git a/server/fleet/queries.go b/server/fleet/queries.go index 025543c35a..7a6d515635 100644 --- a/server/fleet/queries.go +++ b/server/fleet/queries.go @@ -4,20 +4,71 @@ import ( "errors" "fmt" "strings" + "time" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/ghodss/yaml" ) +// QueryPayload is the payload used to create and modify queries. +// +// Fields are pointers to allow omitting fields when modifying existing queries. type QueryPayload struct { - Name *string - Description *string - Query *string + // Name is the name to set to the query. + Name *string `json:"name"` + // Description is the description of the query. + Description *string `json:"description"` + // Query is the actual SQL query to run on devices. + Query *string `json:"query"` + // ObserverCanRun is set to false if not set when creating a query. ObserverCanRun *bool `json:"observer_can_run"` + // TeamID is only used when creating a query. When modifying a query + // TeamID is ignored. + TeamID *uint `json:"team_id"` + // Interval is the interval to set on the query. If not set when creating + // a query, then the default value 0 is set on the query. + Interval *uint `json:"interval"` + // Platform is set to empty if not set when creating a query. + Platform *string `json:"platform"` + // MinOsqueryVersion is set to empty if not set when creating a query. + MinOsqueryVersion *string `json:"min_osquery_version"` + // AutomationsEnabled is set to false if not set when creating a query. + AutomationsEnabled *bool `json:"automations_enabled"` + // Logging is set to "snapshot" if not set when creating a query. + Logging *string `json:"logging"` } +// Query represents a osquery query to run on devices. +// +// - If Interval is 0 or AutomationsEnabled is false, then the query is disabled from running as +// a scheduled query (the only way to run them on devices is manually via the live queries API). +// - If Interval is not 0 and AutomationsEnabled is true, then the query is configured to run on +// devices at the provided interval; the query considered a "scheduled query". Fields `Platform`, +// `MinOsqueryVersion`, `AutomationsEnabled` and `Logging` are used when this is the case. type Query struct { UpdateCreateTimestamps - ID uint `json:"id"` + ID uint `json:"id"` + // TeamID to which team this query belongs. If not set, then the query belongs to the 'Global' + // team. The table schema for queries includes another column related to this one: + // `team_id_char`, this is because the unique constraint for queries is based on both the + // team_id and their name, but since team_id can be null (and (NULL == NULL) != true), we need + // to use something else to guarantee uniqueness, hence the use of team_id_char. team_id_char + // will be computed as string(team_id), if team_id IS NULL then team_char_id will be ''. + TeamID *uint `json:"team_id" db:"team_id"` + // Interval frequency of execution (in seconds), if 0 then, this query will never run. + Interval uint `json:"interval" db:"schedule_interval"` + // Platform if set, specifies the platform(s) this query will target. + // + // It's a comma-separated list of platforms where this query will run be when configured + // on a schedule. + Platform string `json:"platform" db:"platform"` + // MinOsqueryVersion if set, specifies the min required version of osquery that must be + // installed on the host. + MinOsqueryVersion string `json:"min_osquery_version" db:"min_osquery_version"` + // AutomationsEnabled whether to send data to the configured log destination + AutomationsEnabled bool `json:"automations_enabled" db:"automations_enabled"` + // Logging the type of log output for this query + Logging string `json:"logging" db:"logging_type"` Name string `json:"name"` Description string `json:"description"` Query string `json:"query"` @@ -35,14 +86,61 @@ type Query struct { // Packs is loaded when retrieving queries, but is stored in a join // table in the MySQL backend. Packs []Pack `json:"packs" db:"-"` - - AggregatedStats `json:"stats,omitempty"` + // AggregatedStats are the stats aggregated from all the individual stats reported + // by hosts. + // + // This field has null values if the query did not run as a schedule on any host. + AggregatedStats `json:"stats"` } +var ( + LoggingSnapshot = "snapshot" + LoggingDifferential = "differential" + LoggingDifferentialIgnoreRemovals = "differential_ignore_removals" +) + func (q Query) AuthzType() string { return "query" } +// TeamIDStr returns either the string representation of q.TeamID or ” if nil +func (q *Query) TeamIDStr() string { + if q == nil || q.TeamID == nil { + return "" + } + return fmt.Sprint(*q.TeamID) +} + +func (q *Query) GetSnapshot() *bool { + var logging string + if q != nil { + logging = q.Logging + } + + switch logging { + case "snapshot": + return ptr.Bool(true) + default: + return nil + } +} + +func (q *Query) GetRemoved() *bool { + var logging string + if q != nil { + logging = q.Logging + } + + switch logging { + case "differential": + return ptr.Bool(true) + case "differential_ignore_removals": + return ptr.Bool(false) + default: + return nil + } +} + // Verify verifies the query payload is valid. func (q *QueryPayload) Verify() error { if q.Name != nil { @@ -55,6 +153,11 @@ func (q *QueryPayload) Verify() error { return err } } + if q.Logging != nil { + if err := verifyLogging(*q.Logging); err != nil { + return err + } + } return nil } @@ -66,9 +169,23 @@ func (q *Query) Verify() error { if err := verifyQuerySQL(q.Query); err != nil { return err } + if err := verifyLogging(q.Logging); err != nil { + return err + } return nil } +func (q *Query) ToQueryContent() QueryContent { + return QueryContent{ + Query: q.Query, + Interval: q.Interval, + Platform: &q.Platform, + Version: &q.MinOsqueryVersion, + Removed: q.GetRemoved(), + Snapshot: q.GetSnapshot(), + } +} + type TargetedQuery struct { *Query HostTargets HostTargets `json:"host_targets"` @@ -81,6 +198,7 @@ func (tq *TargetedQuery) AuthzType() string { var ( errQueryEmptyName = errors.New("query name cannot be empty") errQueryEmptyQuery = errors.New("query's SQL query cannot be empty") + errInvalidLogging = fmt.Errorf("invalid logging value, must be one of '%s', '%s', '%s'", LoggingSnapshot, LoggingDifferential, LoggingDifferentialIgnoreRemovals) ) func verifyQueryName(name string) error { @@ -97,6 +215,14 @@ func verifyQuerySQL(query string) error { return nil } +func verifyLogging(logging string) error { + // Empty string means snapshot. + if logging != "" && logging != LoggingSnapshot && logging != LoggingDifferential && logging != LoggingDifferentialIgnoreRemovals { + return errInvalidLogging + } + return nil +} + const ( QueryKind = "query" ) @@ -106,10 +232,32 @@ type QueryObject struct { Spec QuerySpec `json:"spec"` } +// QuerySpec allows creating/editing queries using "specs". type QuerySpec struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Query string `json:"query"` + // Name is the name of the query (which is unique in its team or globally). + // This field must be non-empty. + Name string `json:"name"` + // Description is the description of the query. + Description string `json:"description"` + // Query is the actual osquery SQL query. This field must be non-empty. + Query string `json:"query"` + + // TeamName is the team's name, the default "" means the query will be + // created globally. This field is only used when creating a query, + // when editing a query this field is ignored. + TeamName string `json:"team"` + // Interval is set to 0 if not set. + Interval uint `json:"interval"` + // ObserverCanRun is set to false if not set. + ObserverCanRun bool `json:"observer_can_run"` + // Platform is set to empty if not set when creating a query. + Platform string `json:"platform"` + // MinOsqueryVersion is set to empty if not set. + MinOsqueryVersion string `json:"min_osquery_version"` + // AutomationsEnabled is set to false if not set. + AutomationsEnabled bool `json:"automations_enabled"` + // Logging is set to "snapshot" if not set. + Logging string `json:"logging"` } func LoadQueriesFromYaml(yml string) ([]*Query, error) { @@ -126,7 +274,11 @@ func LoadQueriesFromYaml(yml string) ([]*Query, error) { return nil, fmt.Errorf("unmarshal yaml: %w", err) } queries = append(queries, - &Query{Name: q.Spec.Name, Description: q.Spec.Description, Query: q.Spec.Query}, + &Query{ + Name: q.Spec.Name, + Description: q.Spec.Description, + Query: q.Spec.Query, + }, ) } @@ -156,3 +308,22 @@ func WriteQueriesToYaml(queries []*Query) (string, error) { return strings.Join(ymlStrings, "---\n"), nil } + +type QueryStats struct { + ID uint `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Description string `json:"description,omitempty" db:"description"` + TeamID *uint `json:"team_id" db:"team_id"` + + // From osquery directly + AverageMemory int `json:"average_memory" db:"average_memory"` + Denylisted bool `json:"denylisted" db:"denylisted"` + Executions int `json:"executions" db:"executions"` + // Note schedule_interval is used for DB since "interval" is a reserved word in MySQL + Interval int `json:"interval" db:"schedule_interval"` + LastExecuted time.Time `json:"last_executed" db:"last_executed"` + OutputSize int `json:"output_size" db:"output_size"` + SystemTime int `json:"system_time" db:"system_time"` + UserTime int `json:"user_time" db:"user_time"` + WallTime int `json:"wall_time" db:"wall_time"` +} diff --git a/server/fleet/queries_test.go b/server/fleet/queries_test.go index 157626d837..8975a35132 100644 --- a/server/fleet/queries_test.go +++ b/server/fleet/queries_test.go @@ -3,10 +3,89 @@ package fleet import ( "testing" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestGetSnapshot(t *testing.T) { + testCases := []struct { + query *Query + expected *bool + }{ + { + query: nil, + expected: nil, + }, + { + query: &Query{Logging: "snapshot"}, + expected: ptr.Bool(true), + }, + { + query: &Query{Logging: "differential"}, + expected: nil, + }, + { + query: &Query{Logging: "differential_ignore_removals"}, + expected: nil, + }, + } + for _, tCase := range testCases { + require.Equal(t, tCase.expected, tCase.query.GetSnapshot()) + } +} + +func TestGetRemoved(t *testing.T) { + testCases := []struct { + query *Query + expected *bool + }{ + { + query: nil, + expected: nil, + }, + { + query: &Query{Logging: "snapshot"}, + expected: nil, + }, + { + query: &Query{Logging: "differential"}, + expected: ptr.Bool(true), + }, + { + query: &Query{Logging: "differential_ignore_removals"}, + expected: ptr.Bool(false), + }, + } + for i, tCase := range testCases { + require.Equal(t, tCase.expected, tCase.query.GetRemoved(), i) + } +} + +func TestTeamIDStr(t *testing.T) { + testCases := []struct { + query *Query + expected string + }{ + { + query: nil, + expected: "", + }, + { + query: &Query{}, + expected: "", + }, + { + query: &Query{TeamID: ptr.Uint(10)}, + expected: "10", + }, + } + + for _, tCase := range testCases { + require.Equal(t, tCase.expected, tCase.query.TeamIDStr()) + } +} + func TestLoadQueriesFromYamlStrings(t *testing.T) { testCases := []struct { yaml string diff --git a/server/fleet/scheduled_queries.go b/server/fleet/scheduled_queries.go index c7211017e6..e44e2f3988 100644 --- a/server/fleet/scheduled_queries.go +++ b/server/fleet/scheduled_queries.go @@ -1,6 +1,9 @@ package fleet import ( + "fmt" + "strconv" + "strings" "time" "github.com/fleetdm/fleet/v4/server/ptr" @@ -127,3 +130,99 @@ type ScheduledQueryStats struct { UserTime int `json:"user_time" db:"user_time"` WallTime int `json:"wall_time" db:"wall_time"` } + +// TeamID returns the team id if the stat is for a team query stat result +func (sqs *ScheduledQueryStats) TeamID() (*int, error) { + if strings.HasPrefix(sqs.PackName, "team-") { + teamIDParts := strings.Split(sqs.PackName, "-") + if len(teamIDParts) != 2 { + return nil, fmt.Errorf("invalid pack name: %s", sqs.PackName) + } + + teamID, err := strconv.Atoi(teamIDParts[1]) + if err != nil { + return nil, err + } + return &teamID, nil + } + + return nil, nil +} + +func ScheduledQueryFromQuery(query *Query) *ScheduledQuery { + var ( + snapshot *bool + removed *bool + ) + if query.Logging == "" || query.Logging == "snapshot" { + snapshot = ptr.Bool(true) + removed = ptr.Bool(false) + } else if query.Logging == "differential" { + snapshot = ptr.Bool(false) + removed = ptr.Bool(true) + } else { // query.Logging == "differential_ignore_removals" + snapshot = ptr.Bool(false) + removed = ptr.Bool(false) + } + return &ScheduledQuery{ + ID: query.ID, + Name: query.Name, + QueryID: query.ID, + QueryName: query.Name, + Query: query.Query, + Description: query.Description, + Interval: query.Interval, + Snapshot: snapshot, + Removed: removed, + Platform: &query.Platform, + Version: &query.MinOsqueryVersion, + AggregatedStats: query.AggregatedStats, + } +} + +func ScheduledQueryToQueryPayloadForNewQuery(originalQuery *Query, scheduledQuery *ScheduledQuery) QueryPayload { + var logging *string + if scheduledQuery.Snapshot != nil && scheduledQuery.Removed != nil { + if *scheduledQuery.Snapshot { + logging = ptr.String(LoggingSnapshot) + } else if *scheduledQuery.Removed { + logging = ptr.String(LoggingDifferential) + } else { + logging = ptr.String(LoggingDifferentialIgnoreRemovals) + } + } + return QueryPayload{ + Name: &originalQuery.Name, + Description: &originalQuery.Description, + Query: &originalQuery.Query, + ObserverCanRun: &originalQuery.ObserverCanRun, + TeamID: originalQuery.TeamID, + Interval: &scheduledQuery.Interval, + Platform: scheduledQuery.Platform, + MinOsqueryVersion: scheduledQuery.Version, + AutomationsEnabled: ptr.Bool(true), + Logging: logging, + } +} + +// NOTE(lucas): payload.Snapshot and payload.Removed must both be set in order to +// change the logging behavior of a scheduled query. +// Document this API change. +func ScheduledQueryPayloadToQueryPayloadForModifyQuery(payload ScheduledQueryPayload) QueryPayload { + var logging *string + if payload.Snapshot != nil && payload.Removed != nil { + if *payload.Snapshot { + logging = ptr.String(LoggingSnapshot) + } else if *payload.Removed { + logging = ptr.String(LoggingDifferential) + } else { + logging = ptr.String(LoggingDifferentialIgnoreRemovals) + } + } + return QueryPayload{ + Interval: payload.Interval, + Platform: payload.Platform, + MinOsqueryVersion: payload.Version, + Logging: logging, + } +} diff --git a/server/fleet/service.go b/server/fleet/service.go index 042107e833..a29e86983a 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -254,17 +254,20 @@ type Service interface { // ApplyQuerySpecs applies a list of queries (creating or updating them as necessary) ApplyQuerySpecs(ctx context.Context, specs []*QuerySpec) error // GetQuerySpecs gets the YAML file representing all the stored queries. - GetQuerySpecs(ctx context.Context) ([]*QuerySpec, error) - // GetQuerySpec gets the spec for the query with the given name. - GetQuerySpec(ctx context.Context, name string) (*QuerySpec, error) + GetQuerySpecs(ctx context.Context, teamID *uint) ([]*QuerySpec, error) + // GetQuerySpec gets the spec for the query with the given name on a team. + // A nil or 0 teamID means the query is looked for in the global domain. + GetQuerySpec(ctx context.Context, teamID *uint, name string) (*QuerySpec, error) // ListQueries returns a list of saved queries. Note only saved queries should be returned (those that are created // for distributed queries but not saved should not be returned). - ListQueries(ctx context.Context, opt ListOptions) ([]*Query, error) + // When is set to scheduled != nil, then only scheduled queries will be returned if `*scheduled == true` + // 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) NewQuery(ctx context.Context, p QueryPayload) (*Query, error) ModifyQuery(ctx context.Context, id uint, p QueryPayload) (*Query, error) - DeleteQuery(ctx context.Context, name string) error + DeleteQuery(ctx context.Context, teamID *uint, name string) error // DeleteQueryByID deletes a query by ID. For backwards compatibility with UI DeleteQueryByID(ctx context.Context, id uint) error // DeleteQueries deletes the existing query objects with the provided IDs. The number of deleted queries is returned diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 9365544951..7250ab6da9 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -62,7 +62,7 @@ type NewQueryFunc func(ctx context.Context, query *fleet.Query, opts ...fleet.Op type SaveQueryFunc func(ctx context.Context, query *fleet.Query) error -type DeleteQueryFunc func(ctx context.Context, name string) error +type DeleteQueryFunc func(ctx context.Context, teamID *uint, name string) error type DeleteQueriesFunc func(ctx context.Context, ids []uint) (uint, error) @@ -70,7 +70,9 @@ type QueryFunc func(ctx context.Context, id uint) (*fleet.Query, error) type ListQueriesFunc func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) -type QueryByNameFunc func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) +type ListScheduledQueriesForAgentsFunc func(ctx context.Context, teamID *uint) ([]*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) @@ -108,10 +110,6 @@ type PackByNameFunc func(ctx context.Context, name string, opts ...fleet.Optiona type ListPacksForHostFunc func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) -type EnsureGlobalPackFunc func(ctx context.Context) (*fleet.Pack, error) - -type EnsureTeamPackFunc func(ctx context.Context, teamID uint) (*fleet.Pack, error) - type ApplyLabelSpecsFunc func(ctx context.Context, specs []*fleet.LabelSpec) error type GetLabelSpecsFunc func(ctx context.Context) ([]*fleet.LabelSpec, error) @@ -428,8 +426,6 @@ type UpdateAllCronStatsForInstanceFunc func(ctx context.Context, instance string type CleanupCronStatsFunc func(ctx context.Context) error -type UpdateScheduledQueryAggregatedStatsFunc func(ctx context.Context) error - type UpdateQueryAggregatedStatsFunc func(ctx context.Context) error type LoadHostByNodeKeyFunc func(ctx context.Context, nodeKey string) (*fleet.Host, error) @@ -446,7 +442,7 @@ type TeamFeaturesFunc func(ctx context.Context, teamID uint) (*fleet.Features, e type TeamMDMConfigFunc func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) -type SaveHostPackStatsFunc func(ctx context.Context, hostID uint, stats []fleet.PackStats) error +type SaveHostPackStatsFunc func(ctx context.Context, teamID *uint, hostID uint, stats []fleet.PackStats) error type AsyncBatchSaveHostsScheduledQueryStatsFunc func(ctx context.Context, stats map[uint][]fleet.ScheduledQueryStats, batchSize int) (int, error) @@ -739,6 +735,9 @@ type DataStore struct { ListQueriesFunc ListQueriesFunc ListQueriesFuncInvoked bool + ListScheduledQueriesForAgentsFunc ListScheduledQueriesForAgentsFunc + ListScheduledQueriesForAgentsFuncInvoked bool + QueryByNameFunc QueryByNameFunc QueryByNameFuncInvoked bool @@ -796,12 +795,6 @@ type DataStore struct { ListPacksForHostFunc ListPacksForHostFunc ListPacksForHostFuncInvoked bool - EnsureGlobalPackFunc EnsureGlobalPackFunc - EnsureGlobalPackFuncInvoked bool - - EnsureTeamPackFunc EnsureTeamPackFunc - EnsureTeamPackFuncInvoked bool - ApplyLabelSpecsFunc ApplyLabelSpecsFunc ApplyLabelSpecsFuncInvoked bool @@ -1276,9 +1269,6 @@ type DataStore struct { CleanupCronStatsFunc CleanupCronStatsFunc CleanupCronStatsFuncInvoked bool - UpdateScheduledQueryAggregatedStatsFunc UpdateScheduledQueryAggregatedStatsFunc - UpdateScheduledQueryAggregatedStatsFuncInvoked bool - UpdateQueryAggregatedStatsFunc UpdateQueryAggregatedStatsFunc UpdateQueryAggregatedStatsFuncInvoked bool @@ -1781,11 +1771,11 @@ func (s *DataStore) SaveQuery(ctx context.Context, query *fleet.Query) error { return s.SaveQueryFunc(ctx, query) } -func (s *DataStore) DeleteQuery(ctx context.Context, name string) error { +func (s *DataStore) DeleteQuery(ctx context.Context, teamID *uint, name string) error { s.mu.Lock() s.DeleteQueryFuncInvoked = true s.mu.Unlock() - return s.DeleteQueryFunc(ctx, name) + return s.DeleteQueryFunc(ctx, teamID, name) } func (s *DataStore) DeleteQueries(ctx context.Context, ids []uint) (uint, error) { @@ -1809,11 +1799,18 @@ func (s *DataStore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) return s.ListQueriesFunc(ctx, opt) } -func (s *DataStore) QueryByName(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { +func (s *DataStore) ListScheduledQueriesForAgents(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { + s.mu.Lock() + s.ListScheduledQueriesForAgentsFuncInvoked = true + s.mu.Unlock() + return s.ListScheduledQueriesForAgentsFunc(ctx, teamID) +} + +func (s *DataStore) QueryByName(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { s.mu.Lock() s.QueryByNameFuncInvoked = true s.mu.Unlock() - return s.QueryByNameFunc(ctx, name, opts...) + return s.QueryByNameFunc(ctx, teamID, name, opts...) } func (s *DataStore) ObserverCanRunQuery(ctx context.Context, queryID uint) (bool, error) { @@ -1942,20 +1939,6 @@ func (s *DataStore) ListPacksForHost(ctx context.Context, hid uint) (packs []*fl return s.ListPacksForHostFunc(ctx, hid) } -func (s *DataStore) EnsureGlobalPack(ctx context.Context) (*fleet.Pack, error) { - s.mu.Lock() - s.EnsureGlobalPackFuncInvoked = true - s.mu.Unlock() - return s.EnsureGlobalPackFunc(ctx) -} - -func (s *DataStore) EnsureTeamPack(ctx context.Context, teamID uint) (*fleet.Pack, error) { - s.mu.Lock() - s.EnsureTeamPackFuncInvoked = true - s.mu.Unlock() - return s.EnsureTeamPackFunc(ctx, teamID) -} - func (s *DataStore) ApplyLabelSpecs(ctx context.Context, specs []*fleet.LabelSpec) error { s.mu.Lock() s.ApplyLabelSpecsFuncInvoked = true @@ -3062,13 +3045,6 @@ func (s *DataStore) CleanupCronStats(ctx context.Context) error { return s.CleanupCronStatsFunc(ctx) } -func (s *DataStore) UpdateScheduledQueryAggregatedStats(ctx context.Context) error { - s.mu.Lock() - s.UpdateScheduledQueryAggregatedStatsFuncInvoked = true - s.mu.Unlock() - return s.UpdateScheduledQueryAggregatedStatsFunc(ctx) -} - func (s *DataStore) UpdateQueryAggregatedStats(ctx context.Context) error { s.mu.Lock() s.UpdateQueryAggregatedStatsFuncInvoked = true @@ -3125,11 +3101,11 @@ func (s *DataStore) TeamMDMConfig(ctx context.Context, teamID uint) (*fleet.Team return s.TeamMDMConfigFunc(ctx, teamID) } -func (s *DataStore) SaveHostPackStats(ctx context.Context, hostID uint, stats []fleet.PackStats) error { +func (s *DataStore) SaveHostPackStats(ctx context.Context, teamID *uint, hostID uint, stats []fleet.PackStats) error { s.mu.Lock() s.SaveHostPackStatsFuncInvoked = true s.mu.Unlock() - return s.SaveHostPackStatsFunc(ctx, hostID, stats) + return s.SaveHostPackStatsFunc(ctx, teamID, hostID, stats) } func (s *DataStore) AsyncBatchSaveHostsScheduledQueryStats(ctx context.Context, stats map[uint][]fleet.ScheduledQueryStats, batchSize int) (int, error) { diff --git a/server/service/async/async_scheduled_query_stats.go b/server/service/async/async_scheduled_query_stats.go index 2c45413c46..8befa91389 100644 --- a/server/service/async/async_scheduled_query_stats.go +++ b/server/service/async/async_scheduled_query_stats.go @@ -21,10 +21,10 @@ const ( ) // RecordScheduledQueryStats records the scheduled query stats for a given host. -func (t *Task) RecordScheduledQueryStats(ctx context.Context, hostID uint, stats []fleet.PackStats, ts time.Time) error { +func (t *Task) RecordScheduledQueryStats(ctx context.Context, teamID *uint, hostID uint, stats []fleet.PackStats, ts time.Time) error { cfg := t.taskConfigs[config.AsyncTaskScheduledQueryStats] if !cfg.Enabled { - return t.datastore.SaveHostPackStats(ctx, hostID, stats) + return t.datastore.SaveHostPackStats(ctx, teamID, hostID, stats) } // set an expiration on the key, ensuring that if async processing is diff --git a/server/service/async/async_scheduled_query_stats_test.go b/server/service/async/async_scheduled_query_stats_test.go index f03f6a0675..699867c340 100644 --- a/server/service/async/async_scheduled_query_stats_test.go +++ b/server/service/async/async_scheduled_query_stats_test.go @@ -28,10 +28,10 @@ func testCollectScheduledQueryStats(t *testing.T, ds *mysql.Datastore, pool flee p2 := test.NewPack(t, ds, "p2") p3 := test.NewPack(t, ds, "p3") - q1 := test.NewQuery(t, ds, "q1", "select 1", user.ID, true) - q2 := test.NewQuery(t, ds, "q2", "select 2", user.ID, true) - q3 := test.NewQuery(t, ds, "q3", "select 3", user.ID, true) - q4 := test.NewQuery(t, ds, "q4", "select 4", user.ID, true) + q1 := test.NewQuery(t, ds, nil, "q1", "select 1", user.ID, true) + q2 := test.NewQuery(t, ds, nil, "q2", "select 2", user.ID, true) + q3 := test.NewQuery(t, ds, nil, "q3", "select 3", user.ID, true) + q4 := test.NewQuery(t, ds, nil, "q4", "select 4", user.ID, true) sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false, "sq1") sq2 := test.NewScheduledQuery(t, ds, p2.ID, q2.ID, 60, false, false, "sq2") @@ -125,7 +125,7 @@ func testCollectScheduledQueryStats(t *testing.T, ds *mysql.Datastore, pool flee setupTest := func(t *testing.T, task *Task, data map[uint][]fleet.PackStats) collectorExecStats { var wantStats collectorExecStats for hid, stats := range data { - err := task.RecordScheduledQueryStats(ctx, hid, stats, time.Now()) + err := task.RecordScheduledQueryStats(ctx, nil, hid, stats, time.Now()) require.NoError(t, err) } wantStats.Keys = len(data) @@ -177,7 +177,7 @@ func testRecordScheduledQueryStatsSync(t *testing.T, ds *mock.Store, pool fleet. task := NewTask(ds, pool, clock.C, config.OsqueryConfig{}) - err := task.RecordScheduledQueryStats(ctx, host.ID, stats, now) + err := task.RecordScheduledQueryStats(ctx, host.TeamID, host.ID, stats, now) require.NoError(t, err) require.True(t, ds.SaveHostPackStatsFuncInvoked) ds.SaveHostPackStatsFuncInvoked = false @@ -222,7 +222,7 @@ func testRecordScheduledQueryStatsAsync(t *testing.T, ds *mock.Store, pool fleet AsyncHostRedisScanKeysCount: 10, }) - err := task.RecordScheduledQueryStats(ctx, host.ID, stats, now) + err := task.RecordScheduledQueryStats(ctx, host.TeamID, host.ID, stats, now) require.NoError(t, err) require.False(t, ds.SaveHostPackStatsFuncInvoked) @@ -269,7 +269,7 @@ func testRecordScheduledQueryStatsAsync(t *testing.T, ds *mock.Store, pool fleet PackName: "p1", QueryStats: []fleet.ScheduledQueryStats{}, }, } - err = task.RecordScheduledQueryStats(ctx, host.ID, stats, now) + err = task.RecordScheduledQueryStats(ctx, host.TeamID, host.ID, stats, now) require.NoError(t, err) require.False(t, ds.SaveHostPackStatsFuncInvoked) diff --git a/server/service/async/async_test.go b/server/service/async/async_test.go index d42c02b1dd..6291d79a74 100644 --- a/server/service/async/async_test.go +++ b/server/service/async/async_test.go @@ -97,7 +97,7 @@ func TestRecord(t *testing.T) { ds.AsyncBatchUpdatePolicyTimestampFunc = func(ctx context.Context, ids []uint, ts time.Time) error { return nil } - ds.SaveHostPackStatsFunc = func(ctx context.Context, hid uint, stats []fleet.PackStats) error { + ds.SaveHostPackStatsFunc = func(ctx context.Context, teamID *uint, hid uint, stats []fleet.PackStats) error { return nil } ds.AsyncBatchSaveHostsScheduledQueryStatsFunc = func(ctx context.Context, batch map[uint][]fleet.ScheduledQueryStats, batchSize int) (int, error) { diff --git a/server/service/base_client.go b/server/service/base_client.go index 845bdc56bb..0f9e82ce16 100644 --- a/server/service/base_client.go +++ b/server/service/base_client.go @@ -37,10 +37,14 @@ type baseClient struct { clientCapabilities fleet.CapabilityMap } +// parseResponse processes the status code and parses the response body. +// It does not close the response body (should be closed by the caller). func (bc *baseClient) parseResponse(verb, path string, response *http.Response, responseDest interface{}) error { switch response.StatusCode { case http.StatusNotFound: - return notFoundErr{} + return notFoundErr{ + msg: extractServerErrorText(response.Body), + } case http.StatusUnauthorized: return ErrUnauthenticated case http.StatusPaymentRequired: diff --git a/server/service/client.go b/server/service/client.go index 4abb571ebb..86c51a0b64 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -281,6 +281,8 @@ func (c *Client) ApplyGroup( logf(format, args...) } } + + // specs.Queries must be applied before specs.Packs because packs reference queries. if len(specs.Queries) > 0 { if opts.DryRun { logfn("[!] ignoring queries, dry run mode only supported for 'config' and 'team' specs\n") diff --git a/server/service/client_mdm.go b/server/service/client_mdm.go index e627eb53df..33b1e1091f 100644 --- a/server/service/client_mdm.go +++ b/server/service/client_mdm.go @@ -104,6 +104,7 @@ func (c *Client) UploadBootstrapPackage(pkg *fleet.MDMAppleBootstrapPackage) err if err != nil { return fmt.Errorf("do multipart request: %w", err) } + defer response.Body.Close() var bpResponse uploadBootstrapPackageResponse if err := c.parseResponse(verb, path, response, &bpResponse); err != nil { @@ -118,7 +119,7 @@ func (c *Client) EnsureBootstrapPackage(bp *fleet.MDMAppleBootstrapPackage, team oldMeta, err := c.GetBootstrapPackageMetadata(teamID, true) if err != nil { // not found is OK, it means this is our first time uploading a package - if !errors.Is(err, notFoundErr{}) { + if !errors.As(err, ¬FoundErr{}) { return fmt.Errorf("getting bootstrap package metadata: %w", err) } isFirstTime = true diff --git a/server/service/client_queries.go b/server/service/client_queries.go index 8f98cbc210..00c27ee80d 100644 --- a/server/service/client_queries.go +++ b/server/service/client_queries.go @@ -1,6 +1,7 @@ package service import ( + "fmt" "net/url" "github.com/fleetdm/fleet/v4/server/fleet" @@ -15,20 +16,31 @@ func (c *Client) ApplyQueries(specs []*fleet.QuerySpec) error { return c.authenticatedRequest(req, verb, path, &responseBody) } -// GetQuery retrieves the list of all Queries. -func (c *Client) GetQuery(name string) (*fleet.QuerySpec, error) { +// GetQuerySpec returns the query spec of a query by its team+name. +func (c *Client) GetQuerySpec(teamID *uint, name string) (*fleet.QuerySpec, error) { verb, path := "GET", "/api/latest/fleet/spec/queries/"+url.PathEscape(name) + query := url.Values{} + if teamID != nil { + query.Set("team_id", fmt.Sprint(*teamID)) + } var responseBody getQuerySpecResponse - err := c.authenticatedRequest(nil, verb, path, &responseBody) + err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query.Encode()) return responseBody.Spec, err } // GetQueries retrieves the list of all Queries. -func (c *Client) GetQueries() ([]fleet.Query, error) { +func (c *Client) GetQueries(teamID *uint) ([]fleet.Query, error) { verb, path := "GET", "/api/latest/fleet/queries" + query := url.Values{} + if teamID != nil { + query.Set("team_id", fmt.Sprint(*teamID)) + } var responseBody listQueriesResponse - err := c.authenticatedRequest(nil, verb, path, &responseBody) - return responseBody.Queries, err + err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query.Encode()) + if err != nil { + return nil, err + } + return responseBody.Queries, nil } // DeleteQuery deletes the query with the matching name. diff --git a/server/service/client_teams.go b/server/service/client_teams.go index 959de7c5b8..85812c9489 100644 --- a/server/service/client_teams.go +++ b/server/service/client_teams.go @@ -2,6 +2,7 @@ package service import ( "encoding/json" + "fmt" "net/url" "strconv" @@ -33,6 +34,15 @@ func (c *Client) CreateTeam(teamPayload fleet.TeamPayload) (*fleet.Team, error) return responseBody.Team, nil } +func (c *Client) GetTeam(teamID uint) (*fleet.Team, error) { + verb, path := "GET", fmt.Sprintf("/api/latest/fleet/teams/%d", teamID) + var responseBody getTeamResponse + if err := c.authenticatedRequest(getTeamRequest{}, verb, path, &responseBody); err != nil { + return nil, err + } + return responseBody.Team, nil +} + // DeleteTeam deletes a team. func (c *Client) DeleteTeam(teamID uint) error { verb, path := "DELETE", "/api/latest/fleet/teams/"+strconv.FormatUint(uint64(teamID), 10) diff --git a/server/service/global_schedule.go b/server/service/global_schedule.go index f61325c922..a8efa4c87d 100644 --- a/server/service/global_schedule.go +++ b/server/service/global_schedule.go @@ -2,8 +2,8 @@ package service import ( "context" - "fmt" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" ) @@ -37,22 +37,19 @@ func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fle } func (svc *Service) GetGlobalScheduledQueries(ctx context.Context, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) { - if err := svc.authz.Authorize(ctx, &fleet.Pack{ - Type: ptr.String("global"), - }, fleet.ActionRead); err != nil { - return nil, err - } - - gp, err := svc.ds.EnsureGlobalPack(ctx) + queries, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true)) // teamID == nil means global if err != nil { return nil, err } - - return svc.ds.ListScheduledQueriesInPackWithStats(ctx, gp.ID, opts) + scheduledQueries := make([]*fleet.ScheduledQuery, 0, len(queries)) + for _, query := range queries { + scheduledQueries = append(scheduledQueries, fleet.ScheduledQueryFromQuery(query)) + } + return scheduledQueries, nil } //////////////////////////////////////////////////////////////////////////////// -// Global Schedule Query +// Schedule a global query //////////////////////////////////////////////////////////////////////////////// type globalScheduleQueryRequest struct { @@ -90,20 +87,22 @@ func globalScheduleQueryEndpoint(ctx context.Context, request interface{}, svc f return globalScheduleQueryResponse{Scheduled: scheduled}, nil } -func (svc *Service) GlobalScheduleQuery(ctx context.Context, sq *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) { - if err := svc.authz.Authorize(ctx, &fleet.Pack{ - Type: ptr.String("global"), - }, fleet.ActionRead); err != nil { - return nil, err - } - - gp, err := svc.ds.EnsureGlobalPack(ctx) +func (svc *Service) GlobalScheduleQuery(ctx context.Context, scheduledQuery *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) { + originalQuery, err := svc.ds.Query(ctx, scheduledQuery.QueryID) if err != nil { - return nil, err + setAuthCheckedOnPreAuthErr(ctx) + return nil, ctxerr.Wrap(ctx, err, "get query") } - sq.PackID = gp.ID - - return svc.ScheduleQuery(ctx, sq) + if originalQuery.TeamID != nil { + setAuthCheckedOnPreAuthErr(ctx) + return nil, ctxerr.New(ctx, "cannot create a global schedule from a team query") + } + originalQuery.Name = nameForCopiedQuery(originalQuery.Name) + newQuery, err := svc.NewQuery(ctx, fleet.ScheduledQueryToQueryPayloadForNewQuery(originalQuery, scheduledQuery)) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "create new query") + } + return fleet.ScheduledQueryFromQuery(newQuery), nil } //////////////////////////////////////////////////////////////////////////////// @@ -135,21 +134,12 @@ func modifyGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc }, nil } -func (svc *Service) ModifyGlobalScheduledQueries(ctx context.Context, id uint, query fleet.ScheduledQueryPayload) (*fleet.ScheduledQuery, error) { - if err := svc.authz.Authorize(ctx, &fleet.Pack{ - Type: ptr.String("global"), - }, fleet.ActionWrite); err != nil { - return nil, err - } - - gp, err := svc.ds.EnsureGlobalPack(ctx) +func (svc *Service) ModifyGlobalScheduledQueries(ctx context.Context, id uint, scheduledQueryPayload fleet.ScheduledQueryPayload) (*fleet.ScheduledQuery, error) { + query, err := svc.ModifyQuery(ctx, id, fleet.ScheduledQueryPayloadToQueryPayloadForModifyQuery(scheduledQueryPayload)) if err != nil { return nil, err } - - query.PackID = ptr.Uint(gp.ID) - - return svc.ModifyScheduledQuery(ctx, id, query) + return fleet.ScheduledQueryFromQuery(query), nil } //////////////////////////////////////////////////////////////////////////////// @@ -176,24 +166,7 @@ func deleteGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc return deleteGlobalScheduleResponse{}, nil } +// TODO(lucas): Document new behavior. func (svc *Service) DeleteGlobalScheduledQueries(ctx context.Context, id uint) error { - if err := svc.authz.Authorize(ctx, &fleet.Pack{ - Type: ptr.String("global"), - }, fleet.ActionWrite); err != nil { - return err - } - - globalPack, err := svc.ds.EnsureGlobalPack(ctx) - if err != nil { - return err - } - scheduledQuery, err := svc.ds.ScheduledQuery(ctx, id) - if err != nil { - return err - } - if scheduledQuery.PackID != globalPack.ID { - return fmt.Errorf("scheduled query %d is not global", id) - } - - return svc.DeleteScheduledQuery(ctx, id) + return svc.DeleteQueryByID(ctx, id) } diff --git a/server/service/global_schedule_test.go b/server/service/global_schedule_test.go index 4f2f6c6a5b..25f25608c6 100644 --- a/server/service/global_schedule_test.go +++ b/server/service/global_schedule_test.go @@ -14,22 +14,29 @@ func TestGlobalScheduleAuth(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) - ds.ListScheduledQueriesInPackWithStatsFunc = func(ctx context.Context, id uint, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) { + // + // All global schedule query methods use queries datastore methods. + // + + ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) { + return &fleet.Query{ + Name: "foobar", + Query: "SELECT 1;", + }, nil + } + ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query) error { + return nil + } + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + return nil + } + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } - ds.EnsureGlobalPackFunc = func(ctx context.Context) (*fleet.Pack, error) { - return &fleet.Pack{}, nil + ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) { + return &fleet.Query{}, nil } - ds.NewScheduledQueryFunc = func(ctx context.Context, sq *fleet.ScheduledQuery, opts ...fleet.OptionalArg) (*fleet.ScheduledQuery, error) { - return sq, nil - } - ds.ScheduledQueryFunc = func(ctx context.Context, id uint) (*fleet.ScheduledQuery, error) { - return &fleet.ScheduledQuery{}, nil - } - ds.SaveScheduledQueryFunc = func(ctx context.Context, sq *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) { - return sq, nil - } - ds.DeleteScheduledQueryFunc = func(ctx context.Context, id uint) error { + ds.DeleteQueryFunc = func(ctx context.Context, teamID *uint, name string) error { return nil } @@ -83,7 +90,11 @@ func TestGlobalScheduleAuth(t *testing.T) { _, err := svc.GetGlobalScheduledQueries(ctx, fleet.ListOptions{}) checkAuthErr(t, tt.shouldFailRead, err) - _, err = svc.GlobalScheduleQuery(ctx, &fleet.ScheduledQuery{Name: "query", QueryName: "query", Interval: 10}) + _, err = svc.GlobalScheduleQuery(ctx, &fleet.ScheduledQuery{ + Name: "query", + QueryName: "query", + Interval: 10, + }) checkAuthErr(t, tt.shouldFailWrite, err) _, err = svc.ModifyGlobalScheduledQueries(ctx, 1, fleet.ScheduledQueryPayload{}) diff --git a/server/service/handler.go b/server/service/handler.go index d5d81b0724..e278618de6 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -341,8 +341,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC ue.DELETE("/api/_version_/fleet/queries/id/{id:[0-9]+}", deleteQueryByIDEndpoint, deleteQueryByIDRequest{}) ue.POST("/api/_version_/fleet/queries/delete", deleteQueriesEndpoint, deleteQueriesRequest{}) ue.POST("/api/_version_/fleet/spec/queries", applyQuerySpecsEndpoint, applyQuerySpecsRequest{}) - ue.GET("/api/_version_/fleet/spec/queries", getQuerySpecsEndpoint, nil) - ue.GET("/api/_version_/fleet/spec/queries/{name}", getQuerySpecEndpoint, getGenericSpecRequest{}) + ue.GET("/api/_version_/fleet/spec/queries", getQuerySpecsEndpoint, getQuerySpecsRequest{}) + ue.GET("/api/_version_/fleet/spec/queries/{name}", getQuerySpecEndpoint, getQuerySpecRequest{}) ue.GET("/api/_version_/fleet/packs/{id:[0-9]+}", getPackEndpoint, getPackRequest{}) ue.POST("/api/_version_/fleet/packs", createPackEndpoint, createPackRequest{}) diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 879ca18f83..4a21685a3c 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -552,6 +552,7 @@ func (s *integrationTestSuite) TestGlobalSchedule() { Description: "Some description", Query: "select * from osquery;", ObserverCanRun: true, + Saved: true, }) require.NoError(t, err) @@ -565,7 +566,7 @@ func (s *integrationTestSuite) TestGlobalSchedule() { s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusOK, &gs) require.Len(t, gs.GlobalSchedule, 1) assert.Equal(t, uint(42), gs.GlobalSchedule[0].Interval) - assert.Equal(t, "TestQuery1", gs.GlobalSchedule[0].Name) + assert.Contains(t, gs.GlobalSchedule[0].Name, "Copy of TestQuery1 (") id := gs.GlobalSchedule[0].ID // list page 2, should be empty @@ -1211,7 +1212,7 @@ func (s *integrationTestSuite) TestListHosts() { assert.Greater(t, resp.Hosts[0].SoftwareUpdatedAt, resp.Hosts[0].CreatedAt) user1 := test.NewUser(t, s.ds, "Alice", "alice@example.com", true) - q := test.NewQuery(t, s.ds, "query1", "select 1", 0, true) + q := test.NewQuery(t, s.ds, nil, "query1", "select 1", 0, true) defer cleanupQuery(s, q.ID) p, err := s.ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{ QueryID: &q.ID, @@ -4713,7 +4714,7 @@ func (s *integrationTestSuite) TestAppConfig() { // corresponding activity should not have been created. var listActivities listActivitiesResponse s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc") - if !assert.Len(t, listActivities.Activities, 1) { + if len(listActivities.Activities) > 1 { // if there is an activity, make sure it is not edited_agent_options require.NotEqual(t, fleet.ActivityTypeEditedAgentOptions{}.ActivityName(), listActivities.Activities[0].Type) } @@ -4931,6 +4932,7 @@ func (s *integrationTestSuite) TestAppConfig() { s.DoRaw("PATCH", "/api/latest/fleet/config", jsonMustMarshal(t, defAppCfg), http.StatusOK) } +// TODO(lucas): Add tests here. func (s *integrationTestSuite) TestQuerySpecs() { t := s.T() @@ -6578,6 +6580,7 @@ func (s *integrationTestSuite) TestAPIVersion_v1_2022_04() { Name: "TestQuery2", Query: "select * from osquery;", ObserverCanRun: true, + Saved: true, }) require.NoError(t, err) @@ -6594,6 +6597,7 @@ func (s *integrationTestSuite) TestAPIVersion_v1_2022_04() { // list the scheduled queries with the new endpoint, but the old version res = s.DoRaw("GET", "/api/v1/fleet/schedule", nil, http.StatusMethodNotAllowed) res.Body.Close() + // list again, this time with the correct version gs := fleet.GlobalSchedulePayload{} s.DoJSON("GET", "/api/2022-04/fleet/schedule", nil, http.StatusOK, &gs) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 5c62d050f3..bd46481e55 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -472,11 +472,20 @@ func (s *integrationEnterpriseTestSuite) TestTeamSchedule() { qr, err := s.ds.NewQuery( context.Background(), - &fleet.Query{Name: "TestQueryTeamPolicy", Description: "Some description", Query: "select * from osquery;", ObserverCanRun: true}, + &fleet.Query{ + Name: "TestQueryTeamPolicy", + Description: "Some description", + Query: "select * from osquery;", + ObserverCanRun: true, + Saved: true, + }, ) require.NoError(t, err) - gsParams := teamScheduleQueryRequest{ScheduledQueryPayload: fleet.ScheduledQueryPayload{QueryID: &qr.ID, Interval: ptr.Uint(42)}} + gsParams := teamScheduleQueryRequest{ScheduledQueryPayload: fleet.ScheduledQueryPayload{ + QueryID: &qr.ID, + Interval: ptr.Uint(42), + }} r := teamScheduleQueryResponse{} s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", team1.ID), gsParams, http.StatusOK, &r) @@ -484,8 +493,8 @@ func (s *integrationEnterpriseTestSuite) TestTeamSchedule() { s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", team1.ID), nil, http.StatusOK, &ts) require.Len(t, ts.Scheduled, 1) assert.Equal(t, uint(42), ts.Scheduled[0].Interval) - assert.Equal(t, "TestQueryTeamPolicy", ts.Scheduled[0].Name) - assert.Equal(t, qr.ID, ts.Scheduled[0].QueryID) + assert.Contains(t, ts.Scheduled[0].Name, "Copy of TestQueryTeamPolicy") + assert.NotEqual(t, qr.ID, ts.Scheduled[0].QueryID) // it creates a new query (copy) id := ts.Scheduled[0].ID modifyResp := modifyTeamScheduleResponse{} @@ -2950,12 +2959,16 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { ggsr := getGlobalScheduleResponse{} s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusOK, &ggsr) require.NoError(t, ggsr.Err) - var globalPackID uint - mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { - return sqlx.GetContext(context.Background(), q, &globalPackID, - `SELECT id FROM packs WHERE pack_type = 'global'`) - }) - require.NotZero(t, globalPackID) + cpar := createPackResponse{} + var userPackID uint + s.DoJSON("POST", "/api/latest/fleet/packs", createPackRequest{ + PackPayload: fleet.PackPayload{ + Name: ptr.String("Foobar"), + Disabled: ptr.Bool(false), + }, + }, http.StatusOK, &cpar) + userPackID = cpar.Pack.Pack.ID + require.NotZero(t, userPackID) cur := createUserResponse{} s.DoJSON("POST", "/api/latest/fleet/users/admin", createUserRequest{ UserPayload: fleet.UserPayload{ @@ -3178,10 +3191,10 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { s.DoJSON("POST", "/api/latest/fleet/queries/delete", deleteQueriesRequest{IDs: []uint{cqr2.Query.ID}}, http.StatusOK, &deleteQueriesResponse{}) s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/%s", cqr3.Query.Name), deleteQueryRequest{}, http.StatusOK, &deleteQueryResponse{}) - // Attempt to add a query to the global schedule, should allow. + // Attempt to add a query to a user pack, should allow. sqr := scheduleQueryResponse{} s.DoJSON("POST", "/api/latest/fleet/packs/schedule", scheduleQueryRequest{ - PackID: globalPackID, + PackID: userPackID, QueryID: cqr4.Query.ID, Interval: 60, }, http.StatusOK, &sqr) @@ -3196,9 +3209,8 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { // Attempt to remove a query from the global schedule, should allow. s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/packs/schedule/%d", sqr.Scheduled.ID), deleteScheduledQueryRequest{}, http.StatusOK, &scheduleQueryResponse{}) - // Attempt to read the global schedule, should allow. - // This is an exception to the "write only" nature of gitops (packs can be viewed by gitops). - s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusOK, &getGlobalScheduleResponse{}) + // Attempt to read the global schedule, should disallow. + s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusForbidden, &getGlobalScheduleResponse{}) // Attempt to create a pack, should allow. cpr := createPackResponse{} @@ -3391,13 +3403,23 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { s.setTokenForTest(t, "gitops2@example.com", test.GoodPassword) - // Attempt to create queries, should allow. + // Attempt to create queries in global domain, should fail. tcqr := createQueryResponse{} s.DoJSON("POST", "/api/latest/fleet/queries", createQueryRequest{ QueryPayload: fleet.QueryPayload{ Name: ptr.String("foo600"), Query: ptr.String("SELECT * from orbit_info;"), }, + }, http.StatusForbidden, &tcqr) + + // Attempt to create queries in its team, should allow. + tcqr = createQueryResponse{} + s.DoJSON("POST", "/api/latest/fleet/queries", createQueryRequest{ + QueryPayload: fleet.QueryPayload{ + Name: ptr.String("foo600"), + Query: ptr.String("SELECT * from orbit_info;"), + TeamID: &t1.ID, + }, }, http.StatusOK, &tcqr) // Attempt to edit own query, should allow. @@ -3431,19 +3453,28 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { // Attempt to read other team's schedule, should fail. s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", t2.ID), getTeamScheduleRequest{}, http.StatusForbidden, &getTeamScheduleResponse{}) - // Attempt to add a query to the global schedule, should fail. + // Attempt to add a query to a user pack, should fail. tsqr := scheduleQueryResponse{} s.DoJSON("POST", "/api/latest/fleet/packs/schedule", scheduleQueryRequest{ - PackID: globalPackID, + PackID: userPackID, QueryID: cqr4.Query.ID, Interval: 60, }, http.StatusForbidden, &tsqr) // Attempt to add a query to the team's schedule, should allow. + cqrt1 := createQueryResponse{} + s.DoJSON("POST", "/api/latest/fleet/queries", createQueryRequest{ + QueryPayload: fleet.QueryPayload{ + Name: ptr.String("foo8"), + Query: ptr.String("SELECT * from managed_policies;"), + TeamID: &t1.ID, + }, + }, http.StatusOK, &cqrt1) ttsqr := teamScheduleQueryResponse{} + // Add a schedule with the deprecated APIs (by referencing a global query). s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", t1.ID), teamScheduleQueryRequest{ ScheduledQueryPayload: fleet.ScheduledQueryPayload{ - QueryID: ptr.Uint(cqr4.Query.ID), + QueryID: ptr.Uint(q1.ID), Interval: ptr.Uint(60), }, }, http.StatusOK, &ttsqr) diff --git a/server/service/osquery.go b/server/service/osquery.go index 64ba71342e..f4572f43e2 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -346,6 +346,24 @@ func getClientConfigEndpoint(ctx context.Context, request interface{}, svc fleet }, nil } +func (svc *Service) getScheduledQueries(ctx context.Context, teamID *uint) (fleet.Queries, error) { + queries, err := svc.ds.ListScheduledQueriesForAgents(ctx, teamID) + if err != nil { + return nil, err + } + + if len(queries) == 0 { + return nil, nil + } + + config := make(fleet.Queries, len(queries)) + for _, query := range queries { + config[query.Name] = query.ToQueryContent() + } + + return config, nil +} + func (svc *Service) GetClientConfig(ctx context.Context) (map[string]interface{}, error) { // skipauth: Authorization is currently for user endpoints only. svc.authz.SkipAuthorization(ctx) @@ -368,12 +386,12 @@ func (svc *Service) GetClientConfig(ctx context.Context) (map[string]interface{} } } + packConfig := fleet.Packs{} + packs, err := svc.ds.ListPacksForHost(ctx, host.ID) if err != nil { return nil, newOsqueryError("database error: " + err.Error()) } - - packConfig := fleet.Packs{} for _, pack := range packs { // first, we must figure out what queries are in this pack queries, err := svc.ds.ListScheduledQueriesInPack(ctx, pack.ID) @@ -414,6 +432,29 @@ func (svc *Service) GetClientConfig(ctx context.Context) (map[string]interface{} } } + globalQueries, err := svc.getScheduledQueries(ctx, nil) + if err != nil { + return nil, newOsqueryError("database error: " + err.Error()) + } + if len(globalQueries) > 0 { + packConfig["Global"] = fleet.PackContent{ + Queries: globalQueries, + } + } + + if host.TeamID != nil { + teamQueries, err := svc.getScheduledQueries(ctx, host.TeamID) + if err != nil { + return nil, newOsqueryError("database error: " + err.Error()) + } + if len(teamQueries) > 0 { + packName := fmt.Sprintf("team-%d", *host.TeamID) + packConfig[packName] = fleet.PackContent{ + Queries: teamQueries, + } + } + } + if len(packConfig) > 0 { packJSON, err := json.Marshal(packConfig) if err != nil { diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 625bb8761e..e01e0b4256 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -40,6 +40,10 @@ import ( func TestGetClientConfig(t *testing.T) { ds := new(mock.Store) + + ds.TeamAgentOptionsFunc = func(ctx context.Context, teamID uint) (*json.RawMessage, error) { + return nil, nil + } ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { return []*fleet.Pack{}, nil } @@ -61,6 +65,30 @@ func TestGetClientConfig(t *testing.T) { return []*fleet.ScheduledQuery{}, nil } } + ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { + if teamID == nil { + return nil, nil + } + return []*fleet.Query{ + { + Query: "SELECT 1 FROM table_1", + Name: "Some strings carry more weight than others", + Interval: 10, + Platform: "linux", + MinOsqueryVersion: "5.12.2", + Logging: "snapshot", + TeamID: ptr.Uint(1), + }, + { + Query: "SELECT 1 FROM table_2", + Name: "You shall not pass", + Interval: 20, + Platform: "macos", + Logging: "differential", + TeamID: ptr.Uint(1), + }, + }, nil + } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{AgentOptions: ptr.RawMessage(json.RawMessage(`{"config":{"options":{"baz":"bar"}}}`))}, nil } @@ -78,6 +106,7 @@ func TestGetClientConfig(t *testing.T) { ctx1 := hostctx.NewContext(ctx, &fleet.Host{ID: 1}) ctx2 := hostctx.NewContext(ctx, &fleet.Host{ID: 2}) + ctx3 := hostctx.NewContext(ctx, &fleet.Host{ID: 1, TeamID: ptr.Uint(1)}) expectedOptions := map[string]interface{}{ "baz": "bar", @@ -144,6 +173,43 @@ func TestGetClientConfig(t *testing.T) { }`, string(conf["packs"].(json.RawMessage)), ) + + // Check scheduled queries are loaded properly + conf, err = svc.GetClientConfig(ctx3) + require.NoError(t, err) + assert.JSONEq(t, `{ + "pack_by_label": { + "queries":{ + "time":{"query":"select * from time","interval":30,"removed":false} + } + }, + "pack_by_other_label": { + "queries": { + "foobar":{"query":"select 3","interval":20,"shard":42}, + "froobing":{"query":"select 'guacamole'","interval":60,"snapshot":true} + } + }, + "team-1": { + "queries": { + "Some strings carry more weight than others": { + "query": "SELECT 1 FROM table_1", + "interval": 10, + "platform": "linux", + "version": "5.12.2", + "snapshot": true + }, + "You shall not pass": { + "query": "SELECT 1 FROM table_2", + "interval": 20, + "platform": "macos", + "removed": true, + "version": "" + } + } + } + }`, + string(conf["packs"].(json.RawMessage)), + ) } func TestAgentOptionsForHost(t *testing.T) { @@ -1934,9 +2000,16 @@ func TestUpdateHostIntervals(t *testing.T) { svc, ctx := newTestService(t, ds, nil, nil) + ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { + return nil, nil + } + ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { return []*fleet.Pack{}, nil } + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { + return nil, nil + } testCases := []struct { name string diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index cf7d84ea33..c4f4f4ce21 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -1151,7 +1151,7 @@ func directIngestScheduledQueryStats(ctx context.Context, logger log.Logger, hos }, ) } - if err := task.RecordScheduledQueryStats(ctx, host.ID, packStats, time.Now()); err != nil { + if err := task.RecordScheduledQueryStats(ctx, host.TeamID, host.ID, packStats, time.Now()); err != nil { return ctxerr.Wrap(ctx, err, "record host pack stats") } diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index 4ed53b4ce9..bfb0afecac 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -55,7 +55,7 @@ func TestDetailQueryScheduledQueryStats(t *testing.T) { task := async.NewTask(ds, nil, clock.C, config.OsqueryConfig{EnableAsyncHostProcessing: "false"}) var gotPackStats []fleet.PackStats - ds.SaveHostPackStatsFunc = func(ctx context.Context, hostID uint, stats []fleet.PackStats) error { + ds.SaveHostPackStatsFunc = func(ctx context.Context, teamID *uint, hostID uint, stats []fleet.PackStats) error { if hostID != host.ID { return errors.New("not found") } diff --git a/server/service/packs_test.go b/server/service/packs_test.go index b378a33b53..b07e67e22a 100644 --- a/server/service/packs_test.go +++ b/server/service/packs_test.go @@ -72,7 +72,6 @@ func TestPacksWithDS(t *testing.T) { name string fn func(t *testing.T, ds *mysql.Datastore) }{ - {"ModifyPack", testPacksModifyPack}, {"ListPacks", testPacksListPacks}, {"DeletePack", testPacksDeletePack}, {"DeletePackByID", testPacksDeletePackByID}, @@ -86,35 +85,6 @@ func TestPacksWithDS(t *testing.T) { } } -func testPacksModifyPack(t *testing.T, ds *mysql.Datastore) { - svc, ctx := newTestService(t, ds, nil, nil) - test.AddAllHostsLabel(t, ds) - users := createTestUsers(t, ds) - - globalPack, err := ds.EnsureGlobalPack(ctx) - require.NoError(t, err) - - labelids := []uint{1, 2, 3} - hostids := []uint{4, 5, 6} - teamids := []uint{7, 8, 9} - packPayload := fleet.PackPayload{ - Name: ptr.String("foo"), - Description: ptr.String("bar"), - LabelIDs: &labelids, - HostIDs: &hostids, - TeamIDs: &teamids, - } - - user := users["admin1@example.com"] - pack, _ := svc.ModifyPack(test.UserContext(ctx, &user), globalPack.ID, packPayload) - - require.Equal(t, "Global", pack.Name, "name for global pack should not change") - require.Equal(t, "Global pack", pack.Description, "description for global pack should not change") - require.Len(t, pack.LabelIDs, 1) - require.Len(t, pack.HostIDs, 0) - require.Len(t, pack.TeamIDs, 0) -} - func testPacksListPacks(t *testing.T, ds *mysql.Datastore) { svc, ctx := newTestService(t, ds, nil, nil) @@ -135,22 +105,9 @@ func testPacksListPacks(t *testing.T, ds *mysql.Datastore) { func testPacksDeletePack(t *testing.T, ds *mysql.Datastore) { test.AddAllHostsLabel(t, ds) - gp, err := ds.EnsureGlobalPack(context.Background()) - require.NoError(t, err) - users := createTestUsers(t, ds) user := users["admin1@example.com"] - team1, err := ds.NewTeam(context.Background(), &fleet.Team{ - ID: 42, - Name: "team1", - Description: "desc team1", - }) - require.NoError(t, err) - - tp, err := ds.EnsureTeamPack(context.Background(), team1.ID) - require.NoError(t, err) - type args struct { ctx context.Context name string @@ -160,22 +117,6 @@ func testPacksDeletePack(t *testing.T, ds *mysql.Datastore) { args args wantErr bool }{ - { - name: "cannot delete global pack", - args: args{ - ctx: test.UserContext(context.Background(), &user), - name: gp.Name, - }, - wantErr: true, - }, - { - name: "cannot delete team pack", - args: args{ - ctx: test.UserContext(context.Background(), &user), - name: tp.Name, - }, - wantErr: true, - }, { name: "delete pack that doesn't exist", args: args{ @@ -198,9 +139,6 @@ func testPacksDeletePack(t *testing.T, ds *mysql.Datastore) { func testPacksDeletePackByID(t *testing.T, ds *mysql.Datastore) { test.AddAllHostsLabel(t, ds) - globalPack, err := ds.EnsureGlobalPack(context.Background()) - require.NoError(t, err) - type args struct { ctx context.Context id uint @@ -211,10 +149,10 @@ func testPacksDeletePackByID(t *testing.T, ds *mysql.Datastore) { wantErr bool }{ { - name: "cannot delete global pack", + name: "cannot delete pack that doesn't exists", args: args{ ctx: test.UserContext(context.Background(), test.UserAdmin), - id: globalPack.ID, + id: 123456, }, wantErr: true, }, @@ -232,22 +170,9 @@ func testPacksDeletePackByID(t *testing.T, ds *mysql.Datastore) { func testPacksApplyPackSpecs(t *testing.T, ds *mysql.Datastore) { test.AddAllHostsLabel(t, ds) - global, err := ds.EnsureGlobalPack(context.Background()) - require.NoError(t, err) - users := createTestUsers(t, ds) user := users["admin1@example.com"] - team1, err := ds.NewTeam(context.Background(), &fleet.Team{ - ID: 42, - Name: "team1", - Description: "desc team1", - }) - require.NoError(t, err) - - teamPack, err := ds.EnsureTeamPack(context.Background(), team1.ID) - require.NoError(t, err) - type args struct { ctx context.Context specs []*fleet.PackSpec @@ -263,7 +188,6 @@ func testPacksApplyPackSpecs(t *testing.T, ds *mysql.Datastore) { args: args{ ctx: test.UserContext(context.Background(), &user), specs: []*fleet.PackSpec{ - {Name: global.Name, Description: "bar", Platform: "baz"}, {Name: "Foo Pack", Description: "Foo Desc", Platform: "MacOS"}, {Name: "Bar Pack", Description: "Bar Desc", Platform: "MacOS"}, }, @@ -279,7 +203,6 @@ func testPacksApplyPackSpecs(t *testing.T, ds *mysql.Datastore) { args: args{ ctx: test.UserContext(context.Background(), &user), specs: []*fleet.PackSpec{ - {Name: teamPack.Name, Description: "Desc", Platform: "windows"}, {Name: "Test", Description: "Test Desc", Platform: "linux"}, }, }, diff --git a/server/service/queries.go b/server/service/queries.go index 94a2287278..a911790e90 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -2,8 +2,6 @@ package service import ( "context" - "database/sql" - "errors" "fmt" "github.com/fleetdm/fleet/v4/server/authz" @@ -39,11 +37,16 @@ func getQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Servic } func (svc *Service) GetQuery(ctx context.Context, id uint) (*fleet.Query, error) { - if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionRead); err != nil { + // 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 } - - return svc.ds.Query(ctx, id) + return query, nil } //////////////////////////////////////////////////////////////////////////////// @@ -52,6 +55,8 @@ func (svc *Service) GetQuery(ctx context.Context, id uint) (*fleet.Query, error) type listQueriesRequest struct { ListOptions fleet.ListOptions `url:"list_options"` + // TeamID url argument set to 0 means global. + TeamID uint `query:"team_id,optional"` } type listQueriesResponse struct { @@ -63,29 +68,42 @@ func (r listQueriesResponse) error() error { return r.Err } func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*listQueriesRequest) - queries, err := svc.ListQueries(ctx, req.ListOptions) + + var teamID *uint + if req.TeamID != 0 { + teamID = &req.TeamID + } + + queries, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil) if err != nil { return listQueriesResponse{Err: err}, nil } - resp := listQueriesResponse{Queries: []fleet.Query{}} + respQueries := make([]fleet.Query, 0, len(queries)) for _, query := range queries { - resp.Queries = append(resp.Queries, *query) + respQueries = append(respQueries, *query) } - return resp, nil + return listQueriesResponse{ + Queries: respQueries, + }, nil } -func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions) ([]*fleet.Query, error) { - if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionRead); err != nil { +func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool) ([]*fleet.Query, error) { + // Check the user is allowed to list queries on the given team. + if err := svc.authz.Authorize(ctx, &fleet.Query{ + TeamID: teamID, + }, fleet.ActionRead); err != nil { return nil, err } user := authz.UserFromContext(ctx) - onlyShowObserverCanRun := onlyShowObserverCanRunQueries(user) + onlyShowObserverCanRun := onlyShowObserverCanRunQueries(user, teamID) queries, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{ ListOptions: opt, OnlyObserverCanRun: onlyShowObserverCanRun, + TeamID: teamID, + IsScheduled: scheduled, }) if err != nil { return nil, err @@ -94,20 +112,14 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions) ([]* return queries, nil } -func onlyShowObserverCanRunQueries(user *fleet.User) bool { +func onlyShowObserverCanRunQueries(user *fleet.User, teamID *uint) bool { if user.GlobalRole != nil && *user.GlobalRole == fleet.RoleObserver { return true - } else if len(user.Teams) > 0 { - allObserver := true - for _, team := range user.Teams { - if team.Role != fleet.RoleObserver { - allObserver = false - break - } - } - return allObserver } - return false + + return teamID != nil && user.TeamMembership(func(ut fleet.UserTeam) bool { + return ut.Role == fleet.RoleObserver + })[*teamID] } //////////////////////////////////////////////////////////////////////////////// @@ -135,12 +147,10 @@ func createQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Ser } func (svc *Service) NewQuery(ctx context.Context, p fleet.QueryPayload) (*fleet.Query, error) { - user := authz.UserFromContext(ctx) - q := &fleet.Query{} - if user != nil { - q.AuthorID = ptr.Uint(user.ID) - } - if err := svc.authz.Authorize(ctx, q, fleet.ActionWrite); err != nil { + // Check the user is allowed to create a new query on the team. + if err := svc.authz.Authorize(ctx, fleet.Query{ + TeamID: p.TeamID, + }, fleet.ActionWrite); err != nil { return nil, err } @@ -150,26 +160,42 @@ func (svc *Service) NewQuery(ctx context.Context, p fleet.QueryPayload) (*fleet. }) } - query := &fleet.Query{Saved: true} + query := &fleet.Query{ + Saved: true, + + TeamID: p.TeamID, + } if p.Name != nil { query.Name = *p.Name } - if p.Description != nil { query.Description = *p.Description } - if p.Query != nil { query.Query = *p.Query } - - logging.WithExtras(ctx, "name", query.Name, "sql", query.Query) - + if p.Interval != nil { + query.Interval = *p.Interval + } + if p.Platform != nil { + query.Platform = *p.Platform + } + if p.MinOsqueryVersion != nil { + query.MinOsqueryVersion = *p.MinOsqueryVersion + } + if p.AutomationsEnabled != nil { + query.AutomationsEnabled = *p.AutomationsEnabled + } + if p.Logging != nil { + query.Logging = *p.Logging + } if p.ObserverCanRun != nil { query.ObserverCanRun = *p.ObserverCanRun } + logging.WithExtras(ctx, "name", query.Name, "sql", query.Query) + vc, ok := viewer.FromContext(ctx) if ok { query.AuthorID = ptr.Uint(vc.UserID()) @@ -222,12 +248,12 @@ 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) if err != nil { setAuthCheckedOnPreAuthErr(ctx) return nil, err } - if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil { return nil, err } @@ -241,21 +267,33 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo if p.Name != nil { query.Name = *p.Name } - if p.Description != nil { query.Description = *p.Description } - if p.Query != nil { query.Query = *p.Query } - - logging.WithExtras(ctx, "name", query.Name, "sql", query.Query) - + if p.Interval != nil { + query.Interval = *p.Interval + } + if p.Platform != nil { + query.Platform = *p.Platform + } + if p.MinOsqueryVersion != nil { + query.MinOsqueryVersion = *p.MinOsqueryVersion + } + if p.AutomationsEnabled != nil { + query.AutomationsEnabled = *p.AutomationsEnabled + } + if p.Logging != nil { + query.Logging = *p.Logging + } if p.ObserverCanRun != nil { query.ObserverCanRun = *p.ObserverCanRun } + logging.WithExtras(ctx, "name", query.Name, "sql", query.Query) + if err := svc.ds.SaveQuery(ctx, query); err != nil { return nil, err } @@ -280,6 +318,8 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo type deleteQueryRequest struct { Name string `url:"name"` + // TeamID if not set is assumed to be 0 (global). + TeamID uint `url:"team_id,optional"` } type deleteQueryResponse struct { @@ -290,25 +330,29 @@ func (r deleteQueryResponse) error() error { return r.Err } func deleteQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*deleteQueryRequest) - err := svc.DeleteQuery(ctx, req.Name) + var teamID *uint + if req.TeamID != 0 { + teamID = &req.TeamID + } + err := svc.DeleteQuery(ctx, teamID, req.Name) if err != nil { return deleteQueryResponse{Err: err}, nil } return deleteQueryResponse{}, nil } -func (svc *Service) DeleteQuery(ctx context.Context, name string) error { - query, err := svc.ds.QueryByName(ctx, name) +func (svc *Service) DeleteQuery(ctx context.Context, teamID *uint, name string) error { + // Load query first to determine if the user can delete it. + query, err := svc.ds.QueryByName(ctx, teamID, name) if err != nil { setAuthCheckedOnPreAuthErr(ctx) return err } - if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil { return err } - if err := svc.ds.DeleteQuery(ctx, name); err != nil { + if err := svc.ds.DeleteQuery(ctx, teamID, name); err != nil { return err } @@ -348,17 +392,17 @@ func deleteQueryByIDEndpoint(ctx context.Context, request interface{}, svc fleet } func (svc *Service) DeleteQueryByID(ctx context.Context, id uint) error { + // Load query first to determine if the user can delete it. query, err := svc.ds.Query(ctx, id) if err != nil { setAuthCheckedOnPreAuthErr(ctx) return ctxerr.Wrap(ctx, err, "lookup query by ID") } - if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil { return err } - if err := svc.ds.DeleteQuery(ctx, query.Name); err != nil { + if err := svc.ds.DeleteQuery(ctx, query.TeamID, query.Name); err != nil { return ctxerr.Wrap(ctx, err, "delete query") } @@ -399,13 +443,13 @@ func deleteQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.S } func (svc *Service) DeleteQueries(ctx context.Context, ids []uint) (uint, error) { + // Verify that the user is allowed to delete all the requested queries. for _, id := range ids { query, err := svc.ds.Query(ctx, id) if err != nil { setAuthCheckedOnPreAuthErr(ctx) return 0, ctxerr.Wrap(ctx, err, "lookup query by ID") } - if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil { return 0, err } @@ -429,7 +473,7 @@ func (svc *Service) DeleteQueries(ctx context.Context, ids []uint) (uint, error) } //////////////////////////////////////////////////////////////////////////////// -// Apply Query Spec +// Apply Query Specs //////////////////////////////////////////////////////////////////////////////// type applyQuerySpecsRequest struct { @@ -452,38 +496,32 @@ func applyQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet } func (svc *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpec) error { - if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionWrite); err != nil { - return err - } - + // 1. Turn specs into queries. queries := []*fleet.Query{} for _, spec := range specs { - queries = append(queries, queryFromSpec(spec)) + query, err := svc.queryFromSpec(ctx, spec) + if err != nil { + setAuthCheckedOnPreAuthErr(ctx) + return ctxerr.Wrap(ctx, err, "creating query from spec") + } + queries = append(queries, query) } - + // 2. Run authorization checks and verify their fields. for _, query := range queries { + if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil { + return err + } if err := query.Verify(); err != nil { return ctxerr.Wrap(ctx, &fleet.BadRequestError{ Message: fmt.Sprintf("query payload verification: %s", err), }) } - - // check that the user can update the query if it already exists - query, err := svc.ds.QueryByName(ctx, query.Name) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err - } else if err == nil { - if err := svc.authz.Authorize(ctx, query, fleet.ActionWrite); err != nil { - return err - } - } } - + // 3. Apply the queries. 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) if err != nil { return ctxerr.Wrap(ctx, err, "applying queries") @@ -501,12 +539,28 @@ func (svc *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpe return nil } -func queryFromSpec(spec *fleet.QuerySpec) *fleet.Query { +func (svc *Service) queryFromSpec(ctx context.Context, spec *fleet.QuerySpec) (*fleet.Query, error) { + var teamID *uint + if spec.TeamName != "" { + team, err := svc.ds.TeamByName(ctx, spec.TeamName) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get team by name") + } + teamID = &team.ID + } return &fleet.Query{ Name: spec.Name, Description: spec.Description, Query: spec.Query, - } + + TeamID: teamID, + Interval: spec.Interval, + ObserverCanRun: spec.ObserverCanRun, + Platform: spec.Platform, + MinOsqueryVersion: spec.MinOsqueryVersion, + AutomationsEnabled: spec.AutomationsEnabled, + Logging: spec.Logging, + }, nil } //////////////////////////////////////////////////////////////////////////////// @@ -518,39 +572,65 @@ type getQuerySpecsResponse struct { Err error `json:"error,omitempty"` } +type getQuerySpecsRequest struct { + TeamID uint `url:"team_id,optional"` +} + func (r getQuerySpecsResponse) error() error { return r.Err } func getQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - specs, err := svc.GetQuerySpecs(ctx) + req := request.(*getQuerySpecsRequest) + var teamID *uint + if req.TeamID != 0 { + teamID = &req.TeamID + } + specs, err := svc.GetQuerySpecs(ctx, teamID) if err != nil { return getQuerySpecsResponse{Err: err}, nil } return getQuerySpecsResponse{Specs: specs}, nil } -func (svc *Service) GetQuerySpecs(ctx context.Context) ([]*fleet.QuerySpec, error) { - if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionRead); err != nil { - return nil, err - } - - queries, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{}) +func (svc *Service) GetQuerySpecs(ctx context.Context, teamID *uint) ([]*fleet.QuerySpec, error) { + queries, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting queries") } - specs := []*fleet.QuerySpec{} + // Turn queries into specs. + var specs []*fleet.QuerySpec for _, query := range queries { - specs = append(specs, specFromQuery(query)) + spec, err := svc.specFromQuery(ctx, query) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "create spec from query") + } + specs = append(specs, spec) } return specs, nil } -func specFromQuery(query *fleet.Query) *fleet.QuerySpec { +func (svc *Service) specFromQuery(ctx context.Context, query *fleet.Query) (*fleet.QuerySpec, error) { + var teamName string + if query.TeamID != nil { + team, err := svc.ds.Team(ctx, *query.TeamID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get team from id") + } + teamName = team.Name + } return &fleet.QuerySpec{ Name: query.Name, Description: query.Description, Query: query.Query, - } + + TeamName: teamName, + Interval: query.Interval, + ObserverCanRun: query.ObserverCanRun, + Platform: query.Platform, + MinOsqueryVersion: query.MinOsqueryVersion, + AutomationsEnabled: query.AutomationsEnabled, + Logging: query.Logging, + }, nil } //////////////////////////////////////////////////////////////////////////////// @@ -562,25 +642,41 @@ type getQuerySpecResponse struct { Err error `json:"error,omitempty"` } +type getQuerySpecRequest struct { + Name string `url:"name"` + TeamID uint `query:"team_id,optional"` +} + func (r getQuerySpecResponse) error() error { return r.Err } func getQuerySpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - req := request.(*getGenericSpecRequest) - spec, err := svc.GetQuerySpec(ctx, req.Name) + req := request.(*getQuerySpecRequest) + var teamID *uint + if req.TeamID != 0 { + teamID = &req.TeamID + } + spec, err := svc.GetQuerySpec(ctx, teamID, req.Name) if err != nil { return getQuerySpecResponse{Err: err}, nil } return getQuerySpecResponse{Spec: spec}, nil } -func (svc *Service) GetQuerySpec(ctx context.Context, name string) (*fleet.QuerySpec, error) { - if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionRead); err != nil { +func (svc *Service) GetQuerySpec(ctx context.Context, teamID *uint, name string) (*fleet.QuerySpec, error) { + // Check the user is allowed to get the query on the requested team. + if err := svc.authz.Authorize(ctx, &fleet.Query{ + TeamID: teamID, + }, fleet.ActionRead); err != nil { return nil, err } - query, err := svc.ds.QueryByName(ctx, name) + query, err := svc.ds.QueryByName(ctx, teamID, name) if err != nil { - return nil, err + return nil, ctxerr.Wrap(ctx, err, "get query by name") } - return specFromQuery(query), nil + spec, err := svc.specFromQuery(ctx, query) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "create spec from query") + } + return spec, nil } diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 083657a123..7477e5fa4c 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -13,19 +13,63 @@ import ( ) func TestFilterQueriesForObserver(t *testing.T) { - require.True(t, onlyShowObserverCanRunQueries(&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)})) - require.False(t, onlyShowObserverCanRunQueries(&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)})) - require.False(t, onlyShowObserverCanRunQueries(&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)})) + t.Run("global role", func(t *testing.T) { + require.True(t, onlyShowObserverCanRunQueries(&fleet.User{ + GlobalRole: ptr.String(fleet.RoleObserver), + }, nil)) - require.True(t, onlyShowObserverCanRunQueries(&fleet.User{Teams: []fleet.UserTeam{{Role: fleet.RoleObserver}}})) - require.True(t, onlyShowObserverCanRunQueries(&fleet.User{Teams: []fleet.UserTeam{ - {Role: fleet.RoleObserver}, - {Role: fleet.RoleObserver}, - }})) - require.False(t, onlyShowObserverCanRunQueries(&fleet.User{Teams: []fleet.UserTeam{ - {Role: fleet.RoleObserver}, - {Role: fleet.RoleMaintainer}, - }})) + require.False(t, onlyShowObserverCanRunQueries(&fleet.User{ + GlobalRole: ptr.String(fleet.RoleObserverPlus), + }, nil)) + + require.False(t, onlyShowObserverCanRunQueries(&fleet.User{ + GlobalRole: ptr.String(fleet.RoleMaintainer), + }, nil)) + + require.False(t, onlyShowObserverCanRunQueries(&fleet.User{ + GlobalRole: ptr.String(fleet.RoleAdmin), + }, nil)) + }) + + t.Run("user belongs to one or more teams", func(t *testing.T) { + require.True(t, onlyShowObserverCanRunQueries(&fleet.User{Teams: []fleet.UserTeam{{ + Role: fleet.RoleObserver, + Team: fleet.Team{ID: 1}, + }}}, ptr.Uint(1))) + + require.True(t, onlyShowObserverCanRunQueries(&fleet.User{Teams: []fleet.UserTeam{ + { + Role: fleet.RoleObserver, + Team: fleet.Team{ID: 1}, + }, + { + Role: fleet.RoleObserver, + Team: fleet.Team{ID: 2}, + }, + }}, ptr.Uint(2))) + + require.True(t, onlyShowObserverCanRunQueries(&fleet.User{Teams: []fleet.UserTeam{ + { + Role: fleet.RoleObserver, + Team: fleet.Team{ID: 1}, + }, + { + Role: fleet.RoleMaintainer, + Team: fleet.Team{ID: 2}, + }, + }}, ptr.Uint(1))) + + require.False(t, onlyShowObserverCanRunQueries(&fleet.User{Teams: []fleet.UserTeam{ + { + Role: fleet.RoleObserver, + Team: fleet.Team{ID: 1}, + }, + { + Role: fleet.RoleMaintainer, + Team: fleet.Team{ID: 2}, + }, + }}, ptr.Uint(2))) + }) } func TestListQueries(t *testing.T) { @@ -63,7 +107,7 @@ func TestListQueries(t *testing.T) { for _, tt := range cases { t.Run(tt.title, func(t *testing.T) { viewerCtx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) - _, err := svc.ListQueries(viewerCtx, fleet.ListOptions{}) + _, err := svc.ListQueries(viewerCtx, fleet.ListOptions{}, nil, nil) require.NoError(t, err) assert.Equal(t, tt.expectedOpts, calledWithOpts) }) @@ -74,36 +118,105 @@ func TestQueryAuth(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) - authoredQueryID := uint(1) - authoredQueryName := "authored" - queryName := map[uint]string{ - authoredQueryID: authoredQueryName, - 2: "not authored", + team := fleet.Team{ + ID: 1, + Name: "Foobar", + } + teamAdmin := &fleet.User{ + ID: 42, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: team.ID}, + Role: fleet.RoleAdmin, + }, + }, + } + teamMaintainer := &fleet.User{ + ID: 43, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: team.ID}, + Role: fleet.RoleMaintainer, + }, + }, + } + teamObserver := &fleet.User{ + ID: 44, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: team.ID}, + Role: fleet.RoleObserver, + }, + }, + } + teamObserverPlus := &fleet.User{ + ID: 45, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: team.ID}, + Role: fleet.RoleObserverPlus, + }, + }, + } + teamGitOps := &fleet.User{ + ID: 46, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: team.ID}, + Role: fleet.RoleGitOps, + }, + }, + } + globalQuery := fleet.Query{ + ID: 99, + Name: "global query", + TeamID: nil, + } + teamQuery := fleet.Query{ + ID: 88, + Name: "team query", + TeamID: ptr.Uint(team.ID), + } + queriesMap := map[uint]fleet.Query{ + globalQuery.ID: globalQuery, + teamQuery.ID: teamQuery, } - teamMaintainer := &fleet.User{ID: 42, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}} + ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { + return &team, nil + } + ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { + if name == team.Name { + return &team, nil + } + return nil, newNotFoundError() + } ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) { return query, nil } - ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { - if name == authoredQueryName { - return &fleet.Query{ID: 99, AuthorID: ptr.Uint(teamMaintainer.ID)}, nil + ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { + if teamID == nil && name == "global query" { + return &globalQuery, nil + } else if teamID != nil && *teamID == team.ID && name == "team query" { + return &teamQuery, nil } - return &fleet.Query{ID: 8888, AuthorID: ptr.Uint(6666)}, nil + return nil, newNotFoundError() } ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) { - if id == authoredQueryID { - return &fleet.Query{ID: 99, AuthorID: ptr.Uint(teamMaintainer.ID)}, nil + if id == 99 { + return &globalQuery, nil + } else if id == 88 { + return &teamQuery, nil } - return &fleet.Query{ID: 8888, AuthorID: ptr.Uint(6666)}, nil + return nil, newNotFoundError() } ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query) error { return nil } - ds.DeleteQueryFunc = func(ctx context.Context, name string) error { + ds.DeleteQueryFunc = func(ctx context.Context, teamID *uint, name string) error { return nil } ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) { @@ -125,65 +238,183 @@ func TestQueryAuth(t *testing.T) { shouldFailNew bool }{ { - "global admin", + "global admin and global query", &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, - authoredQueryID, + globalQuery.ID, false, false, false, }, { - "global maintainer", + "global admin and team query", + &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, + teamQuery.ID, + false, + false, + false, + }, + { + "global maintainer and global query", &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}, - authoredQueryID, + globalQuery.ID, false, false, false, }, { - "global observer", + "global maintainer and team query", + &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}, + teamQuery.ID, + false, + false, + false, + }, + { + "global observer and global query", &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)}, - authoredQueryID, + globalQuery.ID, true, false, true, }, { - "team maintainer, author of the query", + "global observer and team query", + &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)}, + teamQuery.ID, + true, + false, + true, + }, + { + "global observer+ and global query", + &fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)}, + globalQuery.ID, + true, + false, + true, + }, + { + "global observer+ and team query", + &fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)}, + teamQuery.ID, + true, + false, + true, + }, + { + "global gitops and global query", + &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}, + globalQuery.ID, + false, + true, + false, + }, + { + "global gitops and team query", + &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}, + teamQuery.ID, + false, + true, + false, + }, + { + "team admin and global query", + teamAdmin, + globalQuery.ID, + true, + false, + true, + }, + { + "team admin and team query", + teamAdmin, + teamQuery.ID, + false, + false, + false, + }, + { + "team maintainer and global query", teamMaintainer, - authoredQueryID, - false, - false, + globalQuery.ID, + true, false, + true, }, { - "team maintainer, NOT author of the query", + "team maintainer and team query", teamMaintainer, - 2, - true, + teamQuery.ID, + false, false, false, }, { - "team observer", - &fleet.User{ID: 48, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: authoredQueryID}, Role: fleet.RoleObserver}}}, - 2, + "team observer and global query", + teamObserver, + globalQuery.ID, true, false, true, }, + { + "team observer and team query", + teamObserver, + teamQuery.ID, + true, + false, + true, + }, + { + "team observer+ and global query", + teamObserverPlus, + globalQuery.ID, + true, + false, + true, + }, + { + "team observer+ and team query", + teamObserverPlus, + teamQuery.ID, + true, + false, + true, + }, + { + "team gitops and global query", + teamGitOps, + globalQuery.ID, + true, + true, + true, + }, + { + "team gitops and team query", + teamGitOps, + teamQuery.ID, + false, + true, + false, + }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) - _, err := svc.NewQuery(ctx, fleet.QueryPayload{Name: ptr.String("name"), Query: ptr.String("select 1")}) + query := queriesMap[tt.qid] + + _, err := svc.NewQuery(ctx, fleet.QueryPayload{ + Name: ptr.String("name"), + Query: ptr.String("select 1"), + TeamID: query.TeamID, + }) checkAuthErr(t, tt.shouldFailNew, err) _, err = svc.ModifyQuery(ctx, tt.qid, fleet.QueryPayload{}) checkAuthErr(t, tt.shouldFailWrite, err) - err = svc.DeleteQuery(ctx, queryName[tt.qid]) + err = svc.DeleteQuery(ctx, query.TeamID, query.Name) checkAuthErr(t, tt.shouldFailWrite, err) err = svc.DeleteQueryByID(ctx, tt.qid) @@ -195,16 +426,24 @@ func TestQueryAuth(t *testing.T) { _, err = svc.GetQuery(ctx, tt.qid) checkAuthErr(t, tt.shouldFailRead, err) - _, err = svc.ListQueries(ctx, fleet.ListOptions{}) + _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil) checkAuthErr(t, tt.shouldFailRead, err) - err = svc.ApplyQuerySpecs(ctx, []*fleet.QuerySpec{{Name: queryName[tt.qid], Query: "SELECT 1"}}) + teamName := "" + if query.TeamID != nil { + teamName = team.Name + } + err = svc.ApplyQuerySpecs(ctx, []*fleet.QuerySpec{{ + Name: query.Name, + Query: "SELECT 1", + TeamName: teamName, + }}) checkAuthErr(t, tt.shouldFailWrite, err) - _, err = svc.GetQuerySpecs(ctx) + _, err = svc.GetQuerySpecs(ctx, query.TeamID) checkAuthErr(t, tt.shouldFailRead, err) - _, err = svc.GetQuerySpec(ctx, queryName[tt.qid]) + _, err = svc.GetQuerySpec(ctx, query.TeamID, query.Name) checkAuthErr(t, tt.shouldFailRead, err) }) } diff --git a/server/service/scheduled_queries.go b/server/service/scheduled_queries.go index 8de0e5e7d4..381179f860 100644 --- a/server/service/scheduled_queries.go +++ b/server/service/scheduled_queries.go @@ -7,6 +7,12 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" ) +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// All API endpoints in this file are used for 2017 packs functionality. +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// // Get Scheduled Queries In Pack //////////////////////////////////////////////////////////////////////////////// @@ -46,7 +52,6 @@ func getScheduledQueriesInPackEndpoint(ctx context.Context, request interface{}, } func (svc *Service) GetScheduledQueriesInPack(ctx context.Context, id uint, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) { - // Scheduled queries are currently authorized the same as packs. if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil { return nil, err } diff --git a/server/service/scheduled_queries_test.go b/server/service/scheduled_queries_test.go index d7fa22f26f..c3d8b63ce1 100644 --- a/server/service/scheduled_queries_test.go +++ b/server/service/scheduled_queries_test.go @@ -60,6 +60,18 @@ func TestScheduledQueriesAuth(t *testing.T) { shouldFailWrite: true, shouldFailRead: true, }, + { + name: "global observer+", + user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)}, + shouldFailWrite: true, + shouldFailRead: true, + }, + { + name: "global gitops", + user: &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}, + shouldFailWrite: false, + shouldFailRead: false, // Global gitops can read packs (exception to the write only rule) + }, // Team users cannot read or write scheduled queries using the below service APIs. // Team users must use the "Team" endpoints (GetTeamScheduledQueries, TeamScheduleQuery, // ModifyTeamScheduledQueries and DeleteTeamScheduledQueries). @@ -81,6 +93,18 @@ func TestScheduledQueriesAuth(t *testing.T) { shouldFailWrite: true, shouldFailRead: true, }, + { + name: "team observer+", + user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}}, + shouldFailWrite: true, + shouldFailRead: true, + }, + { + name: "team gitops", + user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}}, + shouldFailWrite: true, + shouldFailRead: true, + }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { diff --git a/server/service/team_policies_test.go b/server/service/team_policies_test.go index e9e218e90d..5cb2b8402b 100644 --- a/server/service/team_policies_test.go +++ b/server/service/team_policies_test.go @@ -176,7 +176,8 @@ func TestTeamPoliciesAuth(t *testing.T) { func checkAuthErr(t *testing.T, shouldFail bool, err error) { if shouldFail { require.Error(t, err) - require.Equal(t, (&authz.Forbidden{}).Error(), err.Error()) + var forbiddenError *authz.Forbidden + require.ErrorAs(t, err, &forbiddenError) } else { require.NoError(t, err) } diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go index 80f3034842..24ad3cde3b 100644 --- a/server/service/team_schedule.go +++ b/server/service/team_schedule.go @@ -3,12 +3,18 @@ package service import ( "context" "fmt" + "time" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" "gopkg.in/guregu/null.v3" ) +///////////////////////////////////////////////////////////////////////////////// +// Get Scheduled Queries of a team. +///////////////////////////////////////////////////////////////////////////////// + type getTeamScheduleRequest struct { TeamID uint `url:"team_id"` ListOptions fleet.ListOptions `url:"list_options"` @@ -37,22 +43,23 @@ func getTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet } func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) { - if err := svc.authz.Authorize(ctx, &fleet.Pack{ - Type: ptr.String(fmt.Sprintf("team-%d", teamID)), - }, fleet.ActionRead); err != nil { - return nil, err + var teamID_ *uint + if teamID != 0 { + teamID_ = &teamID } - - gp, err := svc.ds.EnsureTeamPack(ctx, teamID) + queries, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true)) if err != nil { return nil, err } - - return svc.ds.ListScheduledQueriesInPackWithStats(ctx, gp.ID, opts) + scheduledQueries := make([]*fleet.ScheduledQuery, 0, len(queries)) + for _, query := range queries { + scheduledQueries = append(scheduledQueries, fleet.ScheduledQueryFromQuery(query)) + } + return scheduledQueries, nil } ///////////////////////////////////////////////////////////////////////////////// -// Add +// Add schedule query to a team. ///////////////////////////////////////////////////////////////////////////////// type teamScheduleQueryRequest struct { @@ -100,24 +107,31 @@ func teamScheduleQueryEndpoint(ctx context.Context, request interface{}, svc fle }, nil } -func (svc Service) TeamScheduleQuery(ctx context.Context, teamID uint, q *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) { - if err := svc.authz.Authorize(ctx, &fleet.Pack{ - Type: ptr.String(fmt.Sprintf("team-%d", teamID)), - }, fleet.ActionWrite); err != nil { - return nil, err - } +func nameForCopiedQuery(originalName string) string { + return "Copy of " + originalName + " (" + fmt.Sprintf("%d", time.Now().Unix()) + ")" +} - gp, err := svc.ds.EnsureTeamPack(ctx, teamID) +func (svc Service) TeamScheduleQuery(ctx context.Context, teamID uint, scheduledQuery *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) { + originalQuery, err := svc.ds.Query(ctx, scheduledQuery.QueryID) + if err != nil { + setAuthCheckedOnPreAuthErr(ctx) + return nil, ctxerr.Wrap(ctx, err, "get query from id") + } + if originalQuery.TeamID != nil { + setAuthCheckedOnPreAuthErr(ctx) + return nil, ctxerr.New(ctx, "cannot create a team schedule from a team query") + } + originalQuery.Name = nameForCopiedQuery(originalQuery.Name) + originalQuery.TeamID = &teamID + newQuery, err := svc.NewQuery(ctx, fleet.ScheduledQueryToQueryPayloadForNewQuery(originalQuery, scheduledQuery)) if err != nil { return nil, err } - q.PackID = gp.ID - - return svc.unauthorizedScheduleQuery(ctx, q) + return fleet.ScheduledQueryFromQuery(newQuery), nil } ///////////////////////////////////////////////////////////////////////////////// -// Modify +// Modify team scheduled query. ///////////////////////////////////////////////////////////////////////////////// type modifyTeamScheduleRequest struct { @@ -135,33 +149,29 @@ func (r modifyTeamScheduleResponse) error() error { return r.Err } func modifyTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*modifyTeamScheduleRequest) - resp, err := svc.ModifyTeamScheduledQueries(ctx, req.TeamID, req.ScheduledQueryID, req.ScheduledQueryPayload) - if err != nil { + if _, err := svc.ModifyTeamScheduledQueries(ctx, req.TeamID, req.ScheduledQueryID, req.ScheduledQueryPayload); err != nil { return modifyTeamScheduleResponse{Err: err}, nil } - _ = resp return modifyTeamScheduleResponse{}, nil } -func (svc Service) ModifyTeamScheduledQueries(ctx context.Context, teamID uint, scheduledQueryID uint, query fleet.ScheduledQueryPayload) (*fleet.ScheduledQuery, error) { - if err := svc.authz.Authorize(ctx, &fleet.Pack{ - Type: ptr.String(fmt.Sprintf("team-%d", teamID)), - }, fleet.ActionWrite); err != nil { - return nil, err - } - - gp, err := svc.ds.EnsureTeamPack(ctx, teamID) +// TODO(lucas): Document new behavior. +// teamID is not used because of mismatch between old internal representation and API. +func (svc Service) ModifyTeamScheduledQueries( + ctx context.Context, + teamID uint, + scheduledQueryID uint, + scheduledQueryPayload fleet.ScheduledQueryPayload, +) (*fleet.ScheduledQuery, error) { + query, err := svc.ModifyQuery(ctx, scheduledQueryID, fleet.ScheduledQueryPayloadToQueryPayloadForModifyQuery(scheduledQueryPayload)) if err != nil { return nil, err } - - query.PackID = ptr.Uint(gp.ID) - - return svc.unauthorizedModifyScheduledQuery(ctx, scheduledQueryID, query) + return fleet.ScheduledQueryFromQuery(query), nil } ///////////////////////////////////////////////////////////////////////////////// -// Delete +// Delete a scheduled query from a team. ///////////////////////////////////////////////////////////////////////////////// type deleteTeamScheduleRequest struct { @@ -185,11 +195,8 @@ func deleteTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fl return deleteTeamScheduleResponse{}, nil } +// TODO(lucas): Document new behavior. +// teamID is not used because of mismatch between old internal representation and API. func (svc Service) DeleteTeamScheduledQueries(ctx context.Context, teamID uint, scheduledQueryID uint) error { - if err := svc.authz.Authorize(ctx, &fleet.Pack{ - Type: ptr.String(fmt.Sprintf("team-%d", teamID)), - }, fleet.ActionWrite); err != nil { - return err - } - return svc.ds.DeleteScheduledQuery(ctx, scheduledQueryID) + return svc.DeleteQueryByID(ctx, scheduledQueryID) } diff --git a/server/service/team_schedule_test.go b/server/service/team_schedule_test.go index 7b09d55c48..ab966e5be6 100644 --- a/server/service/team_schedule_test.go +++ b/server/service/team_schedule_test.go @@ -2,7 +2,6 @@ package service import ( "context" - "fmt" "testing" "github.com/fleetdm/fleet/v4/server/contexts/viewer" @@ -15,28 +14,35 @@ func TestTeamScheduleAuth(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) - ds.EnsureTeamPackFunc = func(ctx context.Context, teamID uint) (*fleet.Pack, error) { - return &fleet.Pack{ - ID: 999, - Type: ptr.String(fmt.Sprintf("team-%d", teamID)), - }, nil - } - ds.ListScheduledQueriesInPackWithStatsFunc = func(ctx context.Context, id uint, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) { + if id == 99 { // for testing modify and delete of a schedule + return &fleet.Query{ + Name: "foobar", + Query: "SELECT 1;", + TeamID: ptr.Uint(1), + }, nil + } + return &fleet.Query{ // for testing creation of a schedule + Name: "foobar", + Query: "SELECT 1;", + // TeamID is set to nil because a query must be global to be able to be + // scheduled on a team by the deprecated APIs. + TeamID: nil, + }, nil + } + ds.SaveQueryFunc = func(ctx context.Context, query *fleet.Query) error { + return nil + } + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + return nil + } + ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) { return &fleet.Query{}, nil } - ds.ScheduledQueryFunc = func(ctx context.Context, id uint) (*fleet.ScheduledQuery, error) { - return &fleet.ScheduledQuery{}, nil - } - ds.NewScheduledQueryFunc = func(ctx context.Context, sq *fleet.ScheduledQuery, opts ...fleet.OptionalArg) (*fleet.ScheduledQuery, error) { - return sq, nil - } - ds.SaveScheduledQueryFunc = func(ctx context.Context, sq *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) { - return sq, nil - } - ds.DeleteScheduledQueryFunc = func(ctx context.Context, id uint) error { + ds.DeleteQueryFunc = func(ctx context.Context, teamID *uint, name string) error { return nil } @@ -48,7 +54,9 @@ func TestTeamScheduleAuth(t *testing.T) { }{ { "global admin", - &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, + &fleet.User{ + GlobalRole: ptr.String(fleet.RoleAdmin), + }, false, false, }, @@ -62,11 +70,28 @@ func TestTeamScheduleAuth(t *testing.T) { "global observer", &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)}, true, + false, // global observer can view all queries and scheduled queries. + }, + { + "global observer+", + &fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)}, + true, + false, // global observer+ can view all queries and scheduled queries. + }, + { + "global gitops", + &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}, + false, true, }, { "team admin, belongs to team", - &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}, + &fleet.User{ + Teams: []fleet.UserTeam{{ + Team: fleet.Team{ID: 1}, + Role: fleet.RoleAdmin, + }}, + }, false, false, }, @@ -82,6 +107,18 @@ func TestTeamScheduleAuth(t *testing.T) { true, false, }, + { + "team observer+, belongs to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}}, + true, + false, + }, + { + "team gitops, belongs to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}}, + false, + true, + }, { "team maintainer, DOES NOT belong to team", &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}}, @@ -100,6 +137,18 @@ func TestTeamScheduleAuth(t *testing.T) { true, true, }, + { + "team observer+, DOES NOT belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserverPlus}}}, + true, + true, + }, + { + "team gitops, DOES NOT belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleGitOps}}}, + true, + true, + }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { @@ -114,7 +163,7 @@ func TestTeamScheduleAuth(t *testing.T) { _, err = svc.ModifyTeamScheduledQueries(ctx, 1, 99, fleet.ScheduledQueryPayload{}) checkAuthErr(t, tt.shouldFailWrite, err) - err = svc.DeleteTeamScheduledQueries(ctx, 1, 1) + err = svc.DeleteTeamScheduledQueries(ctx, 1, 99) checkAuthErr(t, tt.shouldFailWrite, err) }) } diff --git a/server/service/teams_test.go b/server/service/teams_test.go index 1579f78fc4..bb3af518b1 100644 --- a/server/service/teams_test.go +++ b/server/service/teams_test.go @@ -261,7 +261,7 @@ func TestApplyTeamSpecs(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { - return nil, ¬FoundError{} + return nil, newNotFoundError() } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { diff --git a/server/service/testing_client.go b/server/service/testing_client.go index d70c1ee5e6..4930f8f283 100644 --- a/server/service/testing_client.go +++ b/server/service/testing_client.go @@ -113,6 +113,18 @@ func (ts *withServer) commonTearDownTest(t *testing.T) { } } + queries, err := ts.ds.ListQueries(ctx, fleet.ListQueryOptions{}) + require.NoError(t, err) + queryIDs := make([]uint, 0, len(queries)) + for _, query := range queries { + queryIDs = append(queryIDs, query.ID) + } + if len(queryIDs) > 0 { + count, err := ts.ds.DeleteQueries(ctx, queryIDs) + require.NoError(t, err) + require.Equal(t, len(queries), int(count)) + } + users, err := ts.ds.ListUsers(ctx, fleet.UserListOptions{}) require.NoError(t, err) for _, u := range users { diff --git a/server/test/comparisons.go b/server/test/comparisons.go index 1dc42ddabf..2bb66d8b6d 100644 --- a/server/test/comparisons.go +++ b/server/test/comparisons.go @@ -230,3 +230,57 @@ func formatListDiff(listA, listB interface{}, extraA, extraB []interface{}) stri return msg.String() } + +// QueryElementsMatch asserts that two queries slices match +func QueryElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { + t.Helper() + + opt := cmp.FilterPath(func(p cmp.Path) bool { + for _, ps := range p { + switch ps := ps.(type) { + case cmp.StructField: + switch ps.Name() { + case "ID", + "UpdateCreateTimestamps", + "AuthorID", + "AuthorName", + "AuthorEmail", + "Packs", + "Saved": + return true + } + } + } + return false + }, cmp.Ignore()) + return ElementsMatchWithOptions(t, listA, listB, []cmp.Option{opt}, msgAndArgs) +} + +// QueriesMatch asserts that two queries 'match'. +func QueriesMatch(t TestingT, a, b interface{}, msgAndArgs ...interface{}) (ok bool) { + t.Helper() + + opt := cmp.FilterPath(func(p cmp.Path) bool { + for _, ps := range p { + switch ps := ps.(type) { + case cmp.StructField: + switch ps.Name() { + case "ID", + "UpdateCreateTimestamps", + "AuthorID", + "AuthorName", + "AuthorEmail", + "Packs", + "Saved": + return true + } + } + } + return false + }, cmp.Ignore()) + + if !cmp.Equal(a, b, opt) { + return assert.Fail(t, cmp.Diff(a, b, opt), msgAndArgs...) + } + return true +} diff --git a/server/test/new_objects.go b/server/test/new_objects.go index 55f6171e4c..407256527b 100644 --- a/server/test/new_objects.go +++ b/server/test/new_objects.go @@ -12,16 +12,19 @@ import ( "github.com/stretchr/testify/require" ) -func NewQuery(t *testing.T, ds fleet.Datastore, name, q string, authorID uint, saved bool) *fleet.Query { +func NewQueryWithSchedule(t *testing.T, ds fleet.Datastore, teamID *uint, name, q string, authorID uint, saved bool, interval uint, automationsEnabled bool) *fleet.Query { authorPtr := &authorID if authorID == 0 { authorPtr = nil } query, err := ds.NewQuery(context.Background(), &fleet.Query{ - Name: name, - Query: q, - AuthorID: authorPtr, - Saved: saved, + Name: name, + Query: q, + AuthorID: authorPtr, + Saved: saved, + TeamID: teamID, + Interval: interval, + AutomationsEnabled: automationsEnabled, }) require.NoError(t, err) @@ -32,6 +35,10 @@ func NewQuery(t *testing.T, ds fleet.Datastore, name, q string, authorID uint, s return query } +func NewQuery(t *testing.T, ds fleet.Datastore, teamID *uint, name, q string, authorID uint, saved bool) *fleet.Query { + return NewQueryWithSchedule(t, ds, teamID, name, q, authorID, saved, 0, false) +} + func NewPack(t *testing.T, ds fleet.Datastore, name string) *fleet.Pack { err := ds.ApplyPackSpecs(context.Background(), []*fleet.PackSpec{{Name: name}}) require.Nil(t, err)