From ccb365e81d124dfd04924bc379ef54af9697fbed Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 6 Jul 2023 16:05:21 -0400 Subject: [PATCH 01/78] Added migration with schema changes --- ...0230706141219_QueriesTableSchemaChanges.go | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go diff --git a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go new file mode 100644 index 0000000000..cb6837f057 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go @@ -0,0 +1,49 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20230706141219, Down_20230706141219) +} + +func Up_20230706141219(tx *sql.Tx) error { + // If we want to drop the uniq constraint on queries.name, we first need to remove this + // constraint on scheduled_queries, due to a FK constraint scheduled_queries (query_name) => + // queries (name). + if _, err := tx.Exec(` + ALTER TABLE scheduled_queries 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 '', + + ADD platform VARCHAR(255) DEFAULT NULL, + ADD min_osquery_version VARCHAR(255) DEFAULT NULL, + + ADD frequency_seconds INT(10) UNSIGNED DEFAULT 0, + ADD automations_enabled TINYINT(1) UNSIGNED DEFAULT 0, + ADD logging_type VARCHAR(255) DEFAULT 'snapshot', + + ADD FOREIGN KEY fk_queries_team_id (team_id) REFERENCES teams (id) ON DELETE CASCADE ON UPDATE CASCADE, + ADD UNIQUE INDEX idx_team_id_name_unq (team_id_char, name); + `); err != nil { + return errors.Wrap(err, "updating queries schema") + } + + return nil +} + +func Down_20230706141219(tx *sql.Tx) error { + return nil +} From fcea5a833c5d7c1e98cb0f3cde1c421af7e7c700 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 6 Jul 2023 16:19:28 -0400 Subject: [PATCH 02/78] Updated query type --- .../20230706141219_QueriesTableSchemaChanges.go | 2 +- server/fleet/queries.go | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go index cb6837f057..cf958d787e 100644 --- a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go +++ b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go @@ -31,7 +31,7 @@ func Up_20230706141219(tx *sql.Tx) error { ADD platform VARCHAR(255) DEFAULT NULL, ADD min_osquery_version VARCHAR(255) DEFAULT NULL, - ADD frequency_seconds INT(10) UNSIGNED DEFAULT 0, + ADD interval INT(10) UNSIGNED DEFAULT 0, ADD automations_enabled TINYINT(1) UNSIGNED DEFAULT 0, ADD logging_type VARCHAR(255) DEFAULT 'snapshot', diff --git a/server/fleet/queries.go b/server/fleet/queries.go index 025543c35a..c246102262 100644 --- a/server/fleet/queries.go +++ b/server/fleet/queries.go @@ -17,7 +17,21 @@ type QueryPayload struct { 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. + 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:"interval"` + // Platform if set, specifies the platform(s) this query will target. + 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"` + // LoggingType the type of log output for this query + LoggingType string `json:"logging" db:"logging_type"` Name string `json:"name"` Description string `json:"description"` Query string `json:"query"` From 010eeff91a3f5c5f062d33266620ad4159c9db9a Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 6 Jul 2023 17:28:25 -0400 Subject: [PATCH 03/78] Updated DB layer --- ...0230706141219_QueriesTableSchemaChanges.go | 2 +- server/datastore/mysql/queries.go | 170 +++++++++++++++--- server/datastore/mysql/schema.sql | 21 ++- server/fleet/queries.go | 10 +- server/fleet/queries_test.go | 25 +++ 5 files changed, 197 insertions(+), 31 deletions(-) diff --git a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go index cf958d787e..9099ac2ff1 100644 --- a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go +++ b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go @@ -31,7 +31,7 @@ func Up_20230706141219(tx *sql.Tx) error { ADD platform VARCHAR(255) DEFAULT NULL, ADD min_osquery_version VARCHAR(255) DEFAULT NULL, - ADD interval INT(10) UNSIGNED DEFAULT 0, + ADD schedule_interval INT(10) UNSIGNED DEFAULT 0, ADD automations_enabled TINYINT(1) UNSIGNED DEFAULT 0, ADD logging_type VARCHAR(255) DEFAULT 'snapshot', diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index f1f488f7ab..7dddc895eb 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 { @@ -56,7 +70,21 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] if q.Name == "" { return ctxerr.New(ctx, "query name must not be empty") } - _, 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.ScheduleInterval, + q.AutomationsEnabled, + q.LoggingType, + ) if err != nil { return ctxerr.Wrap(ctx, err, "exec ApplyQueries insert") } @@ -68,9 +96,24 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] func (ds *Datastore) QueryByName(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { sqlStatement := ` - SELECT * - FROM queries - WHERE name = ? + 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 = ? ` var query fleet.Query err := sqlx.GetContext(ctx, ds.reader(ctx), &query, sqlStatement, name) @@ -97,10 +140,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.ScheduleInterval, + query.AutomationsEnabled, + query.LoggingType, + ) if err != nil && isDuplicate(err) { return nil, ctxerr.Wrap(ctx, alreadyExists("Query", query.Name)) @@ -118,10 +184,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.ScheduleInterval, + q.AutomationsEnabled, + q.LoggingType, + q.ID) if err != nil { return ctxerr.Wrap(ctx, err, "updating query") } @@ -150,7 +244,24 @@ 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 FROM queries q LEFT JOIN users u ON q.author_id = u.id @@ -176,14 +287,28 @@ 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 = ?) @@ -197,6 +322,7 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions results := []*fleet.Query{} if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, false, aggregatedStatsTypeQuery); err != nil { + fmt.Println(err) return nil, ctxerr.Wrap(ctx, err, "listing queries") } diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 1aec42927b..53b5490bea 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=197 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=198 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'); +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,20230706141219,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,19 @@ 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 DEFAULT '', + `platform` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `min_osquery_version` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `schedule_interval` int(10) unsigned DEFAULT '0', + `automations_enabled` tinyint(1) unsigned DEFAULT '0', + `logging_type` varchar(255) COLLATE utf8mb4_unicode_ci 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`), KEY `author_id` (`author_id`), - CONSTRAINT `queries_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL + KEY `fk_queries_team_id` (`team_id`), + 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 ON UPDATE 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 */; @@ -1069,8 +1077,7 @@ CREATE TABLE `scheduled_queries` ( 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 + 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/fleet/queries.go b/server/fleet/queries.go index c246102262..b0af6f2e41 100644 --- a/server/fleet/queries.go +++ b/server/fleet/queries.go @@ -22,7 +22,7 @@ type Query struct { // team. 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:"interval"` + ScheduleInterval uint `json:"interval" db:"schedule_interval"` // Platform if set, specifies the platform(s) this query will target. Platform *string `json:"platform" db:"platform"` // MinOsqueryVersion if set, specifies the min required version of osquery that must be @@ -57,6 +57,14 @@ 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) +} + // Verify verifies the query payload is valid. func (q *QueryPayload) Verify() error { if q.Name != nil { diff --git a/server/fleet/queries_test.go b/server/fleet/queries_test.go index 157626d837..24983d764b 100644 --- a/server/fleet/queries_test.go +++ b/server/fleet/queries_test.go @@ -3,10 +3,35 @@ package fleet import ( "testing" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +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 From d3a78cc736e09eaad3210b747e7ff80851f9eb0a Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 6 Jul 2023 18:01:15 -0400 Subject: [PATCH 04/78] Updated tests --- server/datastore/mysql/queries_test.go | 107 ++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 13 deletions(-) diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index 5814c00388..e244c8306e 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" @@ -43,9 +44,24 @@ 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, + ScheduleInterval: 10, + Platform: ptr.String("macos"), + MinOsqueryVersion: ptr.String("5.2.1"), + AutomationsEnabled: true, + LoggingType: "differential", + }, + { + Name: "bar", + Description: "do some bars", + Query: "select baz from bar", + }, } // Zach creates some queries @@ -55,6 +71,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { queries, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.Nil(t, err) require.Len(t, queries, len(expectedQueries)) + for i, q := range queries { comp := expectedQueries[i] assert.Equal(t, comp.Name, q.Name) @@ -62,6 +79,12 @@ func testQueriesApply(t *testing.T, ds *Datastore) { assert.Equal(t, comp.Query, q.Query) assert.Equal(t, &zwass.ID, q.AuthorID) assert.Equal(t, comp.ObserverCanRun, q.ObserverCanRun) + assert.Equal(t, comp.TeamID, q.TeamID) + assert.Equal(t, comp.ScheduleInterval, q.ScheduleInterval) + assert.Equal(t, comp.Platform, q.Platform) + assert.Equal(t, comp.MinOsqueryVersion, q.MinOsqueryVersion) + assert.Equal(t, comp.AutomationsEnabled, q.AutomationsEnabled) + assert.Equal(t, comp.LoggingType, q.LoggingType) } // Victor modifies a query (but also pushes the same version of the @@ -79,11 +102,22 @@ func testQueriesApply(t *testing.T, ds *Datastore) { assert.Equal(t, comp.Description, q.Description) assert.Equal(t, comp.Query, q.Query) assert.Equal(t, &groob.ID, q.AuthorID) + assert.Equal(t, comp.ObserverCanRun, q.ObserverCanRun) + assert.Equal(t, comp.TeamID, q.TeamID) + assert.Equal(t, comp.ScheduleInterval, q.ScheduleInterval) + assert.Equal(t, comp.Platform, q.Platform) + assert.Equal(t, comp.MinOsqueryVersion, q.MinOsqueryVersion) + assert.Equal(t, comp.AutomationsEnabled, q.AutomationsEnabled) + assert.Equal(t, comp.LoggingType, q.LoggingType) } // 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) @@ -91,12 +125,21 @@ func testQueriesApply(t *testing.T, ds *Datastore) { queries, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.Nil(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, comp.ObserverCanRun, q.ObserverCanRun) + assert.Equal(t, comp.TeamID, q.TeamID) + assert.Equal(t, comp.ScheduleInterval, q.ScheduleInterval) + assert.Equal(t, comp.Platform, q.Platform) + assert.Equal(t, comp.MinOsqueryVersion, q.MinOsqueryVersion) + assert.Equal(t, comp.AutomationsEnabled, q.AutomationsEnabled) + assert.Equal(t, comp.LoggingType, q.LoggingType) } + assert.Equal(t, &groob.ID, queries[0].AuthorID) assert.Equal(t, &groob.ID, queries[1].AuthorID) assert.Equal(t, &zwass.ID, queries[2].AuthorID) @@ -182,23 +225,42 @@ 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) + 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 - err = ds.SaveQuery(context.Background(), query) + query.TeamID = &team.ID + query.ScheduleInterval = 10 + query.Platform = ptr.String("macos") + query.MinOsqueryVersion = ptr.String("5.2.1") + query.AutomationsEnabled = true + query.LoggingType = "differential" - require.Nil(t, err) + err = ds.SaveQuery(context.Background(), query) + require.NoError(t, err) queryVerify, err := ds.Query(context.Background(), query.ID) - require.Nil(t, err) + require.NoError(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) + assert.Equal(t, *query.TeamID, team.ID) + assert.Equal(t, query.ScheduleInterval, uint(10)) + assert.Equal(t, *query.Platform, "macos") + assert.Equal(t, *query.MinOsqueryVersion, "5.2.1") + assert.Equal(t, query.AutomationsEnabled, true) + assert.Equal(t, query.LoggingType, "differential") } func testQueriesList(t *testing.T, ds *Datastore) { @@ -381,21 +443,40 @@ 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) + assert.NotZero(t, globalQ1.ID) _, err = ds.NewQuery(context.Background(), &fleet.Query{ Name: "foo", Query: "select * from osquery_info;", }) + assert.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 + // 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, + }) assert.Contains(t, err.Error(), "already exists") } From 2eacb3bcc28fc15b60aac5dec226f0f52f6ccb11 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 6 Jul 2023 18:11:32 -0400 Subject: [PATCH 05/78] Added changes --- changes/7765-combined-schedules-and-queries | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changes/7765-combined-schedules-and-queries diff --git a/changes/7765-combined-schedules-and-queries b/changes/7765-combined-schedules-and-queries new file mode 100644 index 0000000000..3b4458f25d --- /dev/null +++ b/changes/7765-combined-schedules-and-queries @@ -0,0 +1,2 @@ +- Updated 'queries' table schema to allow storing schedulling information and configuration in the + 'queries' table. From e3d7269c3d348def6f1ab21c4ee4630f668946c1 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 6 Jul 2023 18:13:49 -0400 Subject: [PATCH 06/78] Fixed typo --- changes/7765-combined-schedules-and-queries | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/7765-combined-schedules-and-queries b/changes/7765-combined-schedules-and-queries index 3b4458f25d..b3d65ace26 100644 --- a/changes/7765-combined-schedules-and-queries +++ b/changes/7765-combined-schedules-and-queries @@ -1,2 +1,2 @@ -- Updated 'queries' table schema to allow storing schedulling information and configuration in the +- Updated 'queries' table schema to allow storing scheduling information and configuration in the 'queries' table. From be1f4cfa1225241db8d3241b5757c5648cd1af16 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 6 Jul 2023 18:16:02 -0400 Subject: [PATCH 07/78] Fixed typo --- changes/7765-combined-schedules-and-queries | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changes/7765-combined-schedules-and-queries b/changes/7765-combined-schedules-and-queries index b3d65ace26..9bc0290432 100644 --- a/changes/7765-combined-schedules-and-queries +++ b/changes/7765-combined-schedules-and-queries @@ -1,2 +1 @@ -- Updated 'queries' table schema to allow storing scheduling information and configuration in the - 'queries' table. +- Updated 'queries' table schema to allow storing scheduling information and configuration in the 'queries' table. From 807b2e35d3e5a379a50113c69ec949318e1dec12 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 6 Jul 2023 19:37:08 -0400 Subject: [PATCH 08/78] Updated QueryByName DB access method --- cmd/fleetctl/apply_test.go | 4 +-- cmd/fleetctl/delete_test.go | 2 +- cmd/fleetctl/get_test.go | 2 +- server/datastore/mysql/packs.go | 34 ++++++++++++++++++++------ server/datastore/mysql/queries.go | 19 +++++++++++--- server/datastore/mysql/queries_test.go | 20 +++++++-------- server/fleet/datastore.go | 4 +-- server/mock/datastore_mock.go | 10 ++++---- server/service/queries.go | 6 ++--- server/service/queries_test.go | 2 +- 10 files changed, 68 insertions(+), 35 deletions(-) diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 8d014a6613..cad7412781 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/delete_test.go b/cmd/fleetctl/delete_test.go index ec0963ef6e..deff2d010f 100644 --- a/cmd/fleetctl/delete_test.go +++ b/cmd/fleetctl/delete_test.go @@ -82,7 +82,7 @@ func TestDeleteQuery(t *testing.T) { 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_test.go b/cmd/fleetctl/get_test.go index fc8d4fa76c..7e10951d2a 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -1030,7 +1030,7 @@ spec: func TestGetQuery(t *testing.T) { _, ds := runServerWithMockedDS(t) - 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/server/datastore/mysql/packs.go b/server/datastore/mysql/packs.go index 8c03ce72a5..391d4787cb 100644 --- a/server/datastore/mysql/packs.go +++ b/server/datastore/mysql/packs.go @@ -75,14 +75,34 @@ func applyPackSpecDB(ctx context.Context, tx sqlx.ExtContext, spec *fleet.PackSp if q.Name == "" { 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, - ) - switch { - case isChildForeignKeyError(err): + + // Check if query exists ... we have to do this manual check because the FK + // constraint was removed as part of the work required for combining queries and schedules + var count int + if err := tx.QueryRowxContext( + ctx, + `SELECT COUNT(1) FROM queries WHERE team_id_char = '' AND name = ?`, + q.QueryName, + ).Scan(&count); err != nil { + return ctxerr.Wrap(ctx, err, "checking if query exists") + } + if count == 0 { return ctxerr.Errorf(ctx, "cannot schedule unknown query '%s'", q.QueryName) - case err != nil: + } + + if _, 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, + ); err != nil { return ctxerr.Wrapf(ctx, err, "adding query %s referencing %s", q.Name, q.QueryName) } } diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index 7dddc895eb..2fc1c9416b 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -94,8 +94,13 @@ 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 := ` +func (ds *Datastore) QueryByName( + ctx context.Context, + teamID *uint, + name string, + opts ...fleet.OptionalArg, +) (*fleet.Query, error) { + stmt := ` SELECT id, team_id, @@ -115,8 +120,16 @@ func (ds *Datastore) QueryByName(ctx context.Context, name string, opts ...fleet 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)) diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index e244c8306e..6a0e3d411a 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -169,12 +169,12 @@ func testQueriesDelete(t *testing.T, ds *Datastore) { 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") + actual, err := ds.QueryByName(context.Background(), nil, "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") + actual, err = ds.QueryByName(context.Background(), nil, "xxx") assert.Error(t, err) assert.True(t, fleet.IsNotFound(err)) } @@ -334,11 +334,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) @@ -357,13 +357,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) @@ -390,13 +390,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 }) @@ -424,7 +424,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 }) @@ -432,7 +432,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 }) diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 2d6c41b601..2ac2a1f550 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -82,8 +82,8 @@ 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) + // QueryByName looks up a query by name on a 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) diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 7137d841b5..cc5578b963 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -70,7 +70,7 @@ 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 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) @@ -1622,7 +1622,7 @@ type DataStore struct { MDMWindowsInsertEnrolledDeviceFuncInvoked bool MDMWindowsDeleteEnrolledDeviceFunc MDMWindowsDeleteEnrolledDeviceFunc - MDMWindowsDeleteEnrolledDeviceFuncInvoked bool + MDMWindowsDeleteEnrolledDeviceFuncInvoked bool mu sync.Mutex } @@ -1809,11 +1809,11 @@ 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) 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) { @@ -3872,4 +3872,4 @@ func (s *DataStore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDevic s.MDMWindowsDeleteEnrolledDeviceFuncInvoked = true s.mu.Unlock() return s.MDMWindowsDeleteEnrolledDeviceFunc(ctx, mdmDeviceID) -} \ No newline at end of file +} diff --git a/server/service/queries.go b/server/service/queries.go index 94a2287278..9d03fdfe10 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -298,7 +298,7 @@ func deleteQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Ser } func (svc *Service) DeleteQuery(ctx context.Context, name string) error { - query, err := svc.ds.QueryByName(ctx, name) + query, err := svc.ds.QueryByName(ctx, nil, name) if err != nil { setAuthCheckedOnPreAuthErr(ctx) return err @@ -469,7 +469,7 @@ func (svc *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpe } // check that the user can update the query if it already exists - query, err := svc.ds.QueryByName(ctx, query.Name) + query, err := svc.ds.QueryByName(ctx, nil, query.Name) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } else if err == nil { @@ -578,7 +578,7 @@ func (svc *Service) GetQuerySpec(ctx context.Context, name string) (*fleet.Query return nil, err } - query, err := svc.ds.QueryByName(ctx, name) + query, err := svc.ds.QueryByName(ctx, nil, name) if err != nil { return nil, err } diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 083657a123..178c0b509f 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -85,7 +85,7 @@ func TestQueryAuth(t *testing.T) { 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) { + ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { if name == authoredQueryName { return &fleet.Query{ID: 99, AuthorID: ptr.Uint(teamMaintainer.ID)}, nil } From 390e0565d0b62141bcea9400d75b418c6817c789 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 7 Jul 2023 07:31:36 -0400 Subject: [PATCH 09/78] Updated delete method on the DB layer --- cmd/fleetctl/delete_test.go | 2 +- server/datastore/mysql/policies_test.go | 2 +- server/datastore/mysql/queries.go | 36 ++++++++++++++++--- server/datastore/mysql/queries_test.go | 2 +- .../datastore/mysql/scheduled_queries_test.go | 2 +- server/fleet/datastore.go | 9 ++--- server/mock/datastore_mock.go | 6 ++-- server/service/queries.go | 4 +-- server/service/queries_test.go | 2 +- 9 files changed, 47 insertions(+), 18 deletions(-) diff --git a/cmd/fleetctl/delete_test.go b/cmd/fleetctl/delete_test.go index deff2d010f..301b05d6c1 100644 --- a/cmd/fleetctl/delete_test.go +++ b/cmd/fleetctl/delete_test.go @@ -78,7 +78,7 @@ 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 } 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 2fc1c9416b..9ea4430f7a 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -145,7 +145,11 @@ func (ds *Datastore) QueryByName( } // 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, @@ -243,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 diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index 6a0e3d411a..86c19ef87a 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -158,7 +158,7 @@ func testQueriesDelete(t *testing.T, ds *Datastore) { require.NotNil(t, query) assert.NotEqual(t, query.ID, 0) - err = ds.DeleteQuery(context.Background(), query.Name) + err = ds.DeleteQuery(context.Background(), nil, query.Name) require.Nil(t, err) assert.NotEqual(t, query.ID, 0) diff --git a/server/datastore/mysql/scheduled_queries_test.go b/server/datastore/mysql/scheduled_queries_test.go index 6bff759664..96e0b5b34f 100644 --- a/server/datastore/mysql/scheduled_queries_test.go +++ b/server/datastore/mysql/scheduled_queries_test.go @@ -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{}) diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 2ac2a1f550..28341a941f 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,7 +82,8 @@ 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 on a team. + // 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 diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index cc5578b963..e1f53f86ae 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) @@ -1781,11 +1781,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) { diff --git a/server/service/queries.go b/server/service/queries.go index 9d03fdfe10..e7009d8ff9 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -308,7 +308,7 @@ func (svc *Service) DeleteQuery(ctx context.Context, name string) error { return err } - if err := svc.ds.DeleteQuery(ctx, name); err != nil { + if err := svc.ds.DeleteQuery(ctx, nil, name); err != nil { return err } @@ -358,7 +358,7 @@ func (svc *Service) DeleteQueryByID(ctx context.Context, id uint) error { return err } - if err := svc.ds.DeleteQuery(ctx, query.Name); err != nil { + if err := svc.ds.DeleteQuery(ctx, nil, query.Name); err != nil { return ctxerr.Wrap(ctx, err, "delete query") } diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 178c0b509f..35f710661d 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -103,7 +103,7 @@ func TestQueryAuth(t *testing.T) { 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) { From de9a1540d0c1d6d022dfa386cdb950a72442dece Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 7 Jul 2023 07:36:09 -0400 Subject: [PATCH 10/78] Updated ListQueries method in DB layer --- server/datastore/mysql/queries.go | 12 ++++++++++-- server/fleet/app.go | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index 9ea4430f7a..fc6514807a 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -358,12 +358,20 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions if opt.OnlyObserverCanRun { sql += " AND q.observer_can_run=true" } + + args := []interface{}{false, aggregatedStatsTypeQuery} + whereClause := " AND team_id_char = ''" + if opt.TeamID != nil { + args = append(args, fmt.Sprint(*opt.TeamID)) + whereClause = " AND team_id_char = ?" + } + sql += whereClause + sql = appendListOptionsToSQL(sql, &opt.ListOptions) results := []*fleet.Query{} - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, false, aggregatedStatsTypeQuery); err != nil { - fmt.Println(err) + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, args...); err != nil { return nil, ctxerr.Wrap(ctx, err, "listing queries") } diff --git a/server/fleet/app.go b/server/fleet/app.go index 4b3f21bcca..87dcbddf36 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -761,6 +761,9 @@ 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 OnlyObserverCanRun bool } From 38661ad7b00f4e260280b2c44859d203e2a9acd7 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 7 Jul 2023 08:08:57 -0400 Subject: [PATCH 11/78] Fixed linter error --- server/datastore/mysql/mysql.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index 7096be9448..c1d93e106b 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -1041,20 +1041,6 @@ func generateMysqlConnectionString(conf config.MysqlConfig) string { return dsn } -// isForeignKeyError checks if the provided error is a MySQL child foreign key -// error (Error #1452) -func isChildForeignKeyError(err error) bool { - err = ctxerr.Cause(err) - mysqlErr, ok := err.(*mysql.MySQLError) - if !ok { - return false - } - - // https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_no_referenced_row_2 - const ER_NO_REFERENCED_ROW_2 = 1452 - return mysqlErr.Number == ER_NO_REFERENCED_ROW_2 -} - type patternReplacer func(string) string // likePattern returns a pattern to match m with LIKE. From 8a1ffc97ea5469b72cf2a554b7e6b080e766234e Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 7 Jul 2023 08:51:51 -0400 Subject: [PATCH 12/78] Refactored Apply test --- server/datastore/mysql/queries.go | 4 +- server/datastore/mysql/queries_test.go | 98 +++++++++++--------------- server/test/comparisons.go | 25 +++++++ 3 files changed, 69 insertions(+), 58 deletions(-) diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index fc6514807a..1816c13087 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -67,8 +67,8 @@ 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, diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index 86c19ef87a..d49767dcc4 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -21,15 +21,15 @@ func TestQueries(t *testing.T) { fn func(t *testing.T, ds *Datastore) }{ {"Apply", testQueriesApply}, - {"Delete", testQueriesDelete}, - {"GetByName", testQueriesGetByName}, - {"DeleteMany", testQueriesDeleteMany}, - {"Save", testQueriesSave}, - {"List", testQueriesList}, - {"LoadPacksForQueries", testQueriesLoadPacksForQueries}, - {"DuplicateNew", testQueriesDuplicateNew}, - {"ListFiltersObservers", testQueriesListFiltersObservers}, - {"ObserverCanRunQuery", testObserverCanRunQuery}, + // {"Delete", testQueriesDelete}, + // {"GetByName", testQueriesGetByName}, + // {"DeleteMany", testQueriesDeleteMany}, + // {"Save", testQueriesSave}, + // {"List", testQueriesList}, + // {"LoadPacksForQueries", testQueriesLoadPacksForQueries}, + // {"DuplicateNew", testQueriesDuplicateNew}, + // {"ListFiltersObservers", testQueriesListFiltersObservers}, + // {"ObserverCanRunQuery", testObserverCanRunQuery}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -66,49 +66,38 @@ func testQueriesApply(t *testing.T, ds *Datastore) { // 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) - assert.Equal(t, comp.TeamID, q.TeamID) - assert.Equal(t, comp.ScheduleInterval, q.ScheduleInterval) - assert.Equal(t, comp.Platform, q.Platform) - assert.Equal(t, comp.MinOsqueryVersion, q.MinOsqueryVersion) - assert.Equal(t, comp.AutomationsEnabled, q.AutomationsEnabled) - assert.Equal(t, comp.LoggingType, q.LoggingType) + 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) } // 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) - assert.Equal(t, comp.ObserverCanRun, q.ObserverCanRun) - assert.Equal(t, comp.TeamID, q.TeamID) - assert.Equal(t, comp.ScheduleInterval, q.ScheduleInterval) - assert.Equal(t, comp.Platform, q.Platform) - assert.Equal(t, comp.MinOsqueryVersion, q.MinOsqueryVersion) - assert.Equal(t, comp.AutomationsEnabled, q.AutomationsEnabled) - assert.Equal(t, comp.LoggingType, q.LoggingType) + require.Equal(t, groob.Email, q.AuthorEmail) + require.Equal(t, groob.Name, q.AuthorName) } // Zach adds a third query (but does not re-apply the others) @@ -120,29 +109,26 @@ func testQueriesApply(t *testing.T, ds *Datastore) { }, ) 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) - assert.Equal(t, comp.ObserverCanRun, q.ObserverCanRun) - assert.Equal(t, comp.TeamID, q.TeamID) - assert.Equal(t, comp.ScheduleInterval, q.ScheduleInterval) - assert.Equal(t, comp.Platform, q.Platform) - assert.Equal(t, comp.MinOsqueryVersion, q.MinOsqueryVersion) - assert.Equal(t, comp.AutomationsEnabled, q.AutomationsEnabled) - assert.Equal(t, comp.LoggingType, q.LoggingType) - } + test.QueryElementsMatch(t, expectedQueries, queries) - assert.Equal(t, &groob.ID, queries[0].AuthorID) - assert.Equal(t, &groob.ID, queries[1].AuthorID) - assert.Equal(t, &zwass.ID, queries[2].AuthorID) + for _, q := range queries { + 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) + } + } } func testQueriesDelete(t *testing.T, ds *Datastore) { diff --git a/server/test/comparisons.go b/server/test/comparisons.go index 1dc42ddabf..8adfaa0a24 100644 --- a/server/test/comparisons.go +++ b/server/test/comparisons.go @@ -230,3 +230,28 @@ 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) +} From dc32ccfac8f8ddfcc33b84df0b1d68cafdd25547 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 7 Jul 2023 08:54:09 -0400 Subject: [PATCH 13/78] Refactored Delete test --- server/datastore/mysql/queries_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index d49767dcc4..961a078225 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -21,7 +21,7 @@ func TestQueries(t *testing.T) { fn func(t *testing.T, ds *Datastore) }{ {"Apply", testQueriesApply}, - // {"Delete", testQueriesDelete}, + {"Delete", testQueriesDelete}, // {"GetByName", testQueriesGetByName}, // {"DeleteMany", testQueriesDeleteMany}, // {"Save", testQueriesSave}, @@ -140,16 +140,16 @@ 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(), nil, 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) } func testQueriesGetByName(t *testing.T, ds *Datastore) { From 6a69604c62c470ebd8c20448dec68d1b0d591fb9 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 7 Jul 2023 09:05:51 -0400 Subject: [PATCH 14/78] Updated factory method for creating queries in tests --- server/datastore/mysql/campaigns_test.go | 6 +-- server/datastore/mysql/delete_test.go | 8 ++-- server/datastore/mysql/hosts_test.go | 38 +++++++++---------- server/datastore/mysql/labels_test.go | 4 +- server/datastore/mysql/queries_test.go | 30 ++++++++------- .../datastore/mysql/scheduled_queries_test.go | 14 +++---- server/test/new_objects.go | 3 +- 7 files changed, 53 insertions(+), 50 deletions(-) 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_test.go b/server/datastore/mysql/hosts_test.go index f51fc2bf3d..df9448f348 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -295,7 +295,7 @@ 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{ { @@ -322,7 +322,7 @@ 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{ { @@ -411,7 +411,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 +419,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() @@ -566,7 +566,7 @@ func testHostsWithTeamPackStats(t *testing.T, ds *Datastore) { 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) + tpQuery := test.NewQuery(t, ds, nil, "tp-time", "select * from time", 0, true) tpSquery := test.NewScheduledQuery(t, ds, tp.ID, tpQuery.ID, 30, true, true, "time-scheduled") // Create a new pack and target to the host. @@ -576,7 +576,7 @@ 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{ { @@ -2573,7 +2573,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, }) @@ -3027,8 +3027,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, }) @@ -3121,7 +3121,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, }) @@ -3395,11 +3395,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()) @@ -3628,7 +3628,7 @@ func testHostsAllPackStats(t *testing.T, ds *Datastore) { 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) + globalQuery := test.NewQuery(t, ds, nil, "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) @@ -3641,7 +3641,7 @@ func testHostsAllPackStats(t *testing.T, ds *Datastore) { 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) + teamQuery := test.NewQuery(t, ds, nil, "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). @@ -3650,7 +3650,7 @@ func testHostsAllPackStats(t *testing.T, ds *Datastore) { 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). @@ -3816,7 +3816,7 @@ func testHostsPackStatsMultipleHosts(t *testing.T, ds *Datastore) { 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) + globalQuery := test.NewQuery(t, ds, nil, "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, host1.ID}, @@ -3945,7 +3945,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { 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) + globalQuery := test.NewQuery(t, ds, nil, "global-time", "select * from time", 0, true) globalSQuery1, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For Linux only", PackID: globalPack.ID, @@ -5677,7 +5677,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{ { @@ -6097,7 +6097,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 ac1e2d3b09..660b350b5d 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -926,8 +926,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/queries_test.go b/server/datastore/mysql/queries_test.go index 961a078225..df75445814 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -22,8 +22,8 @@ func TestQueries(t *testing.T) { }{ {"Apply", testQueriesApply}, {"Delete", testQueriesDelete}, - // {"GetByName", testQueriesGetByName}, - // {"DeleteMany", testQueriesDeleteMany}, + {"GetByName", testQueriesGetByName}, + {"DeleteMany", testQueriesDeleteMany}, // {"Save", testQueriesSave}, // {"List", testQueriesList}, // {"LoadPacksForQueries", testQueriesLoadPacksForQueries}, @@ -150,28 +150,30 @@ func testQueriesDelete(t *testing.T, ds *Datastore) { require.NotEqual(t, query.ID, 0) _, err = ds.Query(context.Background(), query.ID) 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(), nil, "q1") - require.Nil(t, err) - assert.Equal(t, "q1", actual.Name) - assert.Equal(t, "select * from time", actual.Query) + q := test.NewQuery(t, ds, nil, "q1", "select * from time", user.ID, true) - actual, err = ds.QueryByName(context.Background(), nil, "xxx") - assert.Error(t, err) - assert.True(t, fleet.IsNotFound(err)) + actual, err := ds.QueryByName(context.Background(), q.TeamID, q.Name) + require.NoError(t, err) + require.Equal(t, "q1", actual.Name) + require.Equal(t, "select * from time", actual.Query) + + actual, err = ds.QueryByName(context.Background(), q.TeamID, "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) diff --git a/server/datastore/mysql/scheduled_queries_test.go b/server/datastore/mysql/scheduled_queries_test.go index 96e0b5b34f..41179a8fc6 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, "") @@ -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") diff --git a/server/test/new_objects.go b/server/test/new_objects.go index 55f6171e4c..f736c7dc51 100644 --- a/server/test/new_objects.go +++ b/server/test/new_objects.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func NewQuery(t *testing.T, ds fleet.Datastore, name, q string, authorID uint, saved bool) *fleet.Query { +func NewQuery(t *testing.T, ds fleet.Datastore, teamID *uint, name, q string, authorID uint, saved bool) *fleet.Query { authorPtr := &authorID if authorID == 0 { authorPtr = nil @@ -22,6 +22,7 @@ func NewQuery(t *testing.T, ds fleet.Datastore, name, q string, authorID uint, s Query: q, AuthorID: authorPtr, Saved: saved, + TeamID: teamID, }) require.NoError(t, err) From ae5057f7609109962f2ec45d7019474fcec7f071 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 7 Jul 2023 09:46:06 -0400 Subject: [PATCH 15/78] Moar refactoring --- server/datastore/mysql/packs.go | 2 +- server/datastore/mysql/queries_test.go | 143 +++++++++++++++++++------ server/test/comparisons.go | 29 +++++ 3 files changed, 143 insertions(+), 31 deletions(-) diff --git a/server/datastore/mysql/packs.go b/server/datastore/mysql/packs.go index 391d4787cb..3cad4129a3 100644 --- a/server/datastore/mysql/packs.go +++ b/server/datastore/mysql/packs.go @@ -76,7 +76,7 @@ func applyPackSpecDB(ctx context.Context, tx sqlx.ExtContext, spec *fleet.PackSp q.Name = q.QueryName } - // Check if query exists ... we have to do this manual check because the FK + // Check if query exists ... we have to do this manually because the FK // constraint was removed as part of the work required for combining queries and schedules var count int if err := tx.QueryRowxContext( diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index df75445814..3d5a6b3ae9 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -24,12 +24,13 @@ func TestQueries(t *testing.T) { {"Delete", testQueriesDelete}, {"GetByName", testQueriesGetByName}, {"DeleteMany", testQueriesDeleteMany}, - // {"Save", testQueriesSave}, - // {"List", testQueriesList}, - // {"LoadPacksForQueries", testQueriesLoadPacksForQueries}, - // {"DuplicateNew", testQueriesDuplicateNew}, - // {"ListFiltersObservers", testQueriesListFiltersObservers}, - // {"ObserverCanRunQuery", testObserverCanRunQuery}, + {"Save", testQueriesSave}, + {"List", testQueriesList}, + {"LoadPacksForQueries", testQueriesLoadPacksForQueries}, + {"DuplicateNew", testQueriesDuplicateNew}, + {"ListFiltersObservers", testQueriesListFiltersObservers}, + {"ObserverCanRunQuery", testObserverCanRunQuery}, + {"ListFiltersByTeamID", testQueriesListFiltersByTeamID}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -79,6 +80,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { 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 @@ -98,6 +100,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { 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) @@ -118,6 +121,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { 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) @@ -155,14 +159,36 @@ func testQueriesDelete(t *testing.T, ds *Datastore) { func testQueriesGetByName(t *testing.T, ds *Datastore) { user := test.NewUser(t, ds, "Zach", "zwass@fleet.co", true) - q := test.NewQuery(t, ds, nil, "q1", "select * from time", user.ID, true) - actual, err := ds.QueryByName(context.Background(), q.TeamID, q.Name) + // 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(), q.TeamID, "xxx") + 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)) } @@ -215,7 +241,7 @@ func testQueriesSave(t *testing.T, ds *Datastore) { query, err := ds.NewQuery(context.Background(), query) 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", @@ -235,20 +261,15 @@ func testQueriesSave(t *testing.T, ds *Datastore) { err = ds.SaveQuery(context.Background(), query) require.NoError(t, err) - queryVerify, err := ds.Query(context.Background(), query.ID) + actual, err := ds.Query(context.Background(), query.ID) require.NoError(t, err) - require.NotNil(t, queryVerify) + require.NotNil(t, actual) - 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) - assert.Equal(t, *query.TeamID, team.ID) - assert.Equal(t, query.ScheduleInterval, uint(10)) - assert.Equal(t, *query.Platform, "macos") - assert.Equal(t, *query.MinOsqueryVersion, "5.2.1") - assert.Equal(t, query.AutomationsEnabled, true) - assert.Equal(t, query.LoggingType, "differential") + 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) { @@ -277,8 +298,8 @@ 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 @@ -290,7 +311,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { 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 { @@ -312,7 +333,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"}, @@ -439,12 +460,12 @@ func testQueriesDuplicateNew(t *testing.T, ds *Datastore) { AuthorID: &user.ID, }) require.NoError(t, err) - assert.NotZero(t, globalQ1.ID) + require.NotZero(t, globalQ1.ID) _, err = ds.NewQuery(context.Background(), &fleet.Query{ Name: "foo", Query: "select * from osquery_info;", }) - assert.Contains(t, err.Error(), "already exists") + require.Contains(t, err.Error(), "already exists") // Check uniqueness constraint on queries that belong to a team team, err := ds.NewTeam(context.Background(), &fleet.Team{ @@ -465,7 +486,7 @@ func testQueriesDuplicateNew(t *testing.T, ds *Datastore) { Query: "select * from osquery_info;", TeamID: &team.ID, }) - assert.Contains(t, err.Error(), "already exists") + require.Contains(t, err.Error(), "already exists") } func testQueriesListFiltersObservers(t *testing.T, ds *Datastore) { @@ -499,7 +520,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) { @@ -532,3 +553,65 @@ func testObserverCanRunQuery(t *testing.T, ds *Datastore) { require.Equal(t, q.ObserverCanRun, canRun) } } + +func testQueriesListFiltersByTeamID(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}) +} diff --git a/server/test/comparisons.go b/server/test/comparisons.go index 8adfaa0a24..2bb66d8b6d 100644 --- a/server/test/comparisons.go +++ b/server/test/comparisons.go @@ -255,3 +255,32 @@ func QueryElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...inte }, 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 +} From 6df0768803089eda2dad47a402fd08a348dc9944 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 7 Jul 2023 09:59:16 -0400 Subject: [PATCH 16/78] Fixed broken tests --- server/service/async/async_scheduled_query_stats_test.go | 8 ++++---- server/service/integration_core_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/service/async/async_scheduled_query_stats_test.go b/server/service/async/async_scheduled_query_stats_test.go index f03f6a0675..784919c8ec 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") diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 9924fc62b7..b456efd107 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -1196,7 +1196,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, From 709f767b89d56d96d55c54051ffbc76ffe44dfa7 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Mon, 10 Jul 2023 07:35:12 -0400 Subject: [PATCH 17/78] Added missing comment about team_id_char --- server/fleet/queries.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/fleet/queries.go b/server/fleet/queries.go index b0af6f2e41..db1413fd23 100644 --- a/server/fleet/queries.go +++ b/server/fleet/queries.go @@ -19,7 +19,11 @@ type Query struct { UpdateCreateTimestamps ID uint `json:"id"` // TeamID to which team this query belongs. If not set, then the query belongs to the 'Global' - // team. + // 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. ScheduleInterval uint `json:"interval" db:"schedule_interval"` From 2b8dd6571685f9040f4702deb44b45bbbdea78b8 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Mon, 10 Jul 2023 13:22:51 -0400 Subject: [PATCH 18/78] Updated default values, updated not null constraints --- .../tables/20230706141219_QueriesTableSchemaChanges.go | 10 +++++----- server/datastore/mysql/queries_test.go | 9 ++++----- server/datastore/mysql/schema.sql | 10 +++++----- server/fleet/queries.go | 4 ++-- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go index 9099ac2ff1..e275bd629e 100644 --- a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go +++ b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go @@ -28,12 +28,12 @@ func Up_20230706141219(tx *sql.Tx) error { ADD team_id INT(10) UNSIGNED DEFAULT NULL, ADD team_id_char CHAR(10) DEFAULT '', - ADD platform VARCHAR(255) DEFAULT NULL, - ADD min_osquery_version VARCHAR(255) DEFAULT 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, - ADD automations_enabled TINYINT(1) UNSIGNED DEFAULT 0, - ADD logging_type VARCHAR(255) DEFAULT 'snapshot', + 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 ON UPDATE CASCADE, ADD UNIQUE INDEX idx_team_id_name_unq (team_id_char, name); diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index 3d5a6b3ae9..edb9d028fc 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -7,7 +7,6 @@ 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" @@ -53,8 +52,8 @@ func testQueriesApply(t *testing.T, ds *Datastore) { Query: "select * from foo", ObserverCanRun: true, ScheduleInterval: 10, - Platform: ptr.String("macos"), - MinOsqueryVersion: ptr.String("5.2.1"), + Platform: "macos", + MinOsqueryVersion: "5.2.1", AutomationsEnabled: true, LoggingType: "differential", }, @@ -253,8 +252,8 @@ func testQueriesSave(t *testing.T, ds *Datastore) { query.ObserverCanRun = true query.TeamID = &team.ID query.ScheduleInterval = 10 - query.Platform = ptr.String("macos") - query.MinOsqueryVersion = ptr.String("5.2.1") + query.Platform = "macos" + query.MinOsqueryVersion = "5.2.1" query.AutomationsEnabled = true query.LoggingType = "differential" diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 53b5490bea..652e4d1f0f 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -1019,11 +1019,11 @@ CREATE TABLE `queries` ( `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 DEFAULT '', - `platform` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `min_osquery_version` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `schedule_interval` int(10) unsigned DEFAULT '0', - `automations_enabled` tinyint(1) unsigned DEFAULT '0', - `logging_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT 'snapshot', + `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_team_id_name_unq` (`team_id_char`,`name`), KEY `author_id` (`author_id`), diff --git a/server/fleet/queries.go b/server/fleet/queries.go index db1413fd23..f6548448e1 100644 --- a/server/fleet/queries.go +++ b/server/fleet/queries.go @@ -28,10 +28,10 @@ type Query struct { // Interval frequency of execution (in seconds), if 0 then, this query will never run. ScheduleInterval uint `json:"interval" db:"schedule_interval"` // Platform if set, specifies the platform(s) this query will target. - Platform *string `json:"platform" db:"platform"` + 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"` + 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"` // LoggingType the type of log output for this query From 1151177938aa515c18e170c01fb5b3ae092d1aab Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Mon, 10 Jul 2023 14:53:00 -0400 Subject: [PATCH 19/78] Added missing FK constraint on scheduled_queries --- ...0230706141219_QueriesTableSchemaChanges.go | 21 ++++++++++++----- server/datastore/mysql/mysql.go | 14 +++++++++++ server/datastore/mysql/packs.go | 23 +++++-------------- server/datastore/mysql/schema.sql | 7 ++++-- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go index e275bd629e..0c6dd60012 100644 --- a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go +++ b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go @@ -11,11 +11,12 @@ func init() { } func Up_20230706141219(tx *sql.Tx) error { - // If we want to drop the uniq constraint on queries.name, we first need to remove this - // constraint on scheduled_queries, due to a FK constraint scheduled_queries (query_name) => - // queries (name). + // Drop FK constraint based on queries (name) - since the uniqueness constraint on the queries + // table changed. if _, err := tx.Exec(` - ALTER TABLE scheduled_queries DROP FOREIGN KEY scheduled_queries_query_name; + ALTER TABLE scheduled_queries + ADD team_id_char CHAR(10) DEFAULT '', + DROP FOREIGN KEY scheduled_queries_query_name; `); err != nil { return errors.Wrap(err, "removing FK on scheduled_queries") } @@ -26,7 +27,7 @@ func Up_20230706141219(tx *sql.Tx) error { DROP INDEX constraint_query_name_unique, ADD team_id INT(10) UNSIGNED DEFAULT NULL, - ADD team_id_char CHAR(10) DEFAULT '', + 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, @@ -35,12 +36,20 @@ func Up_20230706141219(tx *sql.Tx) error { 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 ON UPDATE CASCADE, + 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); + `); err != nil { + return errors.Wrap(err, "adding new FK on scheduled_queries") + } + return nil } diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index c1d93e106b..7096be9448 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -1041,6 +1041,20 @@ func generateMysqlConnectionString(conf config.MysqlConfig) string { return dsn } +// isForeignKeyError checks if the provided error is a MySQL child foreign key +// error (Error #1452) +func isChildForeignKeyError(err error) bool { + err = ctxerr.Cause(err) + mysqlErr, ok := err.(*mysql.MySQLError) + if !ok { + return false + } + + // https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_no_referenced_row_2 + const ER_NO_REFERENCED_ROW_2 = 1452 + return mysqlErr.Number == ER_NO_REFERENCED_ROW_2 +} + type patternReplacer func(string) string // likePattern returns a pattern to match m with LIKE. diff --git a/server/datastore/mysql/packs.go b/server/datastore/mysql/packs.go index 3cad4129a3..b14854d971 100644 --- a/server/datastore/mysql/packs.go +++ b/server/datastore/mysql/packs.go @@ -75,22 +75,7 @@ func applyPackSpecDB(ctx context.Context, tx sqlx.ExtContext, spec *fleet.PackSp if q.Name == "" { q.Name = q.QueryName } - - // Check if query exists ... we have to do this manually because the FK - // constraint was removed as part of the work required for combining queries and schedules - var count int - if err := tx.QueryRowxContext( - ctx, - `SELECT COUNT(1) FROM queries WHERE team_id_char = '' AND name = ?`, - q.QueryName, - ).Scan(&count); err != nil { - return ctxerr.Wrap(ctx, err, "checking if query exists") - } - if count == 0 { - return ctxerr.Errorf(ctx, "cannot schedule unknown query '%s'", q.QueryName) - } - - if _, err := tx.ExecContext(ctx, query, + _, err := tx.ExecContext(ctx, query, packID, q.QueryName, q.Name, @@ -102,7 +87,11 @@ func applyPackSpecDB(ctx context.Context, tx sqlx.ExtContext, spec *fleet.PackSp q.Platform, q.Version, q.Denylist, - ); err != nil { + ) + switch { + case isChildForeignKeyError(err): + return ctxerr.Errorf(ctx, "cannot schedule unknown query '%s'", q.QueryName) + case err != nil: return ctxerr.Wrapf(ctx, err, "adding query %s referencing %s", q.Name, q.QueryName) } } diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 652e4d1f0f..47c67221a2 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -1018,7 +1018,7 @@ CREATE TABLE `queries` ( `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 DEFAULT '', + `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', @@ -1029,7 +1029,7 @@ CREATE TABLE `queries` ( KEY `author_id` (`author_id`), KEY `fk_queries_team_id` (`team_id`), 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 ON UPDATE CASCADE + 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 */; @@ -1073,10 +1073,13 @@ 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 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`), + 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`), 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 */; From 3ede5f8d8508170a986037c99c34b8a00964d2f3 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Mon, 10 Jul 2023 14:56:44 -0400 Subject: [PATCH 20/78] Make team_id_char not null --- .../tables/20230706141219_QueriesTableSchemaChanges.go | 2 +- server/datastore/mysql/schema.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go index 0c6dd60012..783b586142 100644 --- a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go +++ b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go @@ -15,7 +15,7 @@ func Up_20230706141219(tx *sql.Tx) error { // table changed. if _, err := tx.Exec(` ALTER TABLE scheduled_queries - ADD team_id_char CHAR(10) DEFAULT '', + 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") diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 47c67221a2..12321970f4 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -1073,7 +1073,7 @@ 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 DEFAULT '', + `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`), From 19d3c29fe0091cbc081de6e091451243ae0cd117 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Mon, 10 Jul 2023 15:54:25 -0400 Subject: [PATCH 21/78] Allow users to delete queries being used by packs and scheduled_queries --- .../tables/20230706141219_QueriesTableSchemaChanges.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go index 783b586142..1d99f201f1 100644 --- a/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go +++ b/server/datastore/mysql/migrations/tables/20230706141219_QueriesTableSchemaChanges.go @@ -45,7 +45,7 @@ func Up_20230706141219(tx *sql.Tx) error { // 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); + 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") } From 22a6848bc3b349ff4a9b22f7421133e0b72e5703 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Mon, 10 Jul 2023 16:01:46 -0400 Subject: [PATCH 22/78] Updated test schema --- server/datastore/mysql/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 12321970f4..421e82967e 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -1079,7 +1079,7 @@ CREATE TABLE `scheduled_queries` ( KEY `scheduled_queries_pack_id` (`pack_id`), KEY `scheduled_queries_query_name` (`query_name`), 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`), + 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 */; From 2cc7869907b57947c3a8f0ad2d8f52a355e80652 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 13 Jul 2023 08:24:26 -0400 Subject: [PATCH 23/78] Account for possible replication lag --- server/datastore/mysql/testing_utils.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/datastore/mysql/testing_utils.go b/server/datastore/mysql/testing_utils.go index 23f6b85734..a6584df6c4 100644 --- a/server/datastore/mysql/testing_utils.go +++ b/server/datastore/mysql/testing_utils.go @@ -103,6 +103,10 @@ 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 ... + if strings.Contains(err.Error(), "check that column/key exists") { + continue + } require.NoError(t, err) } From 271956158cf3f00661c71c016c8ec17f52fbab64 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 13 Jul 2023 13:10:09 -0400 Subject: [PATCH 24/78] Check if error is not nil --- server/datastore/mysql/testing_utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/datastore/mysql/testing_utils.go b/server/datastore/mysql/testing_utils.go index a6584df6c4..243bd574db 100644 --- a/server/datastore/mysql/testing_utils.go +++ b/server/datastore/mysql/testing_utils.go @@ -103,10 +103,10 @@ 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 ... - if strings.Contains(err.Error(), "check that column/key exists") { + if err != nil && strings.Contains(err.Error(), "check that column/key exists") { continue } + require.NoError(t, err) } From 15f955350af532287d4c5458d9815e0176dc630d Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 13 Jul 2023 13:46:09 -0400 Subject: [PATCH 25/78] Add missing comment --- server/datastore/mysql/testing_utils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/datastore/mysql/testing_utils.go b/server/datastore/mysql/testing_utils.go index 243bd574db..feb1722fe1 100644 --- a/server/datastore/mysql/testing_utils.go +++ b/server/datastore/mysql/testing_utils.go @@ -103,6 +103,7 @@ 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 } From 1d8d6d8c152aed8311f0089dd7e33dd6dbfb6b59 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 13 Jul 2023 15:04:08 -0400 Subject: [PATCH 26/78] Frontend: Combine queries and schedules mock API calls for FE development (#12670) --- frontend/__mocks__/scheduleableQueryMock.ts | 39 ++++++++ frontend/interfaces/query.ts | 21 +--- frontend/services/entities/queries.ts | 3 + .../services/mock_service/mocks/config.ts | 14 +++ .../services/mock_service/mocks/responses.ts | 95 +++++++++++++++++++ 5 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 frontend/__mocks__/scheduleableQueryMock.ts diff --git a/frontend/__mocks__/scheduleableQueryMock.ts b/frontend/__mocks__/scheduleableQueryMock.ts new file mode 100644 index 0000000000..e437edc121 --- /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: 3600, + 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: 1, + system_time_p95: 1, + user_time_p50: 1, + user_time_p95: 1, + total_executions: 3, + }, +}; + +const createMockSchedulableQuery = ( + overrides?: Partial +): ISchedulableQuery => { + return { ...DEFAULT_SCHEDULABLE_QUERY_MOCK, ...overrides }; +}; + +export default createMockSchedulableQuery; diff --git a/frontend/interfaces/query.ts b/frontend/interfaces/query.ts index cd3b6be1c7..96a8efa4d1 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -1,24 +1,7 @@ -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 { 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; diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 1d47ab2360..384c0604f6 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -5,6 +5,9 @@ import { IQueryFormData } from "interfaces/query"; import { ISelectedTargets } from "interfaces/target"; import { AxiosResponse } from "axios"; +// 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) => { const { QUERIES } = endpoints; diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index ab7c5583ec..7b277c7e72 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -22,6 +22,11 @@ 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.queries, + "queries/1": RESPONSES.query1, + "queries/2": RESPONSES.query2, + "queries/3": RESPONSES.query3, }, POST: { // request body is ISelectedTargets @@ -31,6 +36,15 @@ 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;", + 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..d9f0445840 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -364,8 +364,103 @@ const labels = { ], }; +// "SchedulableQueries" to be used in developing frontend for #7765 +const queries = { + queries: [ + { + 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: 3600, + 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", + description: "A second test query", + query: "SELECT * FROM osquery_info", + team_id: 1, + interval: 3600, + platform: "linux", + 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: 3, + name: "Test Query 3", + description: "A third test query", + query: "SELECT * FROM osquery_info", + team_id: 2, + interval: 3600, + platform: "", + 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, + }, + }, + ], +}; + +const query1 = { query: queries.queries[0] }; +const query2 = { query: queries.queries[1] }; +const query3 = { query: queries.queries[2] }; + export default { count, hosts, labels, + queries, + query1, + query2, + query3, }; From 7ff4b77fb9aa5fdefe724c7df3cd5dfe4bd5c967 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 13 Jul 2023 12:11:11 -0700 Subject: [PATCH 27/78] UI: Merge scheduling functionality into queries page (#12713) ## Addresses #12636 ### See issue for list work done ![Screenshot 2023-07-12 at 6 47 04 PM](https://github.com/fleetdm/fleet/assets/61553566/47e3e5b2-0195-4f54-a377-8e5c03313acf) ![Frame-12-07-2023-06-43-32](https://github.com/fleetdm/fleet/assets/61553566/f72f2d41-609f-4409-8595-5f3e4f06d9bb) ### Notes for review: - Because other work is based on this branch, TODOs / fixes are noted here until the team comes to a strategy for merging all of the work: - Add missing space in the Performance impact column "Undetermined" tooltip text - I'm having trouble confirming that the inherited queries table is working right with the mock hard-coded data, though I did see it working correctly previously. There's an issue with the page reverting to "All teams" when trying to show the inherited table, though it does show the table before re-rendering. - This work is organized clearly by commit, so that might be a manageable way to go through this code. - Since the updated API for this work is not yet complete, this work can be manually tested by either: - Using mock API infrastructure, or - in `ManageQueriesPage.tsx`, comment out the two `useQuery` calls and add appropriate mock data. You can then modify any fields of interest to test their related UI functionality. For example, lines 119 -242 might read: ``` // const { // data: curTeamEnhancedQueries, // error: curTeamQueriesError, // isFetching: isFetchingCurTeamQueries, // refetch: refetchCurTeamQueries, // } = useQuery( // [{ scope: "queries", teamId: teamIdForApi }], // () => queriesAPI.loadAll(teamIdForApi), // { // refetchOnWindowFocus: false, // enabled: isRouteOk, // select: (data) => data.queries.map(enhanceQuery), // } // ); // // If a team is selected, fetch inherited global queries as well // const { // data: globalEnhancedQueries, // error: globalQueriesError, // isFetching: isFetchingGlobalQueries, // refetch: refetchGlobalQueries, // } = useQuery( // [{ scope: "queries", teamId: -1 }], // () => queriesAPI.loadAll(), // { // refetchOnWindowFocus: false, // enabled: isRouteOk && isAnyTeamSelected, // select: (data) => data.queries.map(enhanceQuery), // } // ); const [ curTeamEnhancedQueries, curTeamQueriesError, isFetchingCurTeamQueries, refetchCurTeamQueries, ] = useMemo(() => { return [ [ { created_at: "2023-06-08T15:31:35Z", updated_at: "2023-06-08T15:31:35Z", id: 2, name: "test", description: "", query: "SELECT * FROM osquery_info;", team_id: 43, platform: "darwin", min_osquery_version: "", automations_enabled: true, logging: "snapshot", saved: true, // interval: 300, interval: 0, observer_can_run: false, 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: ["darwin"], }, ] as IEnhancedQuery[], undefined, false, () => { console.log("got the new queries"); }, ]; }, []); const [ globalEnhancedQueries, globalQueriesError, isFetchingGlobalQueries, refetchGlobalQueries, ] = useMemo(() => { return [ [ { created_at: "2023-06-08T15:31:35Z", updated_at: "2023-06-08T15:31:35Z", id: 200, name: "test", description: "", query: "SELECT * FROM osquery_info;", team_id: null, platform: "darwin", min_osquery_version: "", automations_enabled: true, logging: "snapshot", saved: true, // interval: 300, interval: 0, observer_can_run: false, 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: ["darwin"], }, ] as IEnhancedQuery[], undefined, false, () => { console.log("got the new inherited queries"); }, ]; }, []); ``` - [x] Changes file added for user-visible changes in `changes/` - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- changes/12636-merge-schedule-into-queries | 1 + .../StatusIndicator/StatusIndicator.tsx | 2 +- .../components/StatusIndicator/_styles.scss | 20 + .../DataTable/PillCell/PillCell.tests.tsx | 4 +- .../DataTable/PillCell/PillCell.tsx | 12 +- .../DataTable/TextCell/TextCell.tsx | 29 +- .../TableContainer/DataTable/_styles.scss | 6 + .../buttons/RevealButton/RevealButton.tsx | 2 +- .../components/top_nav/SiteTopNav/navItems.ts | 9 - .../HostActionsDropdown.tests.tsx | 16 - .../HostActionsDropdown/helpers.tsx | 9 +- .../HostDetailsPage/HostDetailsPage.tsx | 110 ---- .../cards/Packs/PackTable/PackTableConfig.tsx | 1 - .../hosts/details/cards/Schedule/Schedule.tsx | 80 --- .../cards/Schedule/ScheduleTableConfig.tsx | 123 ---- .../hosts/details/cards/Schedule/_styles.scss | 29 - .../hosts/details/cards/Schedule/index.ts | 1 - .../ManageQueriesPage/ManageQueriesPage.tsx | 329 ++++++++--- .../queries/ManageQueriesPage/_styles.scss | 28 +- .../AutomationsModal.tsx | 19 + .../ManageAutomationsModal.tsx | 21 + .../ManageAutomationsModal/index.ts | 1 + .../QueriesTable/QueriesTableConfig.tsx | 98 +++- .../ManageSchedulePage/ManageSchedulePage.tsx | 546 ------------------ .../schedule/ManageSchedulePage/_styles.scss | 122 ---- .../PreviewDataModal/PreviewDataModal.tsx | 61 -- .../components/PreviewDataModal/_styles.scss | 14 - .../components/PreviewDataModal/index.ts | 1 - .../RemoveScheduledQueryModal.tsx | 47 -- .../RemoveScheduledQueryModal/index.ts | 1 - .../ScheduleEditorModal.tsx | 364 ------------ .../ScheduleEditorModal/_styles.scss | 26 - .../components/ScheduleEditorModal/index.ts | 1 - .../ScheduleTable/ScheduleTable.tsx | 224 ------- .../ScheduleTable/ScheduleTableConfig.tsx | 272 --------- .../components/ScheduleTable/index.ts | 1 - .../schedule/ManageSchedulePage/index.ts | 1 - frontend/router/index.tsx | 10 - frontend/services/entities/queries.ts | 15 +- 39 files changed, 433 insertions(+), 2223 deletions(-) create mode 100644 changes/12636-merge-schedule-into-queries delete mode 100644 frontend/pages/hosts/details/cards/Schedule/Schedule.tsx delete mode 100644 frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx delete mode 100644 frontend/pages/hosts/details/cards/Schedule/_styles.scss delete mode 100644 frontend/pages/hosts/details/cards/Schedule/index.ts create mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx create mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx create mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/_styles.scss delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/index.ts 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/frontend/components/StatusIndicator/StatusIndicator.tsx b/frontend/components/StatusIndicator/StatusIndicator.tsx index 7bae7226c2..7f1d81816f 100644 --- a/frontend/components/StatusIndicator/StatusIndicator.tsx +++ b/frontend/components/StatusIndicator/StatusIndicator.tsx @@ -7,7 +7,7 @@ interface IStatusIndicatorProps { value: string; tooltip?: { id: number; - tooltipText: string; + tooltipText: string | JSX.Element; position?: "top" | "bottom"; }; } diff --git a/frontend/components/StatusIndicator/_styles.scss b/frontend/components/StatusIndicator/_styles.scss index 09c490804e..d5eb1721c7 100644 --- a/frontend/components/StatusIndicator/_styles.scss +++ b/frontend/components/StatusIndicator/_styles.scss @@ -52,4 +52,24 @@ background-color: $ui-warning; } } + + // Query automations status + &--on { + &:before { + background-color: $ui-success; + } + } + &--off { + &:before { + background-color: $ui-offline; + } + } + &--paused { + .status-tooltip { + text-transform: none; + } + &:before { + background-color: $ui-offline; + } + } } 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..6d81789f42 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx @@ -7,18 +7,13 @@ import ReactTooltip from "react-tooltip"; interface IPillCellProps { value: { indicator: string; id: number }; customIdPrefix?: string; - hostDetails?: boolean; } const generateClassTag = (rawValue: string): string => { return rawValue.replace(" ", "-").toLowerCase(); }; -const PillCell = ({ - value, - customIdPrefix, - hostDetails, -}: IPillCellProps): JSX.Element => { +const PillCell = ({ value, customIdPrefix }: IPillCellProps): JSX.Element => { const { indicator, id } = value; const pillClassName = classnames( "data-table__pill", @@ -75,9 +70,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 at least one host. ); default: 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..3a850a34b7 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -220,6 +220,9 @@ $shadow-transition-width: 10px; text-overflow: ellipsis; white-space: nowrap; margin: 0; + .__react_component_tooltip { + white-space: normal; + } } .w400 { max-width: calc(400px - 48px); @@ -234,6 +237,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/top_nav/SiteTopNav/navItems.ts b/frontend/components/top_nav/SiteTopNav/navItems.ts index 203e14f597..fd466b07d5 100644 --- a/frontend/components/top_nav/SiteTopNav/navItems.ts +++ b/frontend/components/top_nav/SiteTopNav/navItems.ts @@ -78,15 +78,6 @@ export default ( 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"] }, - }, { name: "Policies", location: { 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/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index 82eede9937..a03079f7f3 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -10,11 +10,6 @@ const DEFAULT_OPTIONS: IDropdownOption[] = [ disabled: false, premiumOnly: true, }, - { - label: "Query", - value: "query", - disabled: false, - }, { label: "Show disk encryption key", value: "diskEncryption", @@ -122,9 +117,7 @@ const setOptionsAsDisabled = ( let optionsToDisable: IDropdownOption[] = []; if (!isHostOnline) { optionsToDisable = optionsToDisable.concat( - options.filter( - (option) => option.value === "query" || option.value === "mdmOff" - ) + options.filter((option) => option.value === "mdmOff") ); } if (isSandboxMode) { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index f67a3f13d7..820915caa2 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -9,7 +9,6 @@ import { pick } from "lodash"; import PATHS from "router/paths"; import hostAPI from "services/entities/hosts"; -import queryAPI from "services/entities/queries"; import teamAPI, { ILoadTeamsResponse } from "services/entities/teams"; import { AppContext } from "context/app"; import { PolicyContext } from "context/policy"; @@ -18,14 +17,11 @@ import { IHost, IDeviceMappingResponse, IMacadminsResponse, - IPackStats, IHostResponse, IHostMdmData, } 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"; @@ -36,7 +32,6 @@ import InfoBanner from "components/InfoBanner"; import BackLink from "components/BackLink"; import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers"; -import permissions from "utilities/permissions"; import HostSummaryCard from "../cards/HostSummary"; import AboutCard from "../cards/About"; @@ -46,9 +41,6 @@ import MunkiIssuesCard from "../cards/MunkiIssues"; import SoftwareCard from "../cards/Software"; 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"; @@ -95,12 +87,6 @@ interface IHostDetailsSubNavItem { pathname: string; } -const TAGGED_TEMPLATES = { - queryByHostRoute: (hostId: number | undefined | null) => { - return `${hostId ? `?host_ids=${hostId}` : ""}`; - }, -}; - const HostDetailsPage = ({ route, router, @@ -113,9 +99,7 @@ const HostDetailsPage = ({ const { config, - currentUser, isGlobalAdmin = false, - isGlobalObserver, isPremiumTier = false, isSandboxMode, isOnlyObserver, @@ -135,7 +119,6 @@ const HostDetailsPage = ({ const [showDeleteHostModal, setShowDeleteHostModal] = useState(false); const [showTransferHostModal, setShowTransferHostModal] = useState(false); - const [showSelectQueryModal, setShowSelectQueryModal] = useState(false); const [showPolicyDetailsModal, setPolicyDetailsModal] = useState(false); const [showOSPolicyModal, setShowOSPolicyModal] = useState(false); const [showMacSettingsModal, setShowMacSettingsModal] = useState(false); @@ -151,26 +134,11 @@ const HostDetailsPage = ({ const [refetchStartTime, setRefetchStartTime] = useState(null); const [showRefetchSpinner, setShowRefetchSpinner] = useState(false); - const [packsState, setPacksState] = useState(); - const [schedule, setSchedule] = 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, - Error, - IQuery[] - >("fleet queries", () => queryAPI.loadAll(), { - enabled: !!hostIdFromURL, - refetchOnMount: false, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - select: (data: IFleetQueriesResponse) => data.queries, - }); - const { data: teams } = useQuery( "teams", () => teamAPI.loadAll(), @@ -294,27 +262,6 @@ const HostDetailsPage = ({ } setHostSoftware(returnedHost.software || []); setUsersState(returnedHost.users || []); - if (returnedHost.pack_stats) { - const packStatsByType = returnedHost.pack_stats.reduce( - ( - dictionary: { - packs: IPackStats[]; - schedule: IQueryStats[]; - }, - pack: IPackStats - ) => { - if (pack.type === "pack") { - dictionary.packs.push(pack); - } else { - dictionary.schedule.push(...pack.query_stats); - } - return dictionary; - }, - { packs: [], schedule: [] } - ); - setPacksState(packStatsByType.packs); - setSchedule(packStatsByType.schedule); - } }, onError: (error) => handlePageError(error), } @@ -480,17 +427,6 @@ const HostDetailsPage = ({ : router.push(PATHS.MANAGE_HOSTS_LABEL(label.id)); }; - const onQueryHostCustom = () => { - router.push(PATHS.NEW_QUERY + TAGGED_TEMPLATES.queryByHostRoute(host?.id)); - }; - - const onQueryHostSaved = (selectedQuery: IQuery) => { - router.push( - PATHS.EDIT_QUERY(selectedQuery) + - TAGGED_TEMPLATES.queryByHostRoute(host?.id) - ); - }; - const onTransferHostSubmit = async (team: ITeam) => { setIsUpdatingHost(true); @@ -528,9 +464,6 @@ const HostDetailsPage = ({ case "transfer": setShowTransferHostModal(true); break; - case "query": - setShowSelectQueryModal(true); - break; case "diskEncryption": setShowDiskEncryptionModal(true); break; @@ -576,11 +509,6 @@ const HostDetailsPage = ({ title: "software", pathname: PATHS.HOST_SOFTWARE(hostIdFromURL), }, - { - name: "Schedule", - title: "schedule", - pathname: PATHS.HOST_SCHEDULE(hostIdFromURL), - }, { name: ( <> @@ -615,23 +543,6 @@ const HostDetailsPage = ({ host?.mdm.name === "Fleet" && host?.mdm.macos_settings?.disk_encryption === "action_required"; - /* Context team id might be different that host's team id - Observer plus must be checked against host's team id */ - const isGlobalOrHostsTeamObserverPlus = - currentUser && host?.team_id - ? permissions.isObserverPlus(currentUser, host.team_id) - : false; - - const isHostsTeamObserver = - currentUser && host?.team_id - ? permissions.isTeamObserver(currentUser, host.team_id) - : false; - - const canViewPacks = - !isGlobalObserver && - !isGlobalOrHostsTeamObserverPlus && - !isHostsTeamObserver; - const bootstrapPackageData = { status: host?.mdm.macos_setup?.bootstrap_package_status, details: host?.mdm.macos_setup?.details, @@ -738,16 +649,6 @@ const HostDetailsPage = ({ /> )} - - - {canViewPacks && ( - - )} - )} - {showSelectQueryModal && host && ( - setShowSelectQueryModal(false)} - queries={fleetQueries || []} - queryErrors={fleetQueriesError} - isOnlyObserver={isOnlyObserver} - onQueryHostCustom={onQueryHostCustom} - onQueryHostSaved={onQueryHostSaved} - hostsTeamId={host?.team_id} - /> - )} {!!host && showTransferHostModal && ( setShowTransferHostModal(false)} diff --git a/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx b/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx index 928074d219..36ca84584a 100644 --- a/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx @@ -104,7 +104,6 @@ const generatePackTableHeaders = (): IDataColumn[] => { ), }, diff --git a/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx b/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx deleted file mode 100644 index 1c37493510..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from "react"; - -import { IQueryStats } from "interfaces/query_stats"; -import TableContainer from "components/TableContainer"; -import EmptyTable from "components/EmptyTable"; -import CustomLink from "components/CustomLink"; - -import { generateTableHeaders, generateDataSet } from "./ScheduleTableConfig"; - -const baseClass = "schedule"; - -interface IScheduleProps { - schedule?: IQueryStats[]; - isChromeOSHost: boolean; - isLoading: boolean; -} - -const Schedule = ({ - schedule, - isChromeOSHost, - isLoading, -}: IScheduleProps): JSX.Element => { - const wrapperClassName = `${baseClass}__pack-table`; - const tableHeaders = generateTableHeaders(); - - const renderEmptyScheduleTab = () => { - if (isChromeOSHost) { - return ( - - Interested in collecting data from your Chromebooks? - - - } - /> - ); - } - return ( - - ); - }; - - return ( -
-

Schedule

- {!schedule || !schedule.length || isChromeOSHost ? ( - renderEmptyScheduleTab() - ) : ( -
- null} - resultsTitle={"queries"} - defaultSortHeader={"scheduled_query_name"} - defaultSortDirection={"asc"} - showMarkAllPages={false} - isAllPagesSelected={false} - emptyComponent={() => <>} - disablePagination - disableCount - /> -
- )} -
- ); -}; - -export default Schedule; diff --git a/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx b/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx deleted file mode 100644 index c02374741b..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from "react"; - -import { IQueryStats } from "interfaces/query_stats"; -import { performanceIndicator, secondsToDhms } from "utilities/helpers"; - -import TextCell from "components/TableContainer/DataTable/TextCell"; -import PillCell from "components/TableContainer/DataTable/PillCell"; -import TooltipWrapper from "components/TooltipWrapper"; - -interface IHeaderProps { - column: { - title: string; - isSortedDesc: boolean; - }; -} - -interface IRowProps { - row: { - original: IQueryStats; - }; -} - -interface ICellProps extends IRowProps { - cell: { - value: string | number | boolean; - }; -} - -interface IPillCellProps extends IRowProps { - cell: { - value: { - indicator: string; - id: number; - }; - }; -} - -interface IDataColumn { - title?: string; - Header: ((props: IHeaderProps) => JSX.Element) | string; - accessor: string; - Cell: - | ((props: ICellProps) => JSX.Element) - | ((props: IPillCellProps) => JSX.Element); - disableHidden?: boolean; - disableSortBy?: boolean; -} - -interface IScheduleTable extends Partial { - frequency: string; - performance: { indicator: string; id: number }; -} - -// NOTE: cellProps come from react-table -// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties -const generateTableHeaders = (): IDataColumn[] => { - return [ - { - title: "Query", - Header: "Query", - disableSortBy: true, - accessor: "query_name", - Cell: (cellProps: ICellProps) => ( - - ), - }, - { - title: "Frequency", - Header: "Frequency", - disableSortBy: true, - accessor: "frequency", - Cell: (cellProps: ICellProps) => ( - - ), - }, - { - Header: () => { - return ( - - Performance impact - - ); - }, - disableSortBy: true, - accessor: "performance", - Cell: (cellProps: IPillCellProps) => ( - - ), - }, - ]; -}; - -const enhanceScheduleData = (query_stats: IQueryStats[]): IScheduleTable[] => { - return Object.values(query_stats).map((query) => { - const scheduledQueryPerformance = { - user_time_p50: query.user_time, - system_time_p50: query.system_time, - total_executions: query.executions, - }; - return { - query_name: query.query_name, - frequency: secondsToDhms(query.interval), - performance: { - indicator: performanceIndicator(scheduledQueryPerformance), - id: query.scheduled_query_id, - }, - }; - }); -}; - -const generateDataSet = (query_stats: IQueryStats[]): IScheduleTable[] => { - if (!query_stats) { - return query_stats; - } - - return [...enhanceScheduleData(query_stats)]; -}; - -export { generateTableHeaders, generateDataSet }; diff --git a/frontend/pages/hosts/details/cards/Schedule/_styles.scss b/frontend/pages/hosts/details/cards/Schedule/_styles.scss deleted file mode 100644 index 4841674726..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/_styles.scss +++ /dev/null @@ -1,29 +0,0 @@ -.section--schedule { - margin-top: $pad-medium; - .section__header { - margin-bottom: $pad-medium; - } - .table-container__header { - display: none; - } - .data-table-block { - .data-table__table { - thead { - .query_name__header { - width: $col-lg; - } - .frequency__header { - width: $col-md; - } - } - tbody { - .query_name__cell { - width: $col-lg; - } - .frequency__cell { - width: $col-md; - } - } - } - } -} diff --git a/frontend/pages/hosts/details/cards/Schedule/index.ts b/frontend/pages/hosts/details/cards/Schedule/index.ts deleted file mode 100644 index 39250f5640..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./Schedule"; diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index f6d46297ff..0f36a049c3 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -5,7 +5,7 @@ import React, { useMemo, useState, } from "react"; -import { RouteProps, InjectedRouter } from "react-router"; +import { InjectedRouter } from "react-router"; import { useQuery } from "react-query"; import { pick } from "lodash"; @@ -14,8 +14,11 @@ 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 { + IListQueriesResponse, + 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"; @@ -23,27 +26,31 @@ 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"; 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 { +interface IEnhancedQuery extends ISchedulableQuery { performance: string; platforms: string[]; } @@ -54,7 +61,7 @@ const getPlatforms = (queryString: string): Array => { return platforms || [DEFAULT_EMPTY_CELL_VALUE]; }; -const enhanceQuery = (q: IQuery) => { +const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { return { ...q, performance: performanceIndicator( @@ -71,51 +78,74 @@ const ManageQueriesPage = ({ const queryParams = location.query; const { + isGlobalAdmin, + isTeamAdmin, isOnlyObserver, isObserverPlus, isAnyTeamObserverPlus, + isOnGlobalTeam, setFilteredQueriesPath, filteredQueriesPath, + isPremiumTier, + isSandboxMode, } = 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 [isUpdatingQueries, setIsUpdatingQueries] = useState(false); + const [showManageAutomationsModal, setShowManageAutomationsModal] = useState( + false + ); + const [showInheritedQueries, setShowInheritedQueries] = 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( + [{ scope: "queries", teamId: teamIdForApi }], + () => queriesAPI.loadAll(teamIdForApi), { refetchOnWindowFocus: false, - select: (data: IFleetQueriesResponse) => data.queries, + enabled: isRouteOk, + select: (data) => data.queries.map(enhanceQuery), } ); - 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, fetch inherited global queries as well + const { + data: globalEnhancedQueries, + error: globalQueriesError, + isFetching: isFetchingGlobalQueries, + refetch: refetchGlobalQueries, + } = useQuery( + [{ scope: "queries", teamId: -1 }], + () => queriesAPI.loadAll(), + { + refetchOnWindowFocus: false, + enabled: isRouteOk && isAnyTeamSelected, + select: (data) => data.queries.map(enhanceQuery), } - }, [enhancedQueriesList, isFetchingFleetQueries]); + ); useEffect(() => { const path = location.pathname + location.search; @@ -130,85 +160,152 @@ const ManageQueriesPage = ({ setShowDeleteQueryModal(!showDeleteQueryModal); }, [showDeleteQueryModal, setShowDeleteQueryModal]); + const toggleManageAutomationsModal = useCallback(() => { + setShowManageAutomationsModal(!showManageAutomationsModal); + }, [showManageAutomationsModal, setShowManageAutomationsModal]); + const onDeleteQueryClick = (selectedTableQueryIds: number[]) => { toggleDeleteQueryModal(); setSelectedQueryIds(selectedTableQueryIds); }; - const onDeleteQuerySubmit = useCallback(async () => { - const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries"; + const refetchAllQueries = useCallback(() => { + refetchCurTeamQueries(); + refetchGlobalQueries(); + }, [refetchCurTeamQueries, refetchGlobalQueries]); + 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 renderModals = () => { + return ( + <> {showDeleteQueryModal && ( )} + {showManageAutomationsModal && ( + + )} + + ); + }; + + 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..6aee329207 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -56,7 +56,7 @@ &__action-button-container { display: flex; - align-items: flex-start; + gap: $pad-small; } .form-field--dropdown { @@ -103,11 +103,11 @@ .platforms__header { width: $col-sm; } - .author_name__header { + .updated_at__header { display: none; width: 0; } - .updated_at__header { + .performance__header { display: none; width: 0; } @@ -151,28 +151,16 @@ .platforms__cell { max-width: $col-md; } - .author_name__cell { - display: none; - max-width: $col-md; - img, - div, - span { - display: flex; - align-items: center; - } - div { - padding-right: $pad-small; - } - .author-name { - display: block; - } - } .updated_at__cell { display: none; max-width: $col-md; } + .performance__cell { + display: none; + max-width: $col-md; + } @media (min-width: $break-md) { - .author_name__cell { + .performance__cell { display: table-cell; } } 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..cb6abd9cb8 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +import Modal from "components/Modal"; + +const baseClass = "automations-modal"; + +interface IManageAutomationsModalProps { + onExit: () => void; +} + +const ManageAutomationsModal = ({ + onExit, +}: IManageAutomationsModalProps): JSX.Element => { + return ( + +
+ + ); +}; + +export default ManageAutomationsModal; 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/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 26e5bcc58f..a728da89a1 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -7,12 +7,11 @@ 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 { ISchedulableQuery } from "interfaces/schedulable_query"; 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 +19,11 @@ 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 StatusIndicator from "components/StatusIndicator"; interface IQueryRow { id: string; - original: IQuery; + original: ISchedulableQuery; } interface IGetToggleAllRowsSelectedProps { @@ -46,7 +46,7 @@ interface IHeaderProps { } interface IRowProps { row: { - original: IQuery; + original: ISchedulableQuery; getToggleRowSelectedProps: () => IGetToggleAllRowsSelectedProps; toggleRowSelected: () => void; }; @@ -54,6 +54,18 @@ interface IRowProps { } interface ICellProps extends IRowProps { + cell: { + value: string | number | boolean; + }; +} + +interface INumberCellProps extends IRowProps { + cell: { + value: number; + }; +} + +interface IStringCellProps extends IRowProps { cell: { value: string; }; @@ -69,7 +81,9 @@ 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); id?: string; title?: string; accessor?: string; @@ -148,28 +162,26 @@ const generateTableHeaders = ({ }, }, { - 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", }, { Header: () => { @@ -189,7 +201,7 @@ const generateTableHeaders = ({ }, disableSortBy: true, accessor: "performance", - Cell: (cellProps: ICellProps) => ( + Cell: (cellProps: IStringCellProps) => ( ), }, + { + title: "Automations", + Header: "Automations", + disableSortBy: true, + accessor: "automations_enabled", + Cell: (cellProps: IStringCellProps): JSX.Element => { + let status; + if (cellProps.cell.value) { + if (cellProps.row.original.interval === 0) { + status = "paused"; + } else { + status = "on"; + } + } else { + status = "off"; + } + + const tooltip = + status === "paused" + ? { + id: cellProps.row.original.id, + tooltipText: ( + <> + Automations will resume for this query when + a frequency is set. + + ), + } + : undefined; + return ; + }, + }, { title: "Last modified", Header: (cellProps) => ( @@ -207,7 +251,7 @@ const generateTableHeaders = ({ /> ), accessor: "updated_at", - Cell: (cellProps: ICellProps): JSX.Element => ( + Cell: (cellProps: INumberCellProps): JSX.Element => ( 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/PreviewDataModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx deleted file mode 100644 index 3156c73288..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* This component is used for creating and editing both global and team scheduled queries */ - -import React from "react"; -import { syntaxHighlight } from "utilities/helpers"; - -import Modal from "components/Modal"; -import Button from "components/buttons/Button"; -import TooltipWrapper from "components/TooltipWrapper"; - -const baseClass = "preview-data-modal"; - -interface IPreviewDataModalProps { - onCancel: () => void; -} - -const PreviewDataModal = ({ - onCancel, -}: IPreviewDataModalProps): JSX.Element => { - const json = { - action: "snapshot", - snapshot: [ - { - remote_address: "0.0.0.0", - remote_port: "0", - cmdline: "/usr/sbin/syslogd", - }, - ], - name: "xxxxxxx", - hostIdentifier: "xxxxxxx", - calendarTime: "xxx xxx x xx:xx:xx xxxx UTC", - unixTime: "xxxxxxxxx", - epoch: "xxxxxxxxx", - counter: "x", - numerics: "x", - }; - - return ( - -
-

- - The data sent to your configured log destination will look similar - to the following JSON: - -

-
-
-        
-
- -
-
-
- ); -}; - -export default PreviewDataModal; 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/PreviewDataModal/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts deleted file mode 100644 index 48fca40136..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./PreviewDataModal"; 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 69909599bd..a4ef257c22 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"; @@ -169,7 +168,6 @@ const routes = ( - @@ -204,14 +202,6 @@ const routes = ( - - - - - - - - diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 384c0604f6..a4a512a2e2 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -4,6 +4,7 @@ import endpoints from "utilities/endpoints"; import { IQueryFormData } from "interfaces/query"; import { ISelectedTargets } from "interfaces/target"; import { AxiosResponse } from "axios"; +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"; @@ -25,16 +26,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, From 1e80f9d29a8f37d0031fb7e4ad885b3aade42c8d Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Thu, 13 Jul 2023 12:58:38 -0700 Subject: [PATCH 28/78] fix style issue from merge --- frontend/pages/queries/ManageQueriesPage/_styles.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 6aee329207..7bd58f022e 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -59,7 +59,7 @@ gap: $pad-small; } - .form-field--dropdown { + form-field--dropdown { margin: 0; } @@ -110,9 +110,7 @@ .performance__header { display: none; width: 0; - } - @media (min-width: $break-md) { - .author_name__header { + @media (min-width: $break-md) { display: table-cell; width: auto; } From 23ac9d226294ed4ddd8d1a40c3be2b735a94c0b7 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Thu, 13 Jul 2023 13:49:23 -0700 Subject: [PATCH 29/78] Have QueriesTable expect team_id param; update mocks --- .../components/QueriesTable/QueriesTable.tsx | 2 + .../services/mock_service/mocks/config.ts | 10 ++-- .../services/mock_service/mocks/responses.ts | 58 +++++++++++++++---- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index b36eee3747..9b6bce19cf 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -39,6 +39,7 @@ interface IQueriesTableProps { query?: string; order_key?: string; order_direction?: "asc" | "desc"; + team_id?: string; }; } @@ -143,6 +144,7 @@ const QueriesTable = ({ ) { newQueryParams.page = 0; } + newQueryParams.team_id = queryParams?.team_id; const locationPath = getNextLocationPath({ pathPrefix: PATHS.MANAGE_QUERIES, queryParams: newQueryParams, diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index 7b277c7e72..d7c7be8ce0 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -23,10 +23,12 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { // expensive data operations "targets?query={*}": RESPONSES.hosts, // "SchedulableQueries" to be used in developing frontend for #7765 - queries: RESPONSES.queries, - "queries/1": RESPONSES.query1, - "queries/2": RESPONSES.query2, - "queries/3": RESPONSES.query3, + queries: RESPONSES.globalQueries, + "queries/1": RESPONSES.globalQuery1, + "queries/2": RESPONSES.globalQuery2, + "queries/3": RESPONSES.globalQuery3, + "queries/4": RESPONSES.teamQuery1, + "queries?team_id=43": RESPONSES.teamQueries, }, POST: { // request body is ISelectedTargets diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index d9f0445840..9695079e90 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -365,7 +365,7 @@ const labels = { }; // "SchedulableQueries" to be used in developing frontend for #7765 -const queries = { +const globalQueries = { queries: [ { created_at: "2022-11-03T17:22:14Z", @@ -401,7 +401,7 @@ const queries = { name: "Test Query 2", description: "A second test query", query: "SELECT * FROM osquery_info", - team_id: 1, + team_id: null, interval: 3600, platform: "linux", min_osquery_version: "", @@ -428,7 +428,7 @@ const queries = { name: "Test Query 3", description: "A third test query", query: "SELECT * FROM osquery_info", - team_id: 2, + team_id: null, interval: 3600, platform: "", min_osquery_version: "", @@ -451,16 +451,54 @@ const queries = { ], }; -const query1 = { query: queries.queries[0] }; -const query2 = { query: queries.queries[1] }; -const query3 = { query: queries.queries[2] }; +const teamQueries = { + queries: [ + { + created_at: "2023-06-08T15:31:35Z", + updated_at: "2023-06-08T15:31:35Z", + id: 4, + 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: 1, + // system_time_p95: null, + user_time_p50: 1, + // user_time_p95: null, + total_executions: 1, + }, + performance: "Undetermined", + platforms: ["windows", "darwin", "linux"], + }, + ], +}; + +const globalQuery1 = { query: globalQueries.queries[0] }; +const globalQuery2 = { query: globalQueries.queries[1] }; +const globalQuery3 = { query: globalQueries.queries[2] }; +const teamQuery1 = { query: teamQueries.queries[0] }; export default { count, hosts, labels, - queries, - query1, - query2, - query3, + globalQueries, + globalQuery1, + globalQuery2, + globalQuery3, + teamQueries, + teamQuery1, }; From 8d559665530cf465fddfb3485f849b2ba7ee01bf Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 14 Jul 2023 13:37:09 -0400 Subject: [PATCH 30/78] Updated osquery/config endpoint to include scheduled queries (#12723) Updated GetClientConfig API endpoint --- ...clude-scheduled-queries-in-getclientconfig | 2 + server/datastore/mysql/hosts_test.go | 265 +++++++----------- server/datastore/mysql/packs.go | 17 +- server/datastore/mysql/packs_test.go | 41 +++ server/datastore/mysql/queries.go | 34 ++- server/datastore/mysql/queries_test.go | 55 ++++ server/fleet/app.go | 6 +- server/fleet/datastore.go | 2 +- server/fleet/queries.go | 42 +++ server/fleet/queries_test.go | 54 ++++ server/service/osquery.go | 54 +++- server/service/osquery_test.go | 72 +++++ 12 files changed, 457 insertions(+), 187 deletions(-) create mode 100644 changes/12644-include-scheduled-queries-in-getclientconfig 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/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index df9448f348..3f84b3300d 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -3621,29 +3621,6 @@ 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, nil, "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, nil, "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", @@ -3657,7 +3634,7 @@ func testHostsAllPackStats(t *testing.T, ds *Datastore) { 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 @@ -3665,23 +3642,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) @@ -3705,38 +3670,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, @@ -3756,22 +3689,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. @@ -3792,6 +3717,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(), @@ -3814,10 +3740,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, nil, "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}, @@ -3825,11 +3756,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, @@ -3841,11 +3772,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, @@ -3874,7 +3805,7 @@ 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) require.NoError(t, err) @@ -3921,6 +3852,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(), @@ -3938,69 +3870,76 @@ 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, nil, "global-time", "select * from time", 0, true) - globalSQuery1, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ + userQuery := test.NewQuery(t, ds, nil, "global-time", "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: userQuery.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: userQuery.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: userQuery.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: userQuery.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: userQuery.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}, @@ -4010,11 +3949,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: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8001, Denylisted: false, Executions: 165, @@ -4026,11 +3965,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: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8002, Denylisted: false, Executions: 166, @@ -4042,11 +3981,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: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8003, Denylisted: false, Executions: 167, @@ -4058,11 +3997,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: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8003, Denylisted: false, Executions: 167, @@ -4083,11 +4022,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: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 8003, Denylisted: false, Executions: 167, @@ -4101,7 +4040,7 @@ 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) require.NoError(t, err) @@ -4130,11 +4069,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: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 0, Denylisted: false, Executions: 0, @@ -4146,11 +4085,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: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 0, Denylisted: false, Executions: 0, @@ -4162,11 +4101,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: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 0, Denylisted: false, Executions: 0, @@ -4178,11 +4117,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: userQuery.Name, + PackName: userPack.Name, + PackID: userPack.ID, AverageMemory: 0, Denylisted: false, Executions: 0, diff --git a/server/datastore/mysql/packs.go b/server/datastore/mysql/packs.go index b14854d971..fd17e09bb4 100644 --- a/server/datastore/mysql/packs.go +++ b/server/datastore/mysql/packs.go @@ -577,10 +577,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 @@ -590,26 +590,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..a774e28269 100644 --- a/server/datastore/mysql/packs_test.go +++ b/server/datastore/mysql/packs_test.go @@ -38,6 +38,7 @@ func TestPacks(t *testing.T) { {"ApplySpecFailsOnTargetIDNull", testPacksApplySpecFailsOnTargetIDNull}, {"ApplyStatsNotLocking", testPacksApplyStatsNotLocking}, {"ApplyStatsNotLockingTryTwo", testPacksApplyStatsNotLockingTryTwo}, + {"ListForHostIncludesOnlyUserPacks", testListForHostIncludesOnlyUserPacks}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -683,3 +684,43 @@ 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)) + + _, err := ds.EnsureGlobalPack(ctx) + require.NoError(t, err) + + team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) + require.NoError(t, err) + + require.NoError(t, ds.AddHostsToTeam(ctx, &team.ID, []uint{h1.ID})) + _, err = ds.EnsureTeamPack(ctx, team.ID) + require.NoError(t, err) + + 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/queries.go b/server/datastore/mysql/queries.go index 1816c13087..0a8af019b0 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -353,24 +353,34 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions 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 - ` - if opt.OnlyObserverCanRun { - sql += " AND q.observer_can_run=true" - } + WHERE saved = true` args := []interface{}{false, aggregatedStatsTypeQuery} - whereClause := " AND team_id_char = ''" - if opt.TeamID != nil { - args = append(args, fmt.Sprint(*opt.TeamID)) - whereClause = " AND team_id_char = ?" - } - sql += whereClause + whereClauses := "" + if opt.OnlyObserverCanRun { + 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, args...); err != nil { return nil, ctxerr.Wrap(ctx, err, "listing queries") } diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index edb9d028fc..2d82321357 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" @@ -30,6 +31,7 @@ func TestQueries(t *testing.T) { {"ListFiltersObservers", testQueriesListFiltersObservers}, {"ObserverCanRunQuery", testObserverCanRunQuery}, {"ListFiltersByTeamID", testQueriesListFiltersByTeamID}, + {"ListFiltersByIsScheduled", testQueriesListFiltersByIsScheduled}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -614,3 +616,56 @@ func testQueriesListFiltersByTeamID(t *testing.T, ds *Datastore) { require.NoError(t, err) test.QueryElementsMatch(t, queries, []*fleet.Query{teamQ1, teamQ2, teamQ3}) } + +func testQueriesListFiltersByIsScheduled(t *testing.T, ds *Datastore) { + q1, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query1", + Query: "select 1;", + Saved: true, + ScheduleInterval: 0, + }) + require.NoError(t, err) + q2, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query2", + Query: "select 1;", + Saved: true, + ScheduleInterval: 10, + AutomationsEnabled: false, + }) + require.NoError(t, err) + q3, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query3", + Query: "select 1;", + Saved: true, + ScheduleInterval: 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) + } +} diff --git a/server/fleet/app.go b/server/fleet/app.go index 87dcbddf36..04512b0de6 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -762,8 +762,10 @@ type ListQueryOptions struct { ListOptions // TeamID which team the queries belong to. If teamID is nil, then it is assumed the 'global' - // team - TeamID *uint + // 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 28341a941f..9c0792d839 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -140,7 +140,7 @@ 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 diff --git a/server/fleet/queries.go b/server/fleet/queries.go index f6548448e1..5d450789c2 100644 --- a/server/fleet/queries.go +++ b/server/fleet/queries.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/ghodss/yaml" ) @@ -69,6 +70,36 @@ func (q *Query) TeamIDStr() string { return fmt.Sprint(*q.TeamID) } +func (q *Query) GetSnapshot() *bool { + var loggingType string + if q != nil { + loggingType = q.LoggingType + } + + switch loggingType { + case "snapshot": + return ptr.Bool(true) + default: + return nil + } +} + +func (q *Query) GetRemoved() *bool { + var loggingType string + if q != nil { + loggingType = q.LoggingType + } + + switch loggingType { + 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 { @@ -95,6 +126,17 @@ func (q *Query) Verify() error { return nil } +func (q *Query) ToQueryContent() QueryContent { + return QueryContent{ + Query: q.Query, + Interval: q.ScheduleInterval, + Platform: &q.Platform, + Version: &q.MinOsqueryVersion, + Removed: q.GetRemoved(), + Snapshot: q.GetSnapshot(), + } +} + type TargetedQuery struct { *Query HostTargets HostTargets `json:"host_targets"` diff --git a/server/fleet/queries_test.go b/server/fleet/queries_test.go index 24983d764b..9786ff5b12 100644 --- a/server/fleet/queries_test.go +++ b/server/fleet/queries_test.go @@ -8,6 +8,60 @@ import ( "github.com/stretchr/testify/require" ) +func TestGetSnapshot(t *testing.T) { + testCases := []struct { + query *Query + expected *bool + }{ + { + query: nil, + expected: nil, + }, + { + query: &Query{LoggingType: "snapshot"}, + expected: ptr.Bool(true), + }, + { + query: &Query{LoggingType: "differential"}, + expected: nil, + }, + { + query: &Query{LoggingType: "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{LoggingType: "snapshot"}, + expected: nil, + }, + { + query: &Query{LoggingType: "differential"}, + expected: ptr.Bool(true), + }, + { + query: &Query{LoggingType: "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 diff --git a/server/service/osquery.go b/server/service/osquery.go index 64ba71342e..af9d8e5286 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -346,6 +346,25 @@ func getClientConfigEndpoint(ctx context.Context, request interface{}, svc fleet }, nil } +func (svc *Service) getScheduledQueries(ctx context.Context, teamID *uint) (fleet.Queries, error) { + opts := fleet.ListQueryOptions{IsScheduled: ptr.Bool(true), TeamID: teamID} + queries, err := svc.ds.ListQueries(ctx, opts) + 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 +387,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 +433,37 @@ 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 { + team, err := svc.ds.Team(ctx, *host.TeamID) + if err != nil { + return nil, newOsqueryError("database error: " + err.Error()) + } + + if team != 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: %s", team.Name) + 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..af76ee5aba 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -40,6 +40,16 @@ import ( func TestGetClientConfig(t *testing.T) { ds := new(mock.Store) + ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { + return &fleet.Team{ + Name: "Alamo", + ID: 1, + }, nil + } + + 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 +71,30 @@ func TestGetClientConfig(t *testing.T) { return []*fleet.ScheduledQuery{}, nil } } + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { + if opt.TeamID == nil { + return nil, nil + } + return []*fleet.Query{ + { + Query: "SELECT 1 FROM table_1", + Name: "Some strings carry more weight than others", + ScheduleInterval: 10, + Platform: "linux", + MinOsqueryVersion: "5.12.2", + LoggingType: "snapshot", + TeamID: ptr.Uint(1), + }, + { + Query: "SELECT 1 FROM table_2", + Name: "You shall not pass", + ScheduleInterval: 20, + Platform: "macos", + LoggingType: "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 +112,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 +179,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: Alamo": { + "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) { From 53f57c44db8109539ecd5d8dd593a41ca9bc9ee6 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Mon, 17 Jul 2023 14:09:12 -0700 Subject: [PATCH 31/78] UI - Queries page updates, pt.1 (#12784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #12636 – follow-up work for PR #12713 - Update Platforms column to render the user-selected platforms for a query if any, otherwise those that are compatible Screenshot 2023-07-14 at 6 03 06 PM - Clean up typing and names around this column - Encapsulate logic for query automations column cells into new QueryAutomationsStatusIndicator component - Increase modularity and decrease coupling of StatusIndicator - Cleanly handle overflowing queries table due to very long query name Screenshot 2023-07-14 at 6 07 20 PM - Small copy and layout fixes - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- .../PlatformCompatibility.tsx | 10 +- .../StatusIndicator.stories.tsx | 1 - .../StatusIndicator/StatusIndicator.tsx | 78 +++++++----- .../components/StatusIndicator/_styles.scss | 20 --- .../DataTable/PillCell/PillCell.tsx | 2 +- .../PlatformCell/PlatformCell.stories.tsx | 2 +- .../PlatformCell/PlatformCell.tests.tsx | 9 +- .../DataTable/PlatformCell/PlatformCell.tsx | 9 +- .../QueryTablePlatforms.tsx | 8 +- frontend/context/policy.tsx | 10 +- frontend/hooks/usePlatformCompatibility.tsx | 4 +- frontend/hooks/usePlatformSelector.tsx | 7 +- frontend/interfaces/osquery_table.ts | 6 +- frontend/interfaces/platform.ts | 28 ++-- frontend/interfaces/policy.ts | 8 +- frontend/interfaces/schedulable_query.ts | 8 +- .../pages/DashboardPage/DashboardPage.tsx | 6 +- .../cards/HostsSummary/HostsSummary.tsx | 4 +- .../OperatingSystems/OperatingSystems.tsx | 6 +- .../hosts/ManageHostsPage/HostTableConfig.tsx | 1 - .../details/cards/HostSummary/HostSummary.tsx | 1 - .../components/PolicyForm/PolicyForm.tsx | 8 +- .../SaveNewPolicyModal/SaveNewPolicyModal.tsx | 4 +- frontend/pages/policies/constants.ts | 4 +- .../ManageQueriesPage/ManageQueriesPage.tsx | 16 +-- .../queries/ManageQueriesPage/_styles.scss | 120 +++++++++--------- .../QueriesTable/QueriesTableConfig.tsx | 65 +++++----- .../QueryAutomationsStatusIndicator.tsx | 44 +++++++ .../_styles.scss | 21 +++ .../QueryAutomationsStatusIndicator/index.ts | 1 + frontend/services/entities/hosts.ts | 6 +- .../services/entities/operating_systems.ts | 4 +- frontend/utilities/constants.ts | 4 +- frontend/utilities/osquery_tables.ts | 2 +- frontend/utilities/sql_tools.ts | 13 +- 35 files changed, 302 insertions(+), 238 deletions(-) create mode 100644 frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator.tsx create mode 100644 frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/_styles.scss create mode 100644 frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/index.ts 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/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 7f1d81816f..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 | 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/StatusIndicator/_styles.scss b/frontend/components/StatusIndicator/_styles.scss index d5eb1721c7..09c490804e 100644 --- a/frontend/components/StatusIndicator/_styles.scss +++ b/frontend/components/StatusIndicator/_styles.scss @@ -52,24 +52,4 @@ background-color: $ui-warning; } } - - // Query automations status - &--on { - &:before { - background-color: $ui-success; - } - } - &--off { - &:before { - background-color: $ui-offline; - } - } - &--paused { - .status-tooltip { - text-transform: none; - } - &:before { - background-color: $ui-offline; - } - } } diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx index 6d81789f42..525ce31d87 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx @@ -70,7 +70,7 @@ const PillCell = ({ value, customIdPrefix }: IPillCellProps): JSX.Element => { case "Undetermined": return ( <> - To see performance impact, this query must have run with + To see performance impact, this query must have run with{" "} automations on at least one host. ); 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..43cbef73bf 100644 --- a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx +++ b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx @@ -1,8 +1,9 @@ import React from "react"; import Icon from "components/Icon"; +import { SupportedPlatform } from "interfaces/platform"; interface IPlatformCellProps { - value: string[]; + platforms: SupportedPlatform[]; } const baseClass = "platform-cell"; @@ -14,7 +15,7 @@ const ICONS: Record = { chrome: "chrome", }; -const DISPLAY_ORDER = [ +const DISPLAY_ORDER: SupportedPlatform[] = [ "darwin", "windows", "linux", @@ -23,9 +24,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) ); 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/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/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/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index bd2961b547..aa9bc46013 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 } 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; @@ -55,7 +55,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; @@ -100,7 +100,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/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 = ({ => { +const getPlatforms = ( + queryString: string +): SupportedPlatform[] | typeof DEFAULT_EMPTY_CELL_VALUE[] => { const { platforms } = checkPlatformCompatibility(queryString); return platforms || [DEFAULT_EMPTY_CELL_VALUE]; diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 7bd58f022e..43f54b491e 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -95,76 +95,81 @@ } .data-table-block { - .data-table__table { - thead { - .name__header { - width: auto; - } - .platforms__header { - width: $col-sm; - } - .updated_at__header { - display: none; - width: 0; - } - .performance__header { - display: none; - width: 0; - @media (min-width: $break-md) { - display: table-cell; + .data-table { + &__wrapper { + overflow-x: scroll; + } + &__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; - } - .updated_at__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 (max-width: $break-md) { + .name__cell { + .w400 { + max-width: calc(400px - 81px); + } + } + } + .platforms__cell { + max-width: $col-md; } - } - @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; + } } } } @@ -174,6 +179,7 @@ .query-icon { position: relative; top: 2px; + display: block; } } } diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index a728da89a1..95a09c398d 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -10,6 +10,11 @@ import permissionsUtils from "utilities/permissions"; import { IUser } from "interfaces/user"; import { secondsToDhms } from "utilities/helpers"; import { ISchedulableQuery } from "interfaces/schedulable_query"; +import { + SelectedPlatformString, + SupportedPlatform, + SUPPORTED_PLATFORMS, +} from "interfaces/platform"; import Icon from "components/Icon"; import Checkbox from "components/forms/fields/Checkbox"; @@ -19,7 +24,7 @@ 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 StatusIndicator from "components/StatusIndicator"; +import QueryAutomationsStatusIndicator from "../QueryAutomationsStatusIndicator"; interface IQueryRow { id: string; @@ -66,14 +71,15 @@ interface INumberCellProps extends IRowProps { } interface IStringCellProps extends IRowProps { - cell: { - value: string; - }; + cell: { value: string }; } +interface IBoolCellProps extends IRowProps { + cell: { value: boolean }; +} interface IPlatformCellProps extends IRowProps { cell: { - value: string[]; + value: SupportedPlatform[]; }; } @@ -83,7 +89,8 @@ interface IDataColumn { | ((props: ICellProps) => JSX.Element) | ((props: IPlatformCellProps) => JSX.Element) | ((props: IStringCellProps) => JSX.Element) - | ((props: INumberCellProps) => JSX.Element); + | ((props: INumberCellProps) => JSX.Element) + | ((props: IBoolCellProps) => JSX.Element); id?: string; title?: string; accessor?: string; @@ -158,7 +165,19 @@ const generateTableHeaders = ({ disableSortBy: true, accessor: "platforms", Cell: (cellProps: IPlatformCellProps): JSX.Element => { - return ; + // translate the SelectedPlatformString into an array of `SupportedPlatform`s + const selectedPlatforms = cellProps.row.original.platform + .split(",") + .filter((platform) => platform !== "") as SupportedPlatform[]; + + const platformIconsToRender: SupportedPlatform[] = + selectedPlatforms.length === 0 + ? // User didn't select any platforms, so we render all compatible + cellProps.cell.value + : // Render the platforms the user has selected for this query + selectedPlatforms; + + return ; }, }, { @@ -215,31 +234,13 @@ const generateTableHeaders = ({ Header: "Automations", disableSortBy: true, accessor: "automations_enabled", - Cell: (cellProps: IStringCellProps): JSX.Element => { - let status; - if (cellProps.cell.value) { - if (cellProps.row.original.interval === 0) { - status = "paused"; - } else { - status = "on"; - } - } else { - status = "off"; - } - - const tooltip = - status === "paused" - ? { - id: cellProps.row.original.id, - tooltipText: ( - <> - Automations will resume for this query when - a frequency is set. - - ), - } - : undefined; - return ; + Cell: (cellProps: IBoolCellProps): JSX.Element => { + return ( + + ); }, }, { 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/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/utilities/constants.ts b/frontend/utilities/constants.ts index 78a79ede80..fa7dd4c870 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -1,5 +1,5 @@ import URL_PREFIX from "router/url_prefix"; -import { IOsqueryPlatform } from "interfaces/platform"; +import { OsqueryPlatform } from "interfaces/platform"; import paths from "router/paths"; const { origin } = global.window.location; @@ -141,7 +141,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", 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); From 1d6870f0a7efdf9281efcfa9de21fd86102765bf Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Mon, 17 Jul 2023 14:09:59 -0700 Subject: [PATCH 32/78] UI: Update the save query modal with scheduling-related fields (#12741) ## Addresses #12646 ### See issue for list of completed work ![Screenshot 2023-07-12 at 5 41 05 PM](https://github.com/fleetdm/fleet/assets/61553566/b4ece0c9-5df1-4320-9dce-1cd8c2758c6c) ### Also see PR #12713 **notes for review** on that PR for help manually testing this work in lieu of the completed API. - [x] Changes file added for user-visible changes in `changes/` - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- changes/12646-update-save-query-modal | 1 + frontend/interfaces/schedulable_query.ts | 2 +- .../ManageQueriesPage/ManageQueriesPage.tsx | 2 +- .../pages/queries/QueryPage/QueryPage.tsx | 44 ++- .../NewQueryModal/NewQueryModal.tsx | 135 --------- .../components/NewQueryModal/index.ts | 1 - .../components/QueryForm/QueryForm.tsx | 26 +- .../SaveQueryModal/SaveQueryModal.tsx | 259 ++++++++++++++++++ .../components/SaveQueryModal/_styles.scss | 32 +++ .../components/SaveQueryModal/index.ts | 1 + .../queries/QueryPage/screens/QueryEditor.tsx | 18 +- frontend/router/paths.ts | 3 +- frontend/services/entities/queries.ts | 13 +- frontend/utilities/constants.ts | 4 + 14 files changed, 362 insertions(+), 179 deletions(-) create mode 100644 changes/12646-update-save-query-modal delete mode 100644 frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx delete mode 100644 frontend/pages/queries/QueryPage/components/NewQueryModal/index.ts create mode 100644 frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx create mode 100644 frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss create mode 100644 frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts 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/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index aa9bc46013..77475e2c93 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -77,7 +77,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 diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 2278e8b237..117cb6b798 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -150,7 +150,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); diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx index 75f636686a..70294448d1 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tsx @@ -12,7 +12,11 @@ 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 { + ICreateQueryRequestBody, + IGetQueryResponse, + ISchedulableQuery, +} from "interfaces/schedulable_query"; import { ITarget } from "interfaces/target"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; @@ -23,12 +27,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 +44,15 @@ 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 { teamIdForApi: teamIdForQuery } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); const handlePageError = useErrorHandler(); const { @@ -78,13 +91,13 @@ 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); @@ -99,9 +112,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,7 +132,9 @@ const QueryPage = ({ } ); - const { mutateAsync: createQuery } = useMutation((formData: IQueryFormData) => + const { + mutateAsync: createQuery, + } = useMutation((formData: ICreateQueryRequestBody) => queryAPI.create(formData) ); @@ -179,10 +194,11 @@ const QueryPage = ({ const goToQueryEditor = useCallback(() => setStep(QUERIES_PAGE_STEPS[1]), []); const renderScreen = () => { - const step1Opts = { + const step1Props = { router, baseClass, queryIdForEdit: queryId, + teamIdForQuery, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, @@ -194,7 +210,7 @@ const QueryPage = ({ renderLiveQueryWarning, }; - const step2Opts = { + const step2Props = { baseClass, queryId, selectedTargets, @@ -211,7 +227,7 @@ const QueryPage = ({ setTargetsTotalCount, }; - const step3Opts = { + const step3Props = { queryId, selectedTargets, storedQuery, @@ -222,11 +238,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 f3b6edcdb0..e539a26f42 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -26,7 +26,7 @@ import Checkbox from "components/forms/fields/Checkbox"; 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,12 +34,13 @@ const baseClass = "query-form"; interface IQueryFormProps { router: InjectedRouter; queryIdForEdit: number | null; + teamIdForQuery?: number; showOpenSchemaActionText: boolean; storedQuery: IQuery | undefined; isStoredQueryLoading: boolean; isQuerySaving: boolean; isQueryUpdating: boolean; - onCreateQuery: (formData: IQueryFormData) => void; + saveQuery: (formData: IQueryFormData) => void; onOsqueryTableSelect: (tableName: string) => void; goToSelectTargets: () => void; onUpdate: (formData: IQueryFormData) => void; @@ -63,12 +64,13 @@ const validateQuerySQL = (query: string) => { const QueryForm = ({ router, queryIdForEdit, + teamIdForQuery, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, isQuerySaving, isQueryUpdating, - onCreateQuery, + saveQuery, onOsqueryTableSelect, goToSelectTargets, onUpdate, @@ -104,7 +106,7 @@ 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 ); @@ -142,6 +144,10 @@ const QueryForm = ({ storedQuery.author_id === currentUser.id : isAnyTeamMaintainerOrTeamAdmin; + const toggleSaveQueryModal = () => { + setShowSaveQueryModal(!showSaveQueryModal); + }; + const onLoad = (editor: IAceEditor) => { editor.setOptions({ enableLinking: true, @@ -260,7 +266,7 @@ const QueryForm = ({ if (valid) { if (!savedQueryMode) { - setIsSaveModalOpen(true); + setShowSaveQueryModal(true); } else { onUpdate({ name: lastEditedQueryName, @@ -565,12 +571,12 @@ const QueryForm = ({
- {isSaveModalOpen && ( - 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..061fabc381 --- /dev/null +++ b/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx @@ -0,0 +1,259 @@ +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 { IPlatformString } from "interfaces/platform"; +import { + ICreateQueryRequestBody, + ISchedulableQuery, + QueryLoggingOption, +} from "interfaces/schedulable_query"; + +const baseClass = "save-query-modal"; +export interface ISaveQueryModalProps { + queryValue: string; + teamIdForQuery?: 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, + teamIdForQuery, + 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: teamIdForQuery, + }); + } + }; + + 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 IPlatformString + ); + } else if (valArray.length > 1 && valArray.indexOf("") > -1) { + setSelectedPlatformOptions(""); + } else { + setSelectedPlatformOptions(values as IPlatformString); + } + }, + [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..98b500b99d 100644 --- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx +++ b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx @@ -8,6 +8,10 @@ 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,15 +23,15 @@ interface IQueryEditorProps { router: InjectedRouter; baseClass: string; queryIdForEdit: number | null; + teamIdForQuery?: number; storedQuery: IQuery | undefined; storedQueryError: Error | null; showOpenSchemaActionText: boolean; isStoredQueryLoading: boolean; createQuery: UseMutateAsyncFunction< - { query: IQuery }, + ISchedulableQuery, unknown, - IQueryFormData, - unknown + ICreateQueryRequestBody >; onOsqueryTableSelect: (tableName: string) => void; goToSelectTargets: () => void; @@ -39,6 +43,7 @@ const QueryEditor = ({ router, baseClass, queryIdForEdit, + teamIdForQuery, storedQuery, storedQueryError, showOpenSchemaActionText, @@ -77,10 +82,10 @@ 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); + const query = await createQuery(formData); router.push(PATHS.EDIT_QUERY(query)); renderFlash("success", "Query created!"); setBackendValidators({}); @@ -149,12 +154,13 @@ const QueryEditor = ({
+ `${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/queries.ts b/frontend/services/entities/queries.ts index a4a512a2e2..a44f394365 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -4,21 +4,14 @@ import endpoints from "utilities/endpoints"; import { IQueryFormData } from "interfaces/query"; import { ISelectedTargets } from "interfaces/target"; import { AxiosResponse } from "axios"; +import { ICreateQueryRequestBody } 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; diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts index fa7dd4c870..f3b06ec3da 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -23,7 +23,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" }, From 810e76d8501939b69fa7994b9274f3628093974e Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Mon, 17 Jul 2023 14:19:35 -0700 Subject: [PATCH 33/78] update IPlatformString to SelectedPlatformString in SaveQueryModal --- .../QueryPage/components/SaveQueryModal/SaveQueryModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx b/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx index 061fabc381..331484370b 100644 --- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx +++ b/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx @@ -17,7 +17,7 @@ import { SCHEDULE_PLATFORM_DROPDOWN_OPTIONS, } from "utilities/constants"; import RevealButton from "components/buttons/RevealButton"; -import { IPlatformString } from "interfaces/platform"; +import { SelectedPlatformString } from "interfaces/platform"; import { ICreateQueryRequestBody, ISchedulableQuery, @@ -63,7 +63,7 @@ const SaveQueryModal = ({ const [ selectedPlatformOptions, setSelectedPlatformOptions, - ] = useState(existingQuery?.platform ?? ""); + ] = useState(existingQuery?.platform ?? ""); const [ selectedMinOsqueryVersionOptions, setSelectedMinOsqueryVersionOptions, From cc009d2d9f579be162630e9c9f73fa498fde4a82 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Mon, 17 Jul 2023 14:19:35 -0700 Subject: [PATCH 34/78] update IPlatformString to SelectedPlatformString in SaveQueryModal --- .../components/SaveQueryModal/SaveQueryModal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx b/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx index 061fabc381..08d27beb82 100644 --- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx +++ b/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx @@ -17,7 +17,7 @@ import { SCHEDULE_PLATFORM_DROPDOWN_OPTIONS, } from "utilities/constants"; import RevealButton from "components/buttons/RevealButton"; -import { IPlatformString } from "interfaces/platform"; +import { SelectedPlatformString } from "interfaces/platform"; import { ICreateQueryRequestBody, ISchedulableQuery, @@ -63,7 +63,7 @@ const SaveQueryModal = ({ const [ selectedPlatformOptions, setSelectedPlatformOptions, - ] = useState(existingQuery?.platform ?? ""); + ] = useState(existingQuery?.platform ?? ""); const [ selectedMinOsqueryVersionOptions, setSelectedMinOsqueryVersionOptions, @@ -128,12 +128,12 @@ const SaveQueryModal = ({ if (valArray.indexOf("") === 0 && valArray.length > 1) { // TODO - inmprove type safety of all 3 options setSelectedPlatformOptions( - pull(valArray, "").join(",") as IPlatformString + pull(valArray, "").join(",") as SelectedPlatformString ); } else if (valArray.length > 1 && valArray.indexOf("") > -1) { setSelectedPlatformOptions(""); } else { - setSelectedPlatformOptions(values as IPlatformString); + setSelectedPlatformOptions(values as SelectedPlatformString); } }, [setSelectedPlatformOptions] From c392837434ad731b6c621aeda85bfe35b6f0b3b1 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Mon, 17 Jul 2023 14:28:13 -0700 Subject: [PATCH 35/78] handle undefined user-slected platform string --- .../components/QueriesTable/QueriesTableConfig.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 95a09c398d..49b8549b1a 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -166,9 +166,11 @@ const generateTableHeaders = ({ accessor: "platforms", Cell: (cellProps: IPlatformCellProps): JSX.Element => { // translate the SelectedPlatformString into an array of `SupportedPlatform`s - const selectedPlatforms = cellProps.row.original.platform - .split(",") - .filter((platform) => platform !== "") as SupportedPlatform[]; + const selectedPlatforms = + (cellProps.row.original.platform + ?.split(",") + .filter((platform) => platform !== "") as SupportedPlatform[]) ?? + []; const platformIconsToRender: SupportedPlatform[] = selectedPlatforms.length === 0 From 095bf635807b18382a12c99b9624c8dd8ee21f76 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Tue, 18 Jul 2023 10:49:51 -0700 Subject: [PATCH 36/78] =?UTF-8?q?UI=20=E2=80=93=20Queries=20page=20updates?= =?UTF-8?q?=20pt.2=20(#12820)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #12636 – follow-up work to PRs #12713 & #12784 - Fix alignment of the Platforms dropdown Screenshot 2023-07-17 at 7 37 56 PM - Hide redundant table headers for inherited queries table Screenshot 2023-07-17 at 7 39 35 PM - Update SaveQueryModal name field's error state copy (note - [awaiting product decision](https://github.com/fleetdm/fleet/issues/12646#issuecomment-1639159107) on further updates to the UI here - likely for separate ticket): - Avoid redundant processing by moving it outside of useQuery's `select` option. - Leverage query key design to cache global queries for inherited table - [x] Manual QA --------- Co-authored-by: Jacob Shandling --- .../ManageQueriesPage/ManageQueriesPage.tsx | 89 +++++++++++-------- .../queries/ManageQueriesPage/_styles.scss | 11 ++- .../components/QueriesTable/QueriesTable.tsx | 12 ++- .../pages/queries/QueryPage/QueryPage.tsx | 4 +- .../SaveQueryModal/SaveQueryModal.tsx | 30 +++---- .../queries/QueryPage/screens/QueryEditor.tsx | 18 ++-- 6 files changed, 99 insertions(+), 65 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 117cb6b798..b7673c25a3 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -8,6 +8,7 @@ import { TableContext } from "context/table"; import { NotificationContext } from "context/notification"; import { performanceIndicator } from "utilities/helpers"; import { SupportedPlatform } from "interfaces/platform"; +import { API_ALL_TEAMS_ID } from "interfaces/team"; import { IListQueriesResponse, ISchedulableQuery, @@ -112,34 +113,55 @@ const ManageQueriesPage = ({ ); const [showInheritedQueries, setShowInheritedQueries] = useState(false); + interface IQueryKeyQueriesLoadAll { + scope: "enhancedQueries"; + teamId: number | undefined; + } + const { data: curTeamEnhancedQueries, error: curTeamQueriesError, isFetching: isFetchingCurTeamQueries, refetch: refetchCurTeamQueries, - } = useQuery( - [{ scope: "queries", teamId: teamIdForApi }], - () => queriesAPI.loadAll(teamIdForApi), + } = useQuery< + IEnhancedQuery[], + Error, + IEnhancedQuery[], + IQueryKeyQueriesLoadAll[] + >( + [{ scope: "enhancedQueries", teamId: teamIdForApi }], + ({ queryKey: [{ teamId }] }) => + queriesAPI.loadAll(teamId).then(({ queries }) => { + return queries.map(enhanceQuery); + }), { refetchOnWindowFocus: false, enabled: isRouteOk, - select: (data) => data.queries.map(enhanceQuery), + staleTime: 5000, } ); - // If a team is selected, fetch inherited global queries as well + // If a team is selected, inherit global queries const { data: globalEnhancedQueries, error: globalQueriesError, isFetching: isFetchingGlobalQueries, refetch: refetchGlobalQueries, - } = useQuery( - [{ scope: "queries", teamId: -1 }], - () => queriesAPI.loadAll(), + } = useQuery< + IEnhancedQuery[], + Error, + IEnhancedQuery[], + IQueryKeyQueriesLoadAll[] + >( + [{ scope: "enhancedQueries", teamId: API_ALL_TEAMS_ID }], + ({ queryKey: [{ teamId }] }) => + queriesAPI.loadAll(teamId).then(({ queries }) => { + return queries.map(enhanceQuery); + }), { refetchOnWindowFocus: false, enabled: isRouteOk && isAnyTeamSelected, - select: (data) => data.queries.map(enhanceQuery), + staleTime: 5000, } ); @@ -227,19 +249,17 @@ const ManageQueriesPage = ({ return ; } return ( -
- -
+ ); }; @@ -274,19 +294,18 @@ const ManageQueriesPage = ({ return ; } return ( -
- -
+ ); }; diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 43f54b491e..36e9fadfcd 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -59,11 +59,14 @@ gap: $pad-small; } - form-field--dropdown { - margin: 0; - } - .queries-table { + .controls { + .form-field { + &--dropdown { + margin: 0; + } + } + } &__platform-dropdown { width: 159px; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index 9b6bce19cf..13b24a84da 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -41,6 +41,7 @@ interface IQueriesTableProps { order_direction?: "asc" | "desc"; team_id?: string; }; + isInherited?: boolean; } const DEFAULT_SORT_DIRECTION = "desc"; @@ -89,8 +90,9 @@ const QueriesTable = ({ isOnlyObserver, isObserverPlus, isAnyTeamObserverPlus, - queryParams, router, + queryParams, + isInherited = false, }: IQueriesTableProps): JSX.Element | null => { const { currentUser } = useContext(AppContext); @@ -239,11 +241,13 @@ const QueriesTable = ({ [currentUser] ); - const searchable = !(queriesList?.length === 0 && searchQuery === ""); + const searchable = + !(queriesList?.length === 0 && searchQuery === "") && !isInherited; return tableHeaders && !isLoading ? (
{ const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null; - const { teamIdForApi: teamIdForQuery } = useTeamIdParam({ + const { currentTeamSummary: teamForQuery } = useTeamIdParam({ location, router, includeAllTeams: true, @@ -198,7 +198,7 @@ const QueryPage = ({ router, baseClass, queryIdForEdit: queryId, - teamIdForQuery, + teamForQuery, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx b/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx index 08d27beb82..9f1224a91c 100644 --- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx +++ b/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx @@ -166,22 +166,20 @@ const SaveQueryModal = ({ 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. -

-
+ { + 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. +

Date: Tue, 18 Jul 2023 11:15:54 -0700 Subject: [PATCH 37/78] Correct topnav team_id behavior --- frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx | 3 --- frontend/components/top_nav/SiteTopNav/navItems.ts | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) 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 fd466b07d5..4b828087ed 100644 --- a/frontend/components/top_nav/SiteTopNav/navItems.ts +++ b/frontend/components/top_nav/SiteTopNav/navItems.ts @@ -77,6 +77,7 @@ export default ( regex: new RegExp(`^${URL_PREFIX}/queries/`), pathname: PATHS.MANAGE_QUERIES, }, + withParams: { type: "query", names: ["team_id"] }, }, { name: "Policies", From 83c124fb7e41b5424fede2cac781550f2d91c0fe Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Tue, 18 Jul 2023 13:24:01 -0700 Subject: [PATCH 38/78] =?UTF-8?q?"Run=20query"=20=E2=80=93>=20"Live=20quer?= =?UTF-8?q?y"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../queries/QueryPage/components/QueryForm/QueryForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index e539a26f42..072dfdcf89 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -451,7 +451,7 @@ const QueryForm = ({ variant="blue-green" onClick={goToSelectTargets} > - Run query + Live query
)} @@ -567,7 +567,7 @@ const QueryForm = ({ variant="blue-green" onClick={goToSelectTargets} > - Run query + Live query
From a45edfdffa48819b1fb175346908b618cdf5a391 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 18 Jul 2023 16:58:52 -0400 Subject: [PATCH 39/78] Fleet UI: New edit query page (#12777) --- changes/12646-new-query-editor | 1 + .../PlatformCompatibility/_styles.scss | 1 + frontend/context/query.tsx | 64 ++++++ frontend/interfaces/schedulable_query.ts | 7 +- .../QueriesTable/QueriesTableConfig.tsx | 2 +- .../pages/queries/QueryPage/QueryPage.tsx | 26 ++- .../components/QueryForm/QueryForm.tsx | 185 +++++++++++++++--- .../components/QueryForm/_styles.scss | 16 ++ .../queries/QueryPage/screens/QueryEditor.tsx | 16 +- frontend/router/paths.ts | 5 +- frontend/services/entities/queries.ts | 6 +- .../services/mock_service/mocks/config.ts | 1 + .../services/mock_service/mocks/responses.ts | 24 +-- frontend/utilities/constants.ts | 15 +- 14 files changed, 314 insertions(+), 55 deletions(-) create mode 100644 changes/12646-new-query-editor 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/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/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/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index 77475e2c93..7a4ace7cbc 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -67,9 +67,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? diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 49b8549b1a..8749d19e92 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -153,7 +153,7 @@ const generateTableHeaders = ({ )} } - path={PATHS.EDIT_QUERY(cellProps.row.original)} + path={PATHS.EDIT_QUERY(cellProps.row.original.id)} /> ); }, diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx index b89c62b73f..ec5205dfc6 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tsx @@ -70,6 +70,10 @@ const QueryPage = ({ setLastEditedQueryDescription, setLastEditedQueryBody, setLastEditedQueryObserverCanRun, + setLastEditedQueryFrequency, + setLastEditedQueryLoggingType, + setLastEditedQueryMinOsqueryVersion, + setLastEditedQueryPlatforms, } = useContext(QueryContext); const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false); @@ -104,6 +108,10 @@ const QueryPage = ({ 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), } @@ -146,12 +154,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); diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index 072dfdcf89..3496634bdb 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -1,6 +1,12 @@ -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"; @@ -9,9 +15,20 @@ 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,6 +40,8 @@ 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"; @@ -36,14 +55,14 @@ interface IQueryFormProps { queryIdForEdit: number | null; teamIdForQuery?: number; showOpenSchemaActionText: boolean; - storedQuery: IQuery | undefined; + storedQuery: ISchedulableQuery | undefined; isStoredQueryLoading: boolean; isQuerySaving: boolean; isQueryUpdating: boolean; - saveQuery: (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 }; @@ -86,10 +105,18 @@ const QueryForm = ({ lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, + lastEditedQueryFrequency, + lastEditedQueryPlatforms, + lastEditedQueryMinOsqueryVersion, + lastEditedQueryLoggingType, setLastEditedQueryName, setLastEditedQueryDescription, setLastEditedQueryBody, setLastEditedQueryObserverCanRun, + setLastEditedQueryFrequency, + setLastEditedQueryPlatforms, + setLastEditedQueryMinOsqueryVersion, + setLastEditedQueryLoggingType, } = useContext(QueryContext); const { @@ -113,6 +140,7 @@ const QueryForm = ({ 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; @@ -179,6 +207,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 ) => { @@ -205,10 +277,14 @@ const QueryForm = ({ description: lastEditedQueryDescription, query: lastEditedQueryBody, 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 }) => { @@ -219,10 +295,14 @@ const QueryForm = ({ description: lastEditedQueryDescription, query: lastEditedQueryBody, 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}".` @@ -273,6 +353,10 @@ const QueryForm = ({ description: lastEditedQueryDescription, query: lastEditedQueryBody, observer_can_run: lastEditedQueryObserverCanRun, + interval: lastEditedQueryFrequency, + platform: lastEditedQueryPlatforms, + min_osquery_version: lastEditedQueryMinOsqueryVersion, + logging: lastEditedQueryLoggingType, }); } } @@ -488,21 +572,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()}
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/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx index 824ea791b7..46ccdf1c1f 100644 --- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx +++ b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx @@ -7,7 +7,6 @@ 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, @@ -27,7 +26,7 @@ interface IQueryEditorProps { name: string; id: number; }; - storedQuery: IQuery | undefined; + storedQuery: ISchedulableQuery | undefined; storedQueryError: Error | null; showOpenSchemaActionText: boolean; isStoredQueryLoading: boolean; @@ -67,6 +66,10 @@ const QueryEditor = ({ lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, + lastEditedQueryFrequency, + lastEditedQueryLoggingType, + lastEditedQueryPlatforms, + lastEditedQueryMinOsqueryVersion, } = useContext(QueryContext); const [isQuerySaving, setIsQuerySaving] = useState(false); @@ -89,7 +92,7 @@ const QueryEditor = ({ setIsQuerySaving(true); try { const query = await createQuery(formData); - router.push(PATHS.EDIT_QUERY(query)); + router.push(PATHS.EDIT_QUERY(query.id)); renderFlash("success", "Query created!"); setBackendValidators({}); } catch (createError: any) { @@ -106,13 +109,14 @@ const QueryEditor = ({ "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; } @@ -124,6 +128,10 @@ const QueryEditor = ({ lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, + lastEditedQueryFrequency, + lastEditedQueryPlatforms, + lastEditedQueryLoggingType, + lastEditedQueryMinOsqueryVersion, }); try { diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index f2d95f3905..a4505eaea0 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"; @@ -44,8 +43,8 @@ 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): string => { + return `${URL_PREFIX}/queries/${queryId}`; }, EDIT_POLICY: (policy: IPolicy): string => { return `${URL_PREFIX}/policies/${policy.id}${ diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index a44f394365..7e29f5c6fe 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -1,12 +1,14 @@ /* 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 } 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: (createQueryRequestBody: ICreateQueryRequestBody) => { const { QUERIES } = endpoints; @@ -69,7 +71,7 @@ export default { throw new Error(getError(response as AxiosResponse)); } }, - update: (id: number, updateParams: IQueryFormData) => { + update: (id: number, updateParams: ICreateQueryRequestBody) => { 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 d7c7be8ce0..15a3b1a6b4 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -44,6 +44,7 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { name: "New query name", observer_can_run: false, query: "SELECT * FROM osquery_info;", + id: 1, team_id: null, platform: "linux", }, diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index 9695079e90..c36175938e 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -371,11 +371,12 @@ const globalQueries = { created_at: "2022-11-03T17:22:14Z", updated_at: "2022-11-03T17:22:14Z", id: 1, - name: "Test Query", + name: + "Test Query (every hour, 3 platforms, snapshot, no observer run, no min osversion)", description: "A test query", - query: "SELECT * FROM users", + query: "SELECT * FROM users;", team_id: null, - interval: 3600, + interval: 3600, // Every hour platform: "darwin,windows,linux", min_osquery_version: "", automations_enabled: true, @@ -398,13 +399,14 @@ const globalQueries = { created_at: "2022-11-03T17:22:14Z", updated_at: "2022-11-03T17:22:14Z", id: 2, - name: "Test Query 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: 3600, - platform: "linux", - min_osquery_version: "", + interval: 43200, // Every 12 hours + platform: "", + min_osquery_version: "5.8.1", automations_enabled: false, logging: "differential", saved: false, @@ -426,11 +428,11 @@ const globalQueries = { updated_at: "2022-11-03T17:22:14Z", id: 3, name: "Test Query 3", - description: "A third test query", - query: "SELECT * FROM osquery_info", + description: "A third test query (Select all from windows_crashes", + query: "SELECT * FROM windows_crashes", team_id: null, - interval: 3600, - platform: "", + interval: 604800, // Every week + platform: "Windows", min_osquery_version: "", automations_enabled: false, logging: "differential", diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts index f3b06ec3da..06c13429aa 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -1,6 +1,7 @@ import URL_PREFIX from "router/url_prefix"; 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`; @@ -51,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" }, @@ -94,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 = { From e13644d664d0eea072cfe6549ec5bf390cf72b13 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:10:45 -0400 Subject: [PATCH 40/78] Fleet UI: New manage query automations modal (#12747) --- changes/12645-manage-query-automations | 1 + frontend/__mocks__/scheduleableQueryMock.ts | 12 +- .../LogDestinationIndicator.stories.tsx | 17 ++ .../LogDestinationIndicator.tsx | 86 ++++++++ .../LogDestinationIndicator/index.ts | 1 + frontend/components/Modal/Modal.tsx | 1 + .../QueryFrequencyIndicator.stories.tsx | 18 ++ .../QueryFrequencyIndicator.tsx | 73 +++++++ .../QueryFrequencyIndicator/_styles.scss | 13 ++ .../QueryFrequencyIndicator/index.ts | 1 + frontend/components/icons/Clock.tsx | 39 ++++ frontend/components/icons/Warning.tsx | 33 +++ frontend/components/icons/index.ts | 4 + frontend/interfaces/query.ts | 6 +- .../ManageQueriesPage/ManageQueriesPage.tsx | 116 +++++++++-- .../ManageAutomationsModal.tsx | 196 +++++++++++++++++- .../ManageAutomationsModal/_styles.scss | 48 +++++ .../PreviewDataModal/PreviewDataModal.tsx | 61 ++++++ .../components/PreviewDataModal/index.ts | 1 + .../QueriesTable/QueriesTableConfig.tsx | 1 + .../services/mock_service/mocks/config.ts | 6 +- .../services/mock_service/mocks/responses.ts | 124 ++++++++++- frontend/styles/var/colors.scss | 1 + 23 files changed, 826 insertions(+), 33 deletions(-) create mode 100644 changes/12645-manage-query-automations create mode 100644 frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx create mode 100644 frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx create mode 100644 frontend/components/LogDestinationIndicator/index.ts create mode 100644 frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx create mode 100644 frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx create mode 100644 frontend/components/QueryFrequencyIndicator/_styles.scss create mode 100644 frontend/components/QueryFrequencyIndicator/index.ts create mode 100644 frontend/components/icons/Clock.tsx create mode 100644 frontend/components/icons/Warning.tsx create mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss create mode 100644 frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx create mode 100644 frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts 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/frontend/__mocks__/scheduleableQueryMock.ts b/frontend/__mocks__/scheduleableQueryMock.ts index e437edc121..edc82a61da 100644 --- a/frontend/__mocks__/scheduleableQueryMock.ts +++ b/frontend/__mocks__/scheduleableQueryMock.ts @@ -10,7 +10,7 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { description: "A test query", query: "SELECT * FROM users", team_id: null, - interval: 3600, + interval: 43200, // Every 12 hours platform: "darwin,windows,linux", min_osquery_version: "", automations_enabled: true, @@ -22,11 +22,11 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { observer_can_run: false, packs: [], stats: { - system_time_p50: 1, - system_time_p95: 1, - user_time_p50: 1, - user_time_p95: 1, - total_executions: 3, + system_time_p50: 28.1053, + system_time_p95: 397.6667, + user_time_p50: 29.9412, + user_time_p95: 251.4615, + total_executions: 5746, }, }; 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/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..fb6624429f --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/_styles.scss @@ -0,0 +1,13 @@ +.query-frequency-indicator { + width: 100px; + display: flex; + 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/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 b42cfaa152..8f1747db5a 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -50,6 +50,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"; @@ -108,6 +110,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/interfaces/query.ts b/frontend/interfaces/query.ts index 96a8efa4d1..d6a948cd25 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -1,5 +1,6 @@ import { IFormField } from "./form_field"; import { IPack } from "./pack"; +import { ISchedulableQuery } from "./schedulable_query"; import { IScheduledQueryStats } from "./scheduled_query_stats"; export interface IQueryFormData { @@ -7,14 +8,15 @@ export interface IQueryFormData { 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/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index b7673c25a3..9697ca1518 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -26,7 +26,8 @@ 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"; +import ManageAutomationsModal from "./components/ManageAutomationsModal/ManageAutomationsModal"; +import PreviewDataModal from "./components/PreviewDataModal/PreviewDataModal"; const baseClass = "manage-queries-page"; interface IManageQueriesPageProps { @@ -85,6 +86,7 @@ const ManageQueriesPage = ({ filteredQueriesPath, isPremiumTier, isSandboxMode, + config, } = useContext(AppContext); const { setResetSelectedRows } = useContext(TableContext); @@ -107,11 +109,13 @@ const ManageQueriesPage = ({ const [selectedQueryIds, setSelectedQueryIds] = useState([]); const [showDeleteQueryModal, setShowDeleteQueryModal] = useState(false); - const [isUpdatingQueries, setIsUpdatingQueries] = 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); interface IQueryKeyQueriesLoadAll { scope: "enhancedQueries"; @@ -165,6 +169,14 @@ const ManageQueriesPage = ({ } ); + const automatedQueryIds = useMemo(() => { + return curTeamEnhancedQueries + ? curTeamEnhancedQueries + .filter((query) => query.automations_enabled) + .map((query) => query.id) + : []; + }, [curTeamEnhancedQueries]); + useEffect(() => { const path = location.pathname + location.search; if (filteredQueriesPath !== path) { @@ -178,10 +190,6 @@ const ManageQueriesPage = ({ setShowDeleteQueryModal(!showDeleteQueryModal); }, [showDeleteQueryModal, setShowDeleteQueryModal]); - const toggleManageAutomationsModal = useCallback(() => { - setShowManageAutomationsModal(!showManageAutomationsModal); - }, [showManageAutomationsModal, setShowManageAutomationsModal]); - const onDeleteQueryClick = (selectedTableQueryIds: number[]) => { toggleDeleteQueryModal(); setSelectedQueryIds(selectedTableQueryIds); @@ -192,6 +200,25 @@ const ManageQueriesPage = ({ 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); @@ -318,6 +345,52 @@ const ManageQueriesPage = ({ ); }; + 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 = []; + updateAutomatedQueries.push( + turnOnAutomations.map((id: number) => + queriesAPI.update(id, { automations_enabled: true }) + ) + ); + updateAutomatedQueries.push( + turnOffAutomations.map((id: number) => + 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 ( <> @@ -329,7 +402,18 @@ const ManageQueriesPage = ({ /> )} {showManageAutomationsModal && ( - + + )} + {showPreviewDataModal && ( + )} ); @@ -347,7 +431,7 @@ const ManageQueriesPage = ({
{(isGlobalAdmin || isTeamAdmin) && ( + <> + + )}
diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index cb6abd9cb8..b5cfea5899 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -1,19 +1,203 @@ -import React from "react"; +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"; -const baseClass = "automations-modal"; +import { ISchedulableQuery } from "interfaces/schedulable_query"; +interface IFrequencyIndicator { + frequency: number; + checked: boolean; +} interface IManageAutomationsModalProps { - onExit: () => void; + 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 = ({ - onExit, + isUpdatingAutomations, + automatedQueryIds, + handleSubmit, + onCancel, + togglePreviewDataModal, + availableQueries, + logDestination, }: IManageAutomationsModalProps): JSX.Element => { + // TODO: Error handling, if any + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + + const { queryItems, updateQueryItems } = useCheckboxListStateManagement( + availableQueries || [], + 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?   + +
+
+
+ +
+
+ + +
+
+
); }; 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..296b36f7aa --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss @@ -0,0 +1,48 @@ +.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; + } + } + + .fleet-checkbox { + height: 20px; + + &__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/PreviewDataModal/PreviewDataModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx new file mode 100644 index 0000000000..3156c73288 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx @@ -0,0 +1,61 @@ +/* This component is used for creating and editing both global and team scheduled queries */ + +import React from "react"; +import { syntaxHighlight } from "utilities/helpers"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; +import TooltipWrapper from "components/TooltipWrapper"; + +const baseClass = "preview-data-modal"; + +interface IPreviewDataModalProps { + onCancel: () => void; +} + +const PreviewDataModal = ({ + onCancel, +}: IPreviewDataModalProps): JSX.Element => { + const json = { + action: "snapshot", + snapshot: [ + { + remote_address: "0.0.0.0", + remote_port: "0", + cmdline: "/usr/sbin/syslogd", + }, + ], + name: "xxxxxxx", + hostIdentifier: "xxxxxxx", + calendarTime: "xxx xxx x xx:xx:xx xxxx UTC", + unixTime: "xxxxxxxxx", + epoch: "xxxxxxxxx", + counter: "x", + numerics: "x", + }; + + return ( + +
+

+ + The data sent to your configured log destination will look similar + to the following JSON: + +

+
+
+        
+
+ +
+
+
+ ); +}; + +export default PreviewDataModal; diff --git a/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts new file mode 100644 index 0000000000..48fca40136 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts @@ -0,0 +1 @@ +export { default } from "./PreviewDataModal"; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 8749d19e92..444edc55ae 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -205,6 +205,7 @@ const generateTableHeaders = ({ }, }, { + title: "Performance impact", Header: () => { return (
diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index 15a3b1a6b4..50ebcbf1f9 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -28,7 +28,11 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { "queries/2": RESPONSES.globalQuery2, "queries/3": RESPONSES.globalQuery3, "queries/4": RESPONSES.teamQuery1, - "queries?team_id=43": RESPONSES.teamQueries, + "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 diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index c36175938e..31edff807e 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -431,6 +431,60 @@ const globalQueries = { 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: "", @@ -450,6 +504,33 @@ const globalQueries = { 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, + }, + }, ], }; @@ -459,6 +540,35 @@ const teamQueries = { 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;", @@ -476,14 +586,14 @@ const teamQueries = { author_email: "jacob@fleetdm.com", packs: [], stats: { - system_time_p50: 1, + system_time_p50: 4, // system_time_p95: null, - user_time_p50: 1, + user_time_p50: 10, // user_time_p95: null, total_executions: 1, }, performance: "Undetermined", - platforms: ["windows", "darwin", "linux"], + platforms: ["darwin"], }, ], }; @@ -491,7 +601,11 @@ const teamQueries = { 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, @@ -501,6 +615,10 @@ export default { 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; From 27a1bfd805c4d8b327e47788b5ae33bd6aea545e Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Wed, 19 Jul 2023 11:02:53 -0700 Subject: [PATCH 41/78] UI - Restore host details Query action and Schedule tab (#12832) ## Addresses a conversation in which we decided _not_ to remove the host details page's Query action and Schedule tab after all. @zhumo @rachaelshaw, related to #12636 ### Restores these features Screenshot 2023-07-18 at 4 17 41 PM - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- .../DataTable/PillCell/PillCell.tsx | 9 +- frontend/interfaces/schedulable_query.ts | 4 + .../HostActionsDropdown/helpers.tsx | 9 +- .../HostDetailsPage/HostDetailsPage.tsx | 89 +++++++++++++ .../SelectQueryModal/SelectQueryModal.tsx | 6 +- .../hosts/details/cards/Schedule/Schedule.tsx | 80 ++++++++++++ .../cards/Schedule/ScheduleTableConfig.tsx | 123 ++++++++++++++++++ .../hosts/details/cards/Schedule/_styles.scss | 29 +++++ .../hosts/details/cards/Schedule/index.ts | 1 + .../ManageQueriesPage/ManageQueriesPage.tsx | 19 +-- frontend/router/index.tsx | 1 + frontend/services/entities/queries.ts | 7 +- 12 files changed, 360 insertions(+), 17 deletions(-) create mode 100644 frontend/pages/hosts/details/cards/Schedule/Schedule.tsx create mode 100644 frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx create mode 100644 frontend/pages/hosts/details/cards/Schedule/_styles.scss create mode 100644 frontend/pages/hosts/details/cards/Schedule/index.ts diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx index 525ce31d87..8fc83621ca 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx @@ -7,13 +7,18 @@ import ReactTooltip from "react-tooltip"; interface IPillCellProps { value: { indicator: string; id: number }; customIdPrefix?: string; + hostDetails?: boolean; } const generateClassTag = (rawValue: string): string => { return rawValue.replace(" ", "-").toLowerCase(); }; -const PillCell = ({ value, customIdPrefix }: IPillCellProps): JSX.Element => { +const PillCell = ({ + value, + customIdPrefix, + hostDetails, +}: IPillCellProps): JSX.Element => { const { indicator, id } = value; const pillClassName = classnames( "data-table__pill", @@ -71,7 +76,7 @@ const PillCell = ({ value, customIdPrefix }: IPillCellProps): JSX.Element => { return ( <> To see performance impact, this query must have run with{" "} - automations on at least one host. + automations on {hostDetails ? "this" : "at least one"} host. ); default: diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index 7a4ace7cbc..6568689711 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -46,6 +46,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 { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index a03079f7f3..82eede9937 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -10,6 +10,11 @@ const DEFAULT_OPTIONS: IDropdownOption[] = [ disabled: false, premiumOnly: true, }, + { + label: "Query", + value: "query", + disabled: false, + }, { label: "Show disk encryption key", value: "diskEncryption", @@ -117,7 +122,9 @@ const setOptionsAsDisabled = ( let optionsToDisable: IDropdownOption[] = []; if (!isHostOnline) { optionsToDisable = optionsToDisable.concat( - options.filter((option) => option.value === "mdmOff") + options.filter( + (option) => option.value === "query" || option.value === "mdmOff" + ) ); } if (isSandboxMode) { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 820915caa2..a982fac447 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -9,6 +9,7 @@ import { pick } from "lodash"; import PATHS from "router/paths"; import hostAPI from "services/entities/hosts"; +import queryAPI from "services/entities/queries"; import teamAPI, { ILoadTeamsResponse } from "services/entities/teams"; import { AppContext } from "context/app"; import { PolicyContext } from "context/policy"; @@ -19,11 +20,18 @@ import { IMacadminsResponse, IHostResponse, IHostMdmData, + IPackStats, } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { IHostPolicy } from "interfaces/policy"; import { ISoftware } from "interfaces/software"; import { ITeam } from "interfaces/team"; +import { + IListQueriesResponse, + IQueryKeyQueriesLoadAll, + ISchedulableQuery, +} from "interfaces/schedulable_query"; +import { IQueryStats } from "interfaces/query_stats"; import Spinner from "components/Spinner"; import TabsWrapper from "components/TabsWrapper"; @@ -41,6 +49,7 @@ import MunkiIssuesCard from "../cards/MunkiIssues"; import SoftwareCard from "../cards/Software"; import UsersCard from "../cards/Users"; import PoliciesCard from "../cards/Policies"; +import ScheduleCard from "../cards/Schedule"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; import OSPolicyModal from "./modals/OSPolicyModal"; import UnenrollMdmModal from "./modals/UnenrollMdmModal"; @@ -53,6 +62,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"; @@ -87,6 +97,12 @@ interface IHostDetailsSubNavItem { pathname: string; } +const TAGGED_TEMPLATES = { + queryByHostRoute: (hostId: number | undefined | null) => { + return `${hostId ? `?host_ids=${hostId}` : ""}`; + }, +}; + const HostDetailsPage = ({ route, router, @@ -119,6 +135,7 @@ const HostDetailsPage = ({ const [showDeleteHostModal, setShowDeleteHostModal] = useState(false); const [showTransferHostModal, setShowTransferHostModal] = useState(false); + const [showSelectQueryModal, setShowSelectQueryModal] = useState(false); const [showPolicyDetailsModal, setPolicyDetailsModal] = useState(false); const [showOSPolicyModal, setShowOSPolicyModal] = useState(false); const [showMacSettingsModal, setShowMacSettingsModal] = useState(false); @@ -134,11 +151,26 @@ const HostDetailsPage = ({ const [refetchStartTime, setRefetchStartTime] = useState(null); const [showRefetchSpinner, setShowRefetchSpinner] = useState(false); + const [schedule, setSchedule] = 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< + IListQueriesResponse, + Error, + ISchedulableQuery[], + IQueryKeyQueriesLoadAll[] + >([{ scope: "queries", teamId: undefined }], () => queryAPI.loadAll(), { + enabled: !!hostIdFromURL, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + retry: false, + select: (data: IListQueriesResponse) => data.queries, + }); + const { data: teams } = useQuery( "teams", () => teamAPI.loadAll(), @@ -262,6 +294,26 @@ const HostDetailsPage = ({ } setHostSoftware(returnedHost.software || []); setUsersState(returnedHost.users || []); + if (returnedHost.pack_stats) { + const packStatsByType = returnedHost.pack_stats.reduce( + ( + dictionary: { + packs: IPackStats[]; + schedule: IQueryStats[]; + }, + pack: IPackStats + ) => { + if (pack.type === "pack") { + dictionary.packs.push(pack); + } else { + dictionary.schedule.push(...pack.query_stats); + } + return dictionary; + }, + { packs: [], schedule: [] } + ); + setSchedule(packStatsByType.schedule); + } }, onError: (error) => handlePageError(error), } @@ -427,6 +479,17 @@ const HostDetailsPage = ({ : router.push(PATHS.MANAGE_HOSTS_LABEL(label.id)); }; + const onQueryHostCustom = () => { + router.push(PATHS.NEW_QUERY + TAGGED_TEMPLATES.queryByHostRoute(host?.id)); + }; + + const onQueryHostSaved = (selectedQuery: ISchedulableQuery) => { + router.push( + PATHS.EDIT_QUERY(selectedQuery.id) + + TAGGED_TEMPLATES.queryByHostRoute(host?.id) + ); + }; + const onTransferHostSubmit = async (team: ITeam) => { setIsUpdatingHost(true); @@ -464,6 +527,9 @@ const HostDetailsPage = ({ case "transfer": setShowTransferHostModal(true); break; + case "query": + setShowSelectQueryModal(true); + break; case "diskEncryption": setShowDiskEncryptionModal(true); break; @@ -509,6 +575,11 @@ const HostDetailsPage = ({ title: "software", pathname: PATHS.HOST_SOFTWARE(hostIdFromURL), }, + { + name: "Schedule", + title: "schedule", + pathname: PATHS.HOST_SCHEDULE(hostIdFromURL), + }, { name: ( <> @@ -649,6 +720,13 @@ const HostDetailsPage = ({ /> )} + + + )} + {showSelectQueryModal && host && ( + setShowSelectQueryModal(false)} + queries={fleetQueries || []} + queryErrors={fleetQueriesError} + isOnlyObserver={isOnlyObserver} + onQueryHostCustom={onQueryHostCustom} + onQueryHostSaved={onQueryHostSaved} + hostsTeamId={host?.team_id} + /> + )} {!!host && showTransferHostModal && ( setShowTransferHostModal(false)} diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx index 606d67c6c5..ce8fb2c0da 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 InputField from "components/forms/fields/InputField"; 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/Schedule/Schedule.tsx b/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx new file mode 100644 index 0000000000..1c37493510 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx @@ -0,0 +1,80 @@ +import React from "react"; + +import { IQueryStats } from "interfaces/query_stats"; +import TableContainer from "components/TableContainer"; +import EmptyTable from "components/EmptyTable"; +import CustomLink from "components/CustomLink"; + +import { generateTableHeaders, generateDataSet } from "./ScheduleTableConfig"; + +const baseClass = "schedule"; + +interface IScheduleProps { + schedule?: IQueryStats[]; + isChromeOSHost: boolean; + isLoading: boolean; +} + +const Schedule = ({ + schedule, + isChromeOSHost, + isLoading, +}: IScheduleProps): JSX.Element => { + const wrapperClassName = `${baseClass}__pack-table`; + const tableHeaders = generateTableHeaders(); + + const renderEmptyScheduleTab = () => { + if (isChromeOSHost) { + return ( + + Interested in collecting data from your Chromebooks? + + + } + /> + ); + } + return ( + + ); + }; + + return ( +
+

Schedule

+ {!schedule || !schedule.length || isChromeOSHost ? ( + renderEmptyScheduleTab() + ) : ( +
+ null} + resultsTitle={"queries"} + defaultSortHeader={"scheduled_query_name"} + defaultSortDirection={"asc"} + showMarkAllPages={false} + isAllPagesSelected={false} + emptyComponent={() => <>} + disablePagination + disableCount + /> +
+ )} +
+ ); +}; + +export default Schedule; diff --git a/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx b/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx new file mode 100644 index 0000000000..c02374741b --- /dev/null +++ b/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx @@ -0,0 +1,123 @@ +import React from "react"; + +import { IQueryStats } from "interfaces/query_stats"; +import { performanceIndicator, secondsToDhms } from "utilities/helpers"; + +import TextCell from "components/TableContainer/DataTable/TextCell"; +import PillCell from "components/TableContainer/DataTable/PillCell"; +import TooltipWrapper from "components/TooltipWrapper"; + +interface IHeaderProps { + column: { + title: string; + isSortedDesc: boolean; + }; +} + +interface IRowProps { + row: { + original: IQueryStats; + }; +} + +interface ICellProps extends IRowProps { + cell: { + value: string | number | boolean; + }; +} + +interface IPillCellProps extends IRowProps { + cell: { + value: { + indicator: string; + id: number; + }; + }; +} + +interface IDataColumn { + title?: string; + Header: ((props: IHeaderProps) => JSX.Element) | string; + accessor: string; + Cell: + | ((props: ICellProps) => JSX.Element) + | ((props: IPillCellProps) => JSX.Element); + disableHidden?: boolean; + disableSortBy?: boolean; +} + +interface IScheduleTable extends Partial { + frequency: string; + performance: { indicator: string; id: number }; +} + +// NOTE: cellProps come from react-table +// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties +const generateTableHeaders = (): IDataColumn[] => { + return [ + { + title: "Query", + Header: "Query", + disableSortBy: true, + accessor: "query_name", + Cell: (cellProps: ICellProps) => ( + + ), + }, + { + title: "Frequency", + Header: "Frequency", + disableSortBy: true, + accessor: "frequency", + Cell: (cellProps: ICellProps) => ( + + ), + }, + { + Header: () => { + return ( + + Performance impact + + ); + }, + disableSortBy: true, + accessor: "performance", + Cell: (cellProps: IPillCellProps) => ( + + ), + }, + ]; +}; + +const enhanceScheduleData = (query_stats: IQueryStats[]): IScheduleTable[] => { + return Object.values(query_stats).map((query) => { + const scheduledQueryPerformance = { + user_time_p50: query.user_time, + system_time_p50: query.system_time, + total_executions: query.executions, + }; + return { + query_name: query.query_name, + frequency: secondsToDhms(query.interval), + performance: { + indicator: performanceIndicator(scheduledQueryPerformance), + id: query.scheduled_query_id, + }, + }; + }); +}; + +const generateDataSet = (query_stats: IQueryStats[]): IScheduleTable[] => { + if (!query_stats) { + return query_stats; + } + + return [...enhanceScheduleData(query_stats)]; +}; + +export { generateTableHeaders, generateDataSet }; diff --git a/frontend/pages/hosts/details/cards/Schedule/_styles.scss b/frontend/pages/hosts/details/cards/Schedule/_styles.scss new file mode 100644 index 0000000000..4841674726 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Schedule/_styles.scss @@ -0,0 +1,29 @@ +.section--schedule { + margin-top: $pad-medium; + .section__header { + margin-bottom: $pad-medium; + } + .table-container__header { + display: none; + } + .data-table-block { + .data-table__table { + thead { + .query_name__header { + width: $col-lg; + } + .frequency__header { + width: $col-md; + } + } + tbody { + .query_name__cell { + width: $col-lg; + } + .frequency__cell { + width: $col-md; + } + } + } + } +} diff --git a/frontend/pages/hosts/details/cards/Schedule/index.ts b/frontend/pages/hosts/details/cards/Schedule/index.ts new file mode 100644 index 0000000000..39250f5640 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Schedule/index.ts @@ -0,0 +1 @@ +export { default } from "./Schedule"; diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 9697ca1518..1884828cd8 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -1,4 +1,10 @@ -import React, { useContext, useCallback, useEffect, useState } from "react"; +import React, { + useContext, + useCallback, + useEffect, + useState, + useMemo, +} from "react"; import { InjectedRouter } from "react-router"; import { useQuery } from "react-query"; import { pick } from "lodash"; @@ -10,7 +16,7 @@ import { performanceIndicator } from "utilities/helpers"; import { SupportedPlatform } from "interfaces/platform"; import { API_ALL_TEAMS_ID } from "interfaces/team"; import { - IListQueriesResponse, + IQueryKeyQueriesLoadAll, ISchedulableQuery, } from "interfaces/schedulable_query"; import queriesAPI from "services/entities/queries"; @@ -117,11 +123,6 @@ const ManageQueriesPage = ({ const [showInheritedQueries, setShowInheritedQueries] = useState(false); const [isUpdatingAutomations, setIsUpdatingAutomations] = useState(false); - interface IQueryKeyQueriesLoadAll { - scope: "enhancedQueries"; - teamId: number | undefined; - } - const { data: curTeamEnhancedQueries, error: curTeamQueriesError, @@ -133,7 +134,7 @@ const ManageQueriesPage = ({ IEnhancedQuery[], IQueryKeyQueriesLoadAll[] >( - [{ scope: "enhancedQueries", teamId: teamIdForApi }], + [{ scope: "queries", teamId: teamIdForApi }], ({ queryKey: [{ teamId }] }) => queriesAPI.loadAll(teamId).then(({ queries }) => { return queries.map(enhanceQuery); @@ -157,7 +158,7 @@ const ManageQueriesPage = ({ IEnhancedQuery[], IQueryKeyQueriesLoadAll[] >( - [{ scope: "enhancedQueries", teamId: API_ALL_TEAMS_ID }], + [{ scope: "queries", teamId: API_ALL_TEAMS_ID }], ({ queryKey: [{ teamId }] }) => queriesAPI.loadAll(teamId).then(({ queries }) => { return queries.map(enhanceQuery); diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index a4ef257c22..e4319e8a4b 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -169,6 +169,7 @@ const routes = ( + diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 7e29f5c6fe..89765f6481 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -3,7 +3,10 @@ import sendRequest, { getError } from "services"; import endpoints from "utilities/endpoints"; import { ISelectedTargets } from "interfaces/target"; import { AxiosResponse } from "axios"; -import { ICreateQueryRequestBody } from "interfaces/schedulable_query"; +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 @@ -71,7 +74,7 @@ export default { throw new Error(getError(response as AxiosResponse)); } }, - update: (id: number, updateParams: ICreateQueryRequestBody) => { + update: (id: number, updateParams: IModifyQueryRequestBody) => { const { QUERIES } = endpoints; const path = `${QUERIES}/${id}`; From b0c1dba44c8a8a64c593758a27b11f2aba83ec80 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 20 Jul 2023 08:06:43 -0400 Subject: [PATCH 42/78] Updated cache strategy on queries used in GetClientConfig (#12815) 1. Cached results of `svc.ds.Team` 2. Cached results of `svc.ds.ListQueries` too for scheduled queries only. 3. Do not load aggregated stats on `svc.ds.ListQueries` insde `GetClientConfig` --- server/datastore/cached_mysql/cached_mysql.go | 43 +++++++++ .../cached_mysql/cached_mysql_test.go | 89 +++++++++++++++++++ server/datastore/mysql/hosts_test.go | 33 +------ server/datastore/mysql/queries.go | 36 +++++++- server/datastore/mysql/queries_test.go | 65 +++++++++++++- server/datastore/mysql/teams.go | 14 +++ server/datastore/mysql/teams_test.go | 22 +++++ server/fleet/datastore.go | 5 ++ server/mock/datastore_mock.go | 24 +++++ server/service/osquery.go | 9 +- server/service/osquery_test.go | 20 +++-- 11 files changed, 312 insertions(+), 48 deletions(-) diff --git a/server/datastore/cached_mysql/cached_mysql.go b/server/datastore/cached_mysql/cached_mysql.go index 850646352f..fca6d3ed5d 100644 --- a/server/datastore/cached_mysql/cached_mysql.go +++ b/server/datastore/cached_mysql/cached_mysql.go @@ -25,6 +25,8 @@ const ( defaultTeamFeaturesExpiration = 1 * time.Minute teamMDMConfigKey = "TeamMDMConfig:team:%d" defaultTeamMDMConfigExpiration = 1 * time.Minute + teamNameByIdKey = "TeamName:team:%d" + scheduledQueriesForAgentsKey = "ScheduledQueriesAgents:team:%d" ) // cloner represents any type that can clone itself. Used by types to provide a more efficient clone method. @@ -296,10 +298,12 @@ func (ds *cachedMysql) SaveTeam(ctx context.Context, team *fleet.Team) (*fleet.T agentOptionsKey := fmt.Sprintf(teamAgentOptionsKey, team.ID) featuresKey := fmt.Sprintf(teamFeaturesKey, team.ID) mdmConfigKey := fmt.Sprintf(teamMDMConfigKey, team.ID) + teamNameKey := fmt.Sprintf(teamNameByIdKey, team.ID) ds.c.Set(agentOptionsKey, team.Config.AgentOptions, ds.teamAgentOptionsExp) ds.c.Set(featuresKey, &team.Config.Features, ds.teamFeaturesExp) ds.c.Set(mdmConfigKey, &team.Config.MDM, ds.teamMDMConfigExp) + ds.c.Set(teamNameKey, &team.Name, ds.scheduledQueriesExp) return team, nil } @@ -313,10 +317,49 @@ func (ds *cachedMysql) DeleteTeam(ctx context.Context, teamID uint) error { agentOptionsKey := fmt.Sprintf(teamAgentOptionsKey, teamID) featuresKey := fmt.Sprintf(teamFeaturesKey, teamID) mdmConfigKey := fmt.Sprintf(teamMDMConfigKey, teamID) + teamNameKey := fmt.Sprintf(teamNameByIdKey, teamID) ds.c.Delete(agentOptionsKey) ds.c.Delete(featuresKey) ds.c.Delete(mdmConfigKey) + ds.c.Delete(teamNameKey) return nil } + +func (ds *cachedMysql) GetTeamName(ctx context.Context, teamID uint) (*string, error) { + key := fmt.Sprintf(teamNameByIdKey, teamID) + if x, found := ds.c.Get(key); found { + if teamName, ok := x.(*string); ok { + return teamName, nil + } + } + + teamName, err := ds.Datastore.GetTeamName(ctx, teamID) + if err != nil { + return nil, err + } + ds.c.Set(key, teamName, ds.scheduledQueriesExp) + return teamName, nil +} + +func (ds *cachedMysql) ListScheduledQueriesForAgents(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { + var teamIDVal uint + if teamID != nil { + teamIDVal = *teamID + } + + key := fmt.Sprintf(scheduledQueriesForAgentsKey, teamIDVal) + if x, found := ds.c.Get(key); found { + if queries, ok := x.([]*fleet.Query); ok { + return queries, nil + } + } + + queries, err := ds.Datastore.ListScheduledQueriesForAgents(ctx, teamID) + if err != nil { + return nil, err + } + ds.c.Set(key, queries, ds.scheduledQueriesExp) + return queries, nil +} diff --git a/server/datastore/cached_mysql/cached_mysql_test.go b/server/datastore/cached_mysql/cached_mysql_test.go index 0417987931..fcbde32d22 100644 --- a/server/datastore/cached_mysql/cached_mysql_test.go +++ b/server/datastore/cached_mysql/cached_mysql_test.go @@ -12,6 +12,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -537,3 +538,91 @@ func TestCachedTeamMDMConfig(t *testing.T) { _, err = ds.TeamMDMConfig(context.Background(), testTeam.ID) require.Error(t, err) } + +func TestCachedGetTeamName(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + mockedDS := new(mock.Store) + ds := New(mockedDS, WithScheduledQueriesExpiration(100*time.Millisecond)) + + team := fleet.Team{ + ID: 1, + CreatedAt: time.Now(), + Name: "test", + } + + deleted := false + mockedDS.GetTeamNameFunc = func(ctx context.Context, teamID uint) (*string, error) { + if deleted { + return nil, errors.New("not found") + } + return &team.Name, nil + } + mockedDS.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) { + return team, nil + } + mockedDS.DeleteTeamFunc = func(ctx context.Context, teamID uint) error { + deleted = true + return nil + } + + // updating updates the cache + result, err := ds.GetTeamName(ctx, 1) + require.NoError(t, err) + require.Equal(t, team.Name, *result) + + updatedTeam := &fleet.Team{ + ID: team.ID, + CreatedAt: team.CreatedAt, + Name: "test II", + } + _, err = ds.SaveTeam(ctx, updatedTeam) + require.NoError(t, err) + + result, err = ds.GetTeamName(ctx, team.ID) + require.NoError(t, err) + require.Equal(t, updatedTeam.Name, *result) + + // deleting updates the cache + err = ds.DeleteTeam(ctx, team.ID) + require.NoError(t, err) + + _, err = ds.GetTeamName(ctx, team.ID) + require.Error(t, err) +} + +func TestCachedListScheduledQueriesForAgents(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + mockedDS := new(mock.Store) + ds := New(mockedDS, WithScheduledQueriesExpiration(100*time.Millisecond)) + + teamID := ptr.Uint(1) + scheduledQueries := []*fleet.Query{ + { + ID: 1, + Name: "test", + ScheduleInterval: 100, + AutomationsEnabled: true, + TeamID: teamID, + }, + { + ID: 2, + Name: "test II", + ScheduleInterval: 100, + AutomationsEnabled: true, + TeamID: teamID, + }, + } + mockedDS.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { + return scheduledQueries, nil + } + + result, err := ds.ListScheduledQueriesForAgents(ctx, teamID) + require.NoError(t, err) + test.QueryElementsMatch(t, result, scheduledQueries) +} diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 3f84b3300d..ef2b7b3838 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -564,10 +564,6 @@ 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, nil, "tp-time", "select * from time", 0, true) - tpSquery := test.NewScheduledQuery(t, ds, tp.ID, tpQuery.ID, 30, true, true, "time-scheduled") // Create a new pack and target to the host. // Pack and query must exist for stats to save successfully @@ -596,28 +592,8 @@ func testHostsWithTeamPackStats(t *testing.T, ds *Datastore) { 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, - }, - } - packStats := []fleet.PackStats{ {PackID: pack1.ID, PackName: pack1.Name, QueryStats: stats1}, - {PackID: tp.ID, PackName: teamScheduleName(team), QueryStats: stats2}, } err = ds.SaveHostPackStats(context.Background(), host.ID, packStats) require.NoError(t, err) @@ -625,14 +601,11 @@ func testHostsWithTeamPackStats(t *testing.T, ds *Datastore) { host, err = ds.Host(context.Background(), host.ID) require.NoError(t, err) - require.Len(t, host.PackStats, 2) + require.Len(t, host.PackStats, 1) sort.Sort(packStatsSlice(host.PackStats)) - assert.Equal(t, host.PackStats[0].PackName, teamScheduleName(team)) - assert.ElementsMatch(t, host.PackStats[0].QueryStats, stats2) - - assert.Equal(t, host.PackStats[1].PackName, pack1.Name) - assert.ElementsMatch(t, host.PackStats[1].QueryStats, stats1) + assert.Equal(t, host.PackStats[0].PackName, pack1.Name) + assert.ElementsMatch(t, host.PackStats[0].QueryStats, stats1) } type packStatsSlice []fleet.PackStats diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index 0a8af019b0..f1b9c2b48b 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -353,10 +353,10 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions 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, aggregatedStatsTypeQuery} - whereClauses := "" + whereClauses := "WHERE saved = true" if opt.OnlyObserverCanRun { whereClauses += " AND q.observer_can_run=true" @@ -453,3 +453,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 2d82321357..3dfe9028b7 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -30,8 +30,9 @@ func TestQueries(t *testing.T) { {"DuplicateNew", testQueriesDuplicateNew}, {"ListFiltersObservers", testQueriesListFiltersObservers}, {"ObserverCanRunQuery", testObserverCanRunQuery}, - {"ListFiltersByTeamID", testQueriesListFiltersByTeamID}, - {"ListFiltersByIsScheduled", testQueriesListFiltersByIsScheduled}, + {"ListQueriesFiltersByTeamID", testListQueriesFiltersByTeamID}, + {"ListQueriesFiltersByIsScheduled", testListQueriesFiltersByIsScheduled}, + {"ListScheduledQueriesForAgents", testListScheduledQueriesForAgents}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -555,7 +556,7 @@ func testObserverCanRunQuery(t *testing.T, ds *Datastore) { } } -func testQueriesListFiltersByTeamID(t *testing.T, ds *Datastore) { +func testListQueriesFiltersByTeamID(t *testing.T, ds *Datastore) { globalQ1, err := ds.NewQuery(context.Background(), &fleet.Query{ Name: "query1", Query: "select 1;", @@ -617,7 +618,7 @@ func testQueriesListFiltersByTeamID(t *testing.T, ds *Datastore) { test.QueryElementsMatch(t, queries, []*fleet.Query{teamQ1, teamQ2, teamQ3}) } -func testQueriesListFiltersByIsScheduled(t *testing.T, ds *Datastore) { +func testListQueriesFiltersByIsScheduled(t *testing.T, ds *Datastore) { q1, err := ds.NewQuery(context.Background(), &fleet.Query{ Name: "query1", Query: "select 1;", @@ -669,3 +670,59 @@ func testQueriesListFiltersByIsScheduled(t *testing.T, ds *Datastore) { 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, + ScheduleInterval: 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, + ScheduleInterval: 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, + ScheduleInterval: 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, + ScheduleInterval: 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/teams.go b/server/datastore/mysql/teams.go index 8aee558444..f1dd45448d 100644 --- a/server/datastore/mysql/teams.go +++ b/server/datastore/mysql/teams.go @@ -428,3 +428,17 @@ func (ds *Datastore) DeleteIntegrationsFromTeams(ctx context.Context, deletedInt } return rows.Err() } + +func (ds *Datastore) GetTeamName(ctx context.Context, teamID uint) (*string, error) { + stmt := `SELECT name FROM teams WHERE id = ?` + var teamName string + + if err := sqlx.GetContext(ctx, ds.reader(ctx), &teamName, stmt, teamID); err != nil { + if err == sql.ErrNoRows { + return nil, ctxerr.Wrap(ctx, notFound("Team").WithID(teamID)) + } + return nil, ctxerr.Wrap(ctx, err, "select team") + } + + return &teamName, nil +} diff --git a/server/datastore/mysql/teams_test.go b/server/datastore/mysql/teams_test.go index 7b46c80b51..858fa65acb 100644 --- a/server/datastore/mysql/teams_test.go +++ b/server/datastore/mysql/teams_test.go @@ -35,6 +35,7 @@ func TestTeams(t *testing.T) { {"DeleteIntegrationsFromTeams", testTeamsDeleteIntegrationsFromTeams}, {"TeamsFeatures", testTeamsFeatures}, {"TeamsMDMConfig", testTeamsMDMConfig}, + {"GetTeamByName", testGetTeamByName}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -624,3 +625,24 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) { }, mdm) }) } + +func testGetTeamByName(t *testing.T, ds *Datastore) { + ctx := context.Background() + + t.Run("team does not exists", func(t *testing.T) { + r, err := ds.GetTeamName(ctx, 123) + require.Nil(t, r) + require.Error(t, err) + }) + + t.Run("returns the team name", func(t *testing.T) { + team, err := ds.NewTeam(ctx, &fleet.Team{ + Name: "team1", + }) + require.NoError(t, err) + + result, err := ds.GetTeamName(ctx, team.ID) + require.NoError(t, err) + require.Equal(t, team.Name, *result) + }) +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 9c0792d839..85af07ab74 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -82,6 +82,9 @@ 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) + // 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) @@ -397,6 +400,8 @@ type Datastore interface { SaveTeam(ctx context.Context, team *Team) (*Team, error) // Team retrieves the Team by ID. Team(ctx context.Context, tid uint) (*Team, error) + // GetTeamName retrieves the team name by their ID. + GetTeamName(ctx context.Context, teamID uint) (*string, error) // Team deletes the Team by ID. DeleteTeam(ctx context.Context, tid uint) error // TeamByName retrieves the Team by Name. diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index e1f53f86ae..5fd0676c89 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -70,6 +70,8 @@ type QueryFunc func(ctx context.Context, id uint) (*fleet.Query, error) type ListQueriesFunc func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) +type ListScheduledQueriesForAgentsFunc func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) + type 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) @@ -300,6 +302,8 @@ type SaveTeamFunc func(ctx context.Context, team *fleet.Team) (*fleet.Team, erro type TeamFunc func(ctx context.Context, tid uint) (*fleet.Team, error) +type GetTeamNameFunc func(ctx context.Context, teamID uint) (*string, error) + type DeleteTeamFunc func(ctx context.Context, tid uint) error type TeamByNameFunc func(ctx context.Context, name string) (*fleet.Team, error) @@ -739,6 +743,9 @@ type DataStore struct { ListQueriesFunc ListQueriesFunc ListQueriesFuncInvoked bool + ListScheduledQueriesForAgentsFunc ListScheduledQueriesForAgentsFunc + ListScheduledQueriesForAgentsFuncInvoked bool + QueryByNameFunc QueryByNameFunc QueryByNameFuncInvoked bool @@ -1084,6 +1091,9 @@ type DataStore struct { TeamFunc TeamFunc TeamFuncInvoked bool + GetTeamNameFunc GetTeamNameFunc + GetTeamNameFuncInvoked bool + DeleteTeamFunc DeleteTeamFunc DeleteTeamFuncInvoked bool @@ -1809,6 +1819,13 @@ func (s *DataStore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) return s.ListQueriesFunc(ctx, opt) } +func (s *DataStore) ListScheduledQueriesForAgents(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { + 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 @@ -2614,6 +2631,13 @@ func (s *DataStore) Team(ctx context.Context, tid uint) (*fleet.Team, error) { return s.TeamFunc(ctx, tid) } +func (s *DataStore) GetTeamName(ctx context.Context, teamID uint) (*string, error) { + s.mu.Lock() + s.GetTeamNameFuncInvoked = true + s.mu.Unlock() + return s.GetTeamNameFunc(ctx, teamID) +} + func (s *DataStore) DeleteTeam(ctx context.Context, tid uint) error { s.mu.Lock() s.DeleteTeamFuncInvoked = true diff --git a/server/service/osquery.go b/server/service/osquery.go index af9d8e5286..16da3b4e2f 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -347,8 +347,7 @@ func getClientConfigEndpoint(ctx context.Context, request interface{}, svc fleet } func (svc *Service) getScheduledQueries(ctx context.Context, teamID *uint) (fleet.Queries, error) { - opts := fleet.ListQueryOptions{IsScheduled: ptr.Bool(true), TeamID: teamID} - queries, err := svc.ds.ListQueries(ctx, opts) + queries, err := svc.ds.ListScheduledQueriesForAgents(ctx, teamID) if err != nil { return nil, err } @@ -444,18 +443,18 @@ func (svc *Service) GetClientConfig(ctx context.Context) (map[string]interface{} } if host.TeamID != nil { - team, err := svc.ds.Team(ctx, *host.TeamID) + teamName, err := svc.ds.GetTeamName(ctx, *host.TeamID) if err != nil { return nil, newOsqueryError("database error: " + err.Error()) } - if team != nil { + if teamName != 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: %s", team.Name) + packName := fmt.Sprintf("Team: %s", *teamName) packConfig[packName] = fleet.PackContent{ Queries: teamQueries, } diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index af76ee5aba..0b014ec45e 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -40,11 +40,9 @@ import ( func TestGetClientConfig(t *testing.T) { ds := new(mock.Store) - ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { - return &fleet.Team{ - Name: "Alamo", - ID: 1, - }, nil + ds.GetTeamNameFunc = func(ctx context.Context, tid uint) (*string, error) { + teamName := "Alamo" + return &teamName, nil } ds.TeamAgentOptionsFunc = func(ctx context.Context, teamID uint) (*json.RawMessage, error) { @@ -71,8 +69,8 @@ func TestGetClientConfig(t *testing.T) { return []*fleet.ScheduledQuery{}, nil } } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { - if opt.TeamID == nil { + ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { + if teamID == nil { return nil, nil } return []*fleet.Query{ @@ -2006,6 +2004,14 @@ func TestUpdateHostIntervals(t *testing.T) { svc, ctx := newTestService(t, ds, nil, nil) + ds.GetTeamNameFunc = func(ctx context.Context, tid uint) (*string, error) { + return 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 } From b63f465ef7986403e53728ea939b2ed41a006081 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Wed, 19 Jul 2023 14:17:59 -0700 Subject: [PATCH 43/78] Use teamIdForAPI for saving new queries --- frontend/pages/queries/QueryPage/QueryPage.tsx | 10 +++++++++- .../pages/queries/QueryPage/screens/QueryEditor.tsx | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx index ec5205dfc6..9ed4765415 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tsx @@ -47,13 +47,21 @@ const QueryPage = ({ location, }: IQueryPageProps): JSX.Element => { const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null; - const { currentTeamSummary: teamForQuery } = useTeamIdParam({ + const { + currentTeamName: teamName, + teamIdForApi: apiTeamIdForQuery, + } = useTeamIdParam({ location, router, includeAllTeams: true, includeNoTeam: false, }); + const teamForQuery = + apiTeamIdForQuery && teamName + ? { id: apiTeamIdForQuery, name: teamName } + : undefined; + const handlePageError = useErrorHandler(); const { isGlobalAdmin, diff --git a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx index 46ccdf1c1f..99160b43ac 100644 --- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx +++ b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx @@ -98,7 +98,7 @@ const QueryEditor = ({ } catch (createError: any) { if (createError.data.errors[0].reason.includes("already exists")) { const teamErrorText = - teamForQuery && teamForQuery?.id !== -1 + teamForQuery && teamForQuery?.id !== 0 ? `the ${teamForQuery.name} team` : "all teams"; setBackendValidators({ From dab19c77d04cd49397ef1f1debcacb8eddbc0e63 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:43:09 -0400 Subject: [PATCH 44/78] Fleet UI: (Unreleased bug) When no platforms are selected, empty string platforms, show ALL platform icons in table and All in platform dropdown (#12865) --- .../QueriesTable/QueriesTableConfig.tsx | 23 +++++-------------- .../services/mock_service/mocks/responses.ts | 3 +-- frontend/utilities/constants.ts | 14 ++++++----- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 444edc55ae..eacbc5cfea 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -10,11 +10,7 @@ import permissionsUtils from "utilities/permissions"; import { IUser } from "interfaces/user"; import { secondsToDhms } from "utilities/helpers"; import { ISchedulableQuery } from "interfaces/schedulable_query"; -import { - SelectedPlatformString, - SupportedPlatform, - SUPPORTED_PLATFORMS, -} from "interfaces/platform"; +import { SupportedPlatform } from "interfaces/platform"; import Icon from "components/Icon"; import Checkbox from "components/forms/fields/Checkbox"; @@ -166,18 +162,11 @@ const generateTableHeaders = ({ accessor: "platforms", Cell: (cellProps: IPlatformCellProps): JSX.Element => { // translate the SelectedPlatformString into an array of `SupportedPlatform`s - const selectedPlatforms = - (cellProps.row.original.platform - ?.split(",") - .filter((platform) => platform !== "") as SupportedPlatform[]) ?? - []; - - const platformIconsToRender: SupportedPlatform[] = - selectedPlatforms.length === 0 - ? // User didn't select any platforms, so we render all compatible - cellProps.cell.value - : // Render the platforms the user has selected for this query - selectedPlatforms; + const platformIconsToRender = (cellProps.row.original.platform === "" + ? ["darwin", "windows", "linux", "chrome"] + : cellProps.row.original.platform + ?.split(",") + .filter((platform) => platform !== "")) as SupportedPlatform[]; return ; }, diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index 31edff807e..524a638ed2 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -486,7 +486,7 @@ const globalQueries = { query: "SELECT * FROM osquery_info", team_id: 2, interval: 604800, // Every week - platform: "Windows", + platform: "windows", min_osquery_version: "", automations_enabled: false, logging: "differential", @@ -563,7 +563,6 @@ const teamQueries = { total_executions: 1, }, performance: "Undetermined", - platforms: ["windows"], }, { created_at: "2023-06-08T15:31:35Z", diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts index 06c13429aa..8034a9ba8a 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -201,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 }, @@ -213,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: "", From b22102bb94f49bf22fafdd5faf6241ccc39b8508 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Wed, 19 Jul 2023 17:50:27 -0700 Subject: [PATCH 45/78] Pass team_id when editing existing queries; update error text --- .../QueriesTable/QueriesTableConfig.tsx | 11 ++++----- .../pages/queries/QueryPage/QueryPage.tsx | 10 +++----- .../components/QueryForm/QueryForm.tsx | 23 +++++++++++++++---- .../SaveQueryModal/SaveQueryModal.tsx | 6 ++--- .../queries/QueryPage/screens/QueryEditor.tsx | 16 ++++++------- frontend/router/paths.ts | 6 +++-- 6 files changed, 41 insertions(+), 31 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 444edc55ae..09f7f19fdd 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -10,11 +10,7 @@ import permissionsUtils from "utilities/permissions"; import { IUser } from "interfaces/user"; import { secondsToDhms } from "utilities/helpers"; import { ISchedulableQuery } from "interfaces/schedulable_query"; -import { - SelectedPlatformString, - SupportedPlatform, - SUPPORTED_PLATFORMS, -} from "interfaces/platform"; +import { SupportedPlatform } from "interfaces/platform"; import Icon from "components/Icon"; import Checkbox from "components/forms/fields/Checkbox"; @@ -153,7 +149,10 @@ const generateTableHeaders = ({ )} } - path={PATHS.EDIT_QUERY(cellProps.row.original.id)} + path={PATHS.EDIT_QUERY( + cellProps.row.original.id, + cellProps.row.original.team_id ?? undefined + )} /> ); }, diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx index 9ed4765415..26eb0cd0e4 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tsx @@ -48,7 +48,7 @@ const QueryPage = ({ }: IQueryPageProps): JSX.Element => { const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null; const { - currentTeamName: teamName, + currentTeamName: teamNameForQuery, teamIdForApi: apiTeamIdForQuery, } = useTeamIdParam({ location, @@ -57,11 +57,6 @@ const QueryPage = ({ includeNoTeam: false, }); - const teamForQuery = - apiTeamIdForQuery && teamName - ? { id: apiTeamIdForQuery, name: teamName } - : undefined; - const handlePageError = useErrorHandler(); const { isGlobalAdmin, @@ -220,7 +215,8 @@ const QueryPage = ({ router, baseClass, queryIdForEdit: queryId, - teamForQuery, + teamNameForQuery, + apiTeamIdForQuery, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index 3496634bdb..81f571abeb 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -53,7 +53,8 @@ const baseClass = "query-form"; interface IQueryFormProps { router: InjectedRouter; queryIdForEdit: number | null; - teamIdForQuery?: number; + apiTeamIdForQuery?: number; + teamNameForQuery?: string; showOpenSchemaActionText: boolean; storedQuery: ISchedulableQuery | undefined; isStoredQueryLoading: boolean; @@ -83,7 +84,8 @@ const validateQuerySQL = (query: string) => { const QueryForm = ({ router, queryIdForEdit, - teamIdForQuery, + apiTeamIdForQuery, + teamNameForQuery, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, @@ -270,12 +272,12 @@ 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, @@ -294,6 +296,7 @@ const QueryForm = ({ name: `Copy of ${lastEditedQueryName}`, description: lastEditedQueryDescription, query: lastEditedQueryBody, + team_id: apiTeamIdForQuery, observer_can_run: lastEditedQueryObserverCanRun, interval: lastEditedQueryFrequency, platform: lastEditedQueryPlatforms, @@ -314,9 +317,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); @@ -709,7 +722,7 @@ const QueryForm = ({ {showSaveQueryModal && ( void; toggleSaveQueryModal: () => void; @@ -48,7 +48,7 @@ const validateQueryName = (name: string) => { const SaveQueryModal = ({ queryValue, - teamIdForQuery, + apiTeamIdForQuery, isLoading, saveQuery, toggleSaveQueryModal, @@ -114,7 +114,7 @@ const SaveQueryModal = ({ // from previous New query page query: queryValue, // from doubly previous ManageQueriesPage - team_id: teamIdForQuery, + team_id: apiTeamIdForQuery, }); } }; diff --git a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx index 99160b43ac..6680dd03d6 100644 --- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx +++ b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx @@ -22,10 +22,8 @@ interface IQueryEditorProps { router: InjectedRouter; baseClass: string; queryIdForEdit: number | null; - teamForQuery?: { - name: string; - id: number; - }; + teamNameForQuery?: string; + apiTeamIdForQuery?: number; storedQuery: ISchedulableQuery | undefined; storedQueryError: Error | null; showOpenSchemaActionText: boolean; @@ -45,7 +43,8 @@ const QueryEditor = ({ router, baseClass, queryIdForEdit, - teamForQuery, + teamNameForQuery, + apiTeamIdForQuery, storedQuery, storedQueryError, showOpenSchemaActionText, @@ -98,8 +97,8 @@ const QueryEditor = ({ } catch (createError: any) { if (createError.data.errors[0].reason.includes("already exists")) { const teamErrorText = - teamForQuery && teamForQuery?.id !== 0 - ? `the ${teamForQuery.name} team` + teamNameForQuery && apiTeamIdForQuery !== 0 + ? `the ${teamNameForQuery} team` : "all teams"; setBackendValidators({ name: `A query with that name already exists for ${teamErrorText}.`, @@ -176,7 +175,8 @@ const QueryEditor = ({ onUpdate={onUpdateQuery} storedQuery={storedQuery} queryIdForEdit={queryIdForEdit} - teamIdForQuery={teamForQuery?.id} + apiTeamIdForQuery={apiTeamIdForQuery} + teamNameForQuery={teamNameForQuery} isStoredQueryLoading={isStoredQueryLoading} showOpenSchemaActionText={showOpenSchemaActionText} onOpenSchemaSidebar={onOpenSchemaSidebar} diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index a4505eaea0..2b25a314fa 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -43,8 +43,10 @@ export default { EDIT_LABEL: (labelId: number): string => { return `${URL_PREFIX}/labels/${labelId}`; }, - EDIT_QUERY: (queryId: number): string => { - return `${URL_PREFIX}/queries/${queryId}`; + 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}${ From 87a7a508a9c045f4756ed90f9d132b5185885b6f Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:38:58 -0400 Subject: [PATCH 46/78] Fleet UI: (Unreleased bug) Redirect to query editor on creating a new query (#12870) ## Issue Unreleased bug #12857 ## Description Clean up create call so we can easily go to the newly created query using the newly created query id ## QA - Rachel and @jacobshandling tested this on the fullstack branch already and works great NOTE: Reed should still test :) # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Manual QA for all new/changed functionality --- frontend/pages/queries/QueryPage/QueryPage.tsx | 8 -------- frontend/pages/queries/QueryPage/screens/QueryEditor.tsx | 8 +------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx index 26eb0cd0e4..ca25321e57 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tsx @@ -13,7 +13,6 @@ import { IHost, IHostResponse } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { ITeam } from "interfaces/team"; import { - ICreateQueryRequestBody, IGetQueryResponse, ISchedulableQuery, } from "interfaces/schedulable_query"; @@ -143,12 +142,6 @@ const QueryPage = ({ } ); - const { - mutateAsync: createQuery, - } = useMutation((formData: ICreateQueryRequestBody) => - queryAPI.create(formData) - ); - const detectIsFleetQueryRunnable = () => { statusAPI.live_query().catch(() => { setIsLiveQueryRunnable(false); @@ -221,7 +214,6 @@ const QueryPage = ({ storedQuery, isStoredQueryLoading, storedQueryError, - createQuery, onOsqueryTableSelect, goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]), onOpenSchemaSidebar, diff --git a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx index 6680dd03d6..7b94956efd 100644 --- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx +++ b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx @@ -28,11 +28,6 @@ interface IQueryEditorProps { storedQueryError: Error | null; showOpenSchemaActionText: boolean; isStoredQueryLoading: boolean; - createQuery: UseMutateAsyncFunction< - ISchedulableQuery, - unknown, - ICreateQueryRequestBody - >; onOsqueryTableSelect: (tableName: string) => void; goToSelectTargets: () => void; onOpenSchemaSidebar: () => void; @@ -49,7 +44,6 @@ const QueryEditor = ({ storedQueryError, showOpenSchemaActionText, isStoredQueryLoading, - createQuery, onOsqueryTableSelect, goToSelectTargets, onOpenSchemaSidebar, @@ -90,7 +84,7 @@ const QueryEditor = ({ const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => { setIsQuerySaving(true); try { - const query = await createQuery(formData); + const { query } = await queryAPI.create(formData); router.push(PATHS.EDIT_QUERY(query.id)); renderFlash("success", "Query created!"); setBackendValidators({}); From 640e9a8dda18f326f638e1e361ad716e7c06f87f Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:41:21 -0400 Subject: [PATCH 47/78] Fleet UI: (Unreleased bug) Platform compatibility always loads (#12876) ## Issue Cerra #12859 ## Description - Ensure compatibility is being calculated and rendered even if query has loaded before ## Screen recording clicking back and forth into same query and still seeing compatibility # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Manual QA for all new/changed functionality --- .../pages/queries/QueryPage/components/QueryForm/QueryForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index 81f571abeb..8bfd1ba48a 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -165,7 +165,7 @@ const QueryForm = ({ } debounceSQL(lastEditedQueryBody); - }, [lastEditedQueryBody, lastEditedQueryId]); + }, [lastEditedQueryBody, lastEditedQueryId, isStoredQueryLoading]); const hasTeamMaintainerPermissions = savedQueryMode ? isAnyTeamMaintainerOrTeamAdmin && From bc25e23f205071e706e61adb06936a050c90e63d Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 20 Jul 2023 11:42:01 -0700 Subject: [PATCH 48/78] UI - Remove select boxes from inherited QueriesTable (#12875) ## Addresses #12636 Screenshot 2023-07-20 at 11 23 18 AM - [x] Manual QA for all new/changed functionality Co-authored-by: Jacob Shandling --- .../components/QueriesTable/QueriesTable.tsx | 4 ++-- .../components/QueriesTable/QueriesTableConfig.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index 13b24a84da..d016fd53eb 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -237,8 +237,8 @@ const QueriesTable = ({ }; const tableHeaders = useMemo( - () => currentUser && generateTableHeaders({ currentUser }), - [currentUser] + () => currentUser && generateTableHeaders({ currentUser, isInherited }), + [currentUser, isInherited] ); const searchable = diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 823b0ceb5c..c8c7742615 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -97,12 +97,14 @@ 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( @@ -257,7 +259,7 @@ const generateTableHeaders = ({ ), }, ]; - if (!isOnlyObserver) { + if (!isOnlyObserver && !isInherited) { tableHeaders.splice(0, 0, { id: "selection", Header: (cellProps: IHeaderProps): JSX.Element => { From 0aa293201a1efb656a697377c76b6f9a6a20d1fb Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:52:56 -0400 Subject: [PATCH 49/78] Fleet UI: (Unreleased bug) Team admin/maintainer can edit and delete any of their team's queries (#12863) ## Issue Cerra #12858 ## Description - Allow any team admin/maintainer to use checkboxes on all queries on the manage queries page for deleting queries - Allow any team admin/maintainer to click save to update a query on the edit query page ## QA - [x] I QAed frontend only on team admin, team maintainer that they had the functionality - [x] I QAed frontend only on team observer that they do NOT have the functionality NOTE: Needs QA on a branch with backend to confirm full E2E changes ## Screen recording of team maintainer who didn't author the query now having functionality https://github.com/fleetdm/fleet/assets/71795832/1e93714a-5721-4278-bac3-5ce9f896a49f # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Manual QA for all new/changed functionality --- .../QueriesTable/QueriesTableConfig.tsx | 82 +------------------ .../components/QueryForm/QueryForm.tsx | 43 ++-------- 2 files changed, 10 insertions(+), 115 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index c8c7742615..225a5ebb23 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -265,56 +265,15 @@ const generateTableHeaders = ({ Header: (cellProps: IHeaderProps): 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 ; @@ -325,44 +284,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/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index 8bfd1ba48a..75c4e45488 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -670,43 +670,14 @@ const QueryForm = ({ )}
-
- -
{" "} - - <> - You can only save -
changes to a query if you -
are the author. - -
+ Save +
)} From 099ed43acf5923b06970b64fad10ebb02c3c4da1 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 20 Jul 2023 15:38:09 -0400 Subject: [PATCH 50/78] Fleet UI: (Unreleased bug) Team admin/maintainers cannot save global queries (#12878) --- .../components/QueryForm/QueryForm.tsx | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index 75c4e45488..8d8661eb51 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -30,6 +30,7 @@ import { } from "interfaces/schedulable_query"; import { SelectedPlatformString } from "interfaces/platform"; import queryAPI from "services/entities/queries"; +import { COLORS } from "styles/var/colors"; import { IAceEditor } from "react-ace/lib/types"; import ReactTooltip from "react-tooltip"; @@ -670,14 +671,40 @@ const QueryForm = ({ )}
- + +
{" "} + + <> + You can only save changes +
to a team level query. + +
)} From fc9a970e57bc438da27e27d313b9c14ae5727143 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:55:11 -0700 Subject: [PATCH 51/78] UI - Search only the current-scope queries (#12880) ## Addresses #12868 Screenshot 2023-07-20 at 12 44 16 PM - [x] Manual QA for all new/changed functionality Co-authored-by: Jacob Shandling --- .../ManageQueriesPage/components/QueriesTable/QueriesTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index d016fd53eb..c3730ad5c1 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -251,7 +251,7 @@ const QueriesTable = ({ resultsTitle="queries" columns={tableHeaders} data={queriesList} - filters={{ global: searchQuery }} + filters={{ global: isInherited ? "" : searchQuery }} isLoading={isLoading} defaultSortHeader={sortHeader || DEFAULT_SORT_HEADER} defaultSortDirection={sortDirection || DEFAULT_SORT_DIRECTION} From 2faa476272c695ed32b40fca3c5a23b94b7d2d44 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:45:25 -0700 Subject: [PATCH 52/78] UI: 2 Query name styling issues (#12885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #12883 and #12877 - Truncate long query names such that the name itself is truncated with ellipses while the "Observer can run" icon and associated tooltip, if present, remain visible. - Address lower-case "p"s in query name getting cutoff. Screenshot 2023-07-20 at 2 21 16 PM ## QA @xpkoala –The style that was removed here to address the text cutoff issue is a far-reaching change, since this problem may be happening elsewhere the TextCell component is used. I did manually QA this, but please take a careful look at other tables to double check that other text cells are still styled correctly. - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- frontend/components/TableContainer/DataTable/_styles.scss | 2 -- .../components/PoliciesTable/PoliciesTableConfig.tsx | 3 ++- frontend/pages/queries/ManageQueriesPage/_styles.scss | 8 ++++++++ .../components/QueriesTable/QueriesTableConfig.tsx | 5 +++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index 3a850a34b7..7ada8b5274 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -216,8 +216,6 @@ $shadow-transition-width: 10px; .link-cell, .text-cell { display: block; - overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; margin: 0; .__react_component_tooltip { 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/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 36e9fadfcd..722bd10239 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -179,6 +179,14 @@ } } + .query-name-cell { + .children-wrapper { + .query-name-text { + text-overflow: ellipsis; + overflow: hidden; + } + } + } .query-icon { position: relative; top: 2px; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 225a5ebb23..b8ab7431ff 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -20,6 +20,7 @@ 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 { @@ -124,7 +125,7 @@ const generateTableHeaders = ({ Cell: (cellProps: ICellProps): JSX.Element => { return (
{cellProps.cell.value}
@@ -143,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. From a05cf0618b7a50d50ed989477fe1e12c34db6773 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 20 Jul 2023 16:43:13 -0700 Subject: [PATCH 53/78] =?UTF-8?q?UI=20=E2=80=93=20ManageAutomationsModal?= =?UTF-8?q?=20style=20fixes=20(#12892)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #12891 Screenshot 2023-07-20 at 4 32 00 PM - [x] Manual QA for all new/changed functionality Co-authored-by: Jacob Shandling --- .../QueryFrequencyIndicator/_styles.scss | 1 + .../ManageAutomationsModal.tsx | 21 ++++++++----------- .../ManageAutomationsModal/_styles.scss | 17 +++++++++++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/frontend/components/QueryFrequencyIndicator/_styles.scss b/frontend/components/QueryFrequencyIndicator/_styles.scss index fb6624429f..f5b5f74d01 100644 --- a/frontend/components/QueryFrequencyIndicator/_styles.scss +++ b/frontend/components/QueryFrequencyIndicator/_styles.scss @@ -1,6 +1,7 @@ .query-frequency-indicator { width: 100px; display: flex; + align-items: center; padding: 8px 12px; .icon { diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index b5cfea5899..107267c8e3 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -11,10 +11,6 @@ import LogDestinationIndicator from "components/LogDestinationIndicator/LogDesti import { ISchedulableQuery } from "interfaces/schedulable_query"; -interface IFrequencyIndicator { - frequency: number; - checked: boolean; -} interface IManageAutomationsModalProps { isUpdatingAutomations: boolean; handleSubmit: (formData: any) => void; // TODO @@ -163,14 +159,15 @@ const ManageAutomationsModal = ({
- Automations currently run on macOS, Windows, and Linux hosts. -
- Interested in query automations for your Chromebooks?   - +

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

+

+ Interested in query automations for your Chromebooks?   + +

diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss index 296b36f7aa..78a5f043f7 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss @@ -26,8 +26,25 @@ } } + &__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; From eb63cf89833529c869ac274ae3ca66cf9bd6e99c Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 21 Jul 2023 13:03:18 -0400 Subject: [PATCH 54/78] Fleet UI: (Unreleased bug) Fix create team query from not disabling the save button (#12903) --- .../QueryPage/components/QueryForm/QueryForm.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index 8d8661eb51..be4a3763cf 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -9,6 +9,7 @@ import { InjectedRouter } from "react-router"; 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"; @@ -30,7 +31,6 @@ import { } from "interfaces/schedulable_query"; import { SelectedPlatformString } from "interfaces/platform"; import queryAPI from "services/entities/queries"; -import { COLORS } from "styles/var/colors"; import { IAceEditor } from "react-ace/lib/types"; import ReactTooltip from "react-tooltip"; @@ -676,7 +676,11 @@ const QueryForm = ({ data-for="save-query-button" // Tooltip shows for team maintainer/admins viewing global queries data-tip-disable={ - !(isAnyTeamMaintainerOrTeamAdmin && !storedQuery?.team_id) + !( + isAnyTeamMaintainerOrTeamAdmin && + !storedQuery?.team_id && + !!queryIdForEdit + ) } > -
- )} -
-
-

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 = []; + updateAutomatedQueries.push( + turnOnAutomations.map((id: number) => + queriesAPI.update(id, { automations_enabled: true }) + ) + ); + updateAutomatedQueries.push( + turnOffAutomations.map((id: number) => + 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..722bd10239 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,99 @@ } .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; + } + &__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..107267c8e3 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -0,0 +1,202 @@ +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 }>({}); + + const { queryItems, updateQueryItems } = useCheckboxListStateManagement( + availableQueries || [], + 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..c3730ad5c1 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -39,7 +39,9 @@ interface IQueriesTableProps { query?: string; order_key?: string; order_direction?: "asc" | "desc"; + team_id?: string; }; + isInherited?: boolean; } const DEFAULT_SORT_DIRECTION = "desc"; @@ -88,8 +90,9 @@ const QueriesTable = ({ isOnlyObserver, isObserverPlus, isAnyTeamObserverPlus, - queryParams, router, + queryParams, + isInherited = false, }: IQueriesTableProps): JSX.Element | null => { const { currentUser } = useContext(AppContext); @@ -143,6 +146,7 @@ const QueriesTable = ({ ) { newQueryParams.page = 0; } + newQueryParams.team_id = queryParams?.team_id; const locationPath = getNextLocationPath({ pathPrefix: PATHS.MANAGE_QUERIES, queryParams: newQueryParams, @@ -233,19 +237,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 ? (
IGetToggleAllRowsSelectedProps; toggleRowSelected: () => void; }; @@ -55,13 +57,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 +84,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,12 +98,14 @@ 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( @@ -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 + )} /> ); }, @@ -144,34 +167,40 @@ const generateTableHeaders = ({ disableSortBy: true, accessor: "platforms", Cell: (cellProps: IPlatformCellProps): JSX.Element => { - return ; + // translate the SelectedPlatformString into an array of `SupportedPlatform`s + const platformIconsToRender = (cellProps.row.original.platform === "" + ? ["darwin", "windows", "linux", "chrome"] + : cellProps.row.original.platform + ?.split(",") + .filter((platform) => platform !== "")) as SupportedPlatform[]; + + 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 +218,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 +250,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 +285,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..524a638ed2 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -364,8 +364,260 @@ 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", + }, + { + 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 78a79ede80..8034a9ba8a 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); From fdf89989c54cce26fe0454e716e3a1db674e8722 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 24 Jul 2023 10:46:52 -0400 Subject: [PATCH 57/78] Fleet UI: Queries default to alpha order (#12924) --- .../components/PoliciesTable/PoliciesTable.tsx | 2 +- .../ManageAutomationsModal/ManageAutomationsModal.tsx | 8 +++++++- .../components/QueriesTable/QueriesTable.tsx | 7 +++---- 3 files changed, 11 insertions(+), 6 deletions(-) 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/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index 107267c8e3..0e9d5daae1 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -66,8 +66,14 @@ const ManageAutomationsModal = ({ // 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( - availableQueries || [], + sortedAvailableQueries, automatedQueryIds || [] ); diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index c3730ad5c1..abb55b687e 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -44,8 +44,8 @@ interface IQueriesTableProps { 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"; @@ -99,8 +99,7 @@ const QueriesTable = ({ // 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 = (() => From fe7cd295a49b2914c616e00316c3a455b25089f5 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 24 Jul 2023 11:00:16 -0400 Subject: [PATCH 58/78] Fleet integration tests: Remove schedule tab from frontend integration tests (#12925) --- .../components/top_nav/SiteTopNav/SiteTopNav.tests.tsx | 9 --------- 1 file changed, 9 deletions(-) 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(); }); From a265559ee7091109f7ef19304f4a795a3fe320b3 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Mon, 24 Jul 2023 19:59:34 -0400 Subject: [PATCH 59/78] Combined schedules and queries data migration (#12855) Added data migration for migrating scheduled queries in the global and team packs to the new query structure. --- .../20230721161508_QueriesDataMigrator.go | 467 ++++++++++++++++++ ...20230721161508_QueriesDataMigrator_test.go | 293 +++++++++++ server/datastore/mysql/schema.sql | 4 +- 3 files changed, 762 insertions(+), 2 deletions(-) create mode 100644 server/datastore/mysql/migrations/tables/20230721161508_QueriesDataMigrator.go create mode 100644 server/datastore/mysql/migrations/tables/20230721161508_QueriesDataMigrator_test.go 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/schema.sql b/server/datastore/mysql/schema.sql index 7a29fbbab0..3ca3d2e285 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=199 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=200 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'); +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'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( From 2afbd24021b0dada19ccf121a40a36c0d2da99cc Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Mon, 24 Jul 2023 21:17:20 -0300 Subject: [PATCH 60/78] Combine Schedules and Queries: API changes (#12778) Combining schedules and queries API changes. --- cmd/fleet/cron.go | 6 - cmd/fleetctl/convert_test.go | 2 +- cmd/fleetctl/get.go | 43 +- cmd/fleetctl/get_test.go | 298 ++++++- cmd/fleetctl/query.go | 10 +- cmd/fleetctl/testdata/convert_output.yml | 35 + docs/Using-Fleet/Permissions.md | 14 +- server/authz/policy.rego | 165 ++-- server/authz/policy_test.go | 770 ++++++++++-------- server/datastore/cached_mysql/cached_mysql.go | 24 +- .../cached_mysql/cached_mysql_test.go | 58 +- server/datastore/mysql/aggregated_stats.go | 44 +- .../datastore/mysql/aggregated_stats_test.go | 34 +- server/datastore/mysql/hosts.go | 242 +++++- server/datastore/mysql/hosts_test.go | 223 +++-- server/datastore/mysql/packs.go | 106 --- server/datastore/mysql/packs_test.go | 138 +--- server/datastore/mysql/queries.go | 30 +- server/datastore/mysql/queries_test.go | 38 +- server/datastore/mysql/scheduled_queries.go | 197 +++-- .../datastore/mysql/scheduled_queries_test.go | 144 +++- server/datastore/mysql/teams.go | 32 +- server/datastore/mysql/teams_test.go | 52 -- server/fleet/datastore.go | 11 +- server/fleet/queries.go | 141 +++- server/fleet/queries_test.go | 12 +- server/fleet/scheduled_queries.go | 99 +++ server/fleet/service.go | 13 +- server/mock/datastore_mock.go | 54 +- .../async/async_scheduled_query_stats.go | 4 +- .../async/async_scheduled_query_stats_test.go | 8 +- server/service/async/async_test.go | 2 +- server/service/base_client.go | 6 +- server/service/client.go | 2 + server/service/client_mdm.go | 3 +- server/service/client_queries.go | 24 +- server/service/client_teams.go | 10 + server/service/global_schedule.go | 81 +- server/service/global_schedule_test.go | 39 +- server/service/handler.go | 4 +- server/service/integration_core_test.go | 8 +- server/service/integration_enterprise_test.go | 69 +- server/service/osquery.go | 18 +- server/service/osquery_test.go | 29 +- server/service/osquery_utils/queries.go | 2 +- server/service/osquery_utils/queries_test.go | 2 +- server/service/packs_test.go | 81 +- server/service/queries.go | 254 ++++-- server/service/queries_test.go | 265 +++++- server/service/scheduled_queries.go | 7 +- server/service/scheduled_queries_test.go | 24 + server/service/team_policies_test.go | 3 +- server/service/team_schedule.go | 91 ++- server/service/team_schedule_test.go | 91 ++- server/service/teams_test.go | 2 +- server/service/testing_client.go | 12 + server/test/new_objects.go | 18 +- 57 files changed, 2558 insertions(+), 1636 deletions(-) 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/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/get.go b/cmd/fleetctl/get.go index af6f9aed59..f512ea26cb 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, @@ -304,6 +304,10 @@ func getQueriesCommand() *cli.Command { 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 +322,14 @@ func getQueriesCommand() *cli.Command { name := c.Args().First() + var teamID *uint + if tid := c.Uint(teamFlagName); tid != 0 { + teamID = &tid + } + // if name wasn't provided, list all queries if name == "" { - queries, err := client.GetQueries() + queries, err := client.GetQueries(teamID) if err != nil { return fmt.Errorf("could not list queries: %w", err) } @@ -354,12 +363,29 @@ func getQueriesCommand() *cli.Command { return nil } + var teamName string + if teamID != nil { + team, err := client.GetTeam(*teamID) + if err != nil { + return fmt.Errorf("get team: %w", err) + } + teamName = team.Name + } + 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) } @@ -382,12 +408,12 @@ func getQueriesCommand() *cli.Command { 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 +452,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 +483,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 +496,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, diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 7e10951d2a..7a1f0afe23 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,273 @@ 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 := `+--------+-------------+-----------+ + expectedGlobal := `+--------+-------------+-----------+ | NAME | DESCRIPTION | QUERY | +--------+-------------+-----------+ | query1 | some desc | select 1; | +--------+-------------+-----------+ | query2 | some desc 2 | select 2; | +--------+-------------+-----------+ +| query4 | some desc 4 | select 4; | ++--------+-------------+-----------+ ` - 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 | ++--------+-------------+-----------+ +| query3 | some desc 3 | select 3; | ++--------+-------------+-----------+ +` + 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.QueryByNameFunc = func(ctx context.Context, teamID *uint, 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 @@ -1167,11 +1349,18 @@ func TestGetQueriesAsObserver(t *testing.T) { 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"})) @@ -1213,27 +1402,48 @@ spec: 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"})) 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/docs/Using-Fleet/Permissions.md b/docs/Using-Fleet/Permissions.md index 5aa124d52a..d845bf54a1 100644 --- a/docs/Using-Fleet/Permissions.md +++ b/docs/Using-Fleet/Permissions.md @@ -10,12 +10,12 @@ Users with the admin role receive all permissions. ### Maintainer -Maintainers can manage most entities in Fleet, like queries, policies, labels and schedules. +Maintainers can manage most entities in Fleet, like queries, policies and labels. Unlike admins, maintainers cannot edit higher level settings like application configuration, teams or users. ### Observer -The Observer role is a read-only role. It can access most entities in Fleet, like queries, policies, labels, schedules, application configuration, teams, etc. +The Observer role is a read-only role. It can access most entities in Fleet, like queries, policies, labels, application configuration, teams, etc. They can also run queries configured with the `observer_can_run` flag set to `true`. ### Observer+ @@ -51,7 +51,6 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. | Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) against all hosts | | ✅ | ✅ | ✅ | | | Create, edit, and delete queries | | | ✅ | ✅ | ✅ | | View all queries\** | ✅ | ✅ | ✅ | ✅ | | -| Add, edit, and remove queries from all schedules | | | ✅ | ✅ | ✅ | | Create, edit, view, and delete packs | | | ✅ | ✅ | ✅ | | View all policies | ✅ | ✅ | ✅ | ✅ | | | Filter hosts using policies | ✅ | ✅ | ✅ | ✅ | | @@ -100,11 +99,11 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. Users in Fleet either have team access or global access. -Users with team access only have access to the [hosts](https://fleetdm.com/docs/using-fleet/rest-api#hosts), [software](https://fleetdm.com/docs/using-fleet/rest-api#software), [schedules](https://fleetdm.com/docs/using-fleet/fleet-ui#schedule-a-query) , and [policies](https://fleetdm.com/docs/using-fleet/rest-api#policies) assigned to +Users with team access only have access to the [hosts](https://fleetdm.com/docs/using-fleet/rest-api#hosts), [software](https://fleetdm.com/docs/using-fleet/rest-api#software), and [policies](https://fleetdm.com/docs/using-fleet/rest-api#policies) assigned to their team. Users with global access have access to all -[hosts](https://fleetdm.com/docs/using-fleet/rest-api#hosts), [software](https://fleetdm.com/docs/using-fleet/rest-api#software), [queries](https://fleetdm.com/docs/using-fleet/rest-api#queries), [schedules](https://fleetdm.com/docs/using-fleet/fleet-ui#schedule-a-query) , and [policies](https://fleetdm.com/docs/using-fleet/rest-api#policies). Check out [the user permissions +[hosts](https://fleetdm.com/docs/using-fleet/rest-api#hosts), [software](https://fleetdm.com/docs/using-fleet/rest-api#software), [queries](https://fleetdm.com/docs/using-fleet/rest-api#queries), and [policies](https://fleetdm.com/docs/using-fleet/rest-api#policies). Check out [the user permissions table](#user-permissions) above for global user permissions. Users can be a member of multiple teams in Fleet. @@ -120,11 +119,10 @@ Users that are members of multiple teams can be assigned different roles for eac | Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ | ✅ | | | Filter hosts by software | ✅ | ✅ | ✅ | ✅ | | | Filter software | ✅ | ✅ | ✅ | ✅ | | -| Run queries designated "**observer can run**" as live queries against hosts | ✅ | ✅ | ✅ | ✅ | | +| Run global and team queries designated "**observer can run**" as live queries against hosts | ✅ | ✅ | ✅ | ✅ | | | Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) | | ✅ | ✅ | ✅ | | -| Create, edit, and delete only **self authored** queries | | | ✅ | ✅ | ✅ | +| Create, edit, and delete team queries | | | ✅ | ✅ | ✅ | | View all queries\** | ✅ | ✅ | ✅ | ✅ | | -| Add, edit, and remove queries from the schedule | | | ✅ | ✅ | ✅ | | View policies | ✅ | ✅ | ✅ | ✅ | | | View global (inherited) policies | ✅ | ✅ | ✅ | ✅ | | | Run global (inherited) policies as a live policy | | | ✅ | ✅ | | 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/cached_mysql/cached_mysql.go b/server/datastore/cached_mysql/cached_mysql.go index fca6d3ed5d..80c2820d2d 100644 --- a/server/datastore/cached_mysql/cached_mysql.go +++ b/server/datastore/cached_mysql/cached_mysql.go @@ -25,8 +25,8 @@ const ( defaultTeamFeaturesExpiration = 1 * time.Minute teamMDMConfigKey = "TeamMDMConfig:team:%d" defaultTeamMDMConfigExpiration = 1 * time.Minute - teamNameByIdKey = "TeamName:team:%d" - scheduledQueriesForAgentsKey = "ScheduledQueriesAgents:team:%d" + // scheduledQueriesForAgentsKey uses defaultScheduledQueriesExpiration for expiration. + scheduledQueriesForAgentsKey = "ScheduledQueriesAgents:team:%d" ) // cloner represents any type that can clone itself. Used by types to provide a more efficient clone method. @@ -298,12 +298,10 @@ func (ds *cachedMysql) SaveTeam(ctx context.Context, team *fleet.Team) (*fleet.T agentOptionsKey := fmt.Sprintf(teamAgentOptionsKey, team.ID) featuresKey := fmt.Sprintf(teamFeaturesKey, team.ID) mdmConfigKey := fmt.Sprintf(teamMDMConfigKey, team.ID) - teamNameKey := fmt.Sprintf(teamNameByIdKey, team.ID) ds.c.Set(agentOptionsKey, team.Config.AgentOptions, ds.teamAgentOptionsExp) ds.c.Set(featuresKey, &team.Config.Features, ds.teamFeaturesExp) ds.c.Set(mdmConfigKey, &team.Config.MDM, ds.teamMDMConfigExp) - ds.c.Set(teamNameKey, &team.Name, ds.scheduledQueriesExp) return team, nil } @@ -317,32 +315,14 @@ func (ds *cachedMysql) DeleteTeam(ctx context.Context, teamID uint) error { agentOptionsKey := fmt.Sprintf(teamAgentOptionsKey, teamID) featuresKey := fmt.Sprintf(teamFeaturesKey, teamID) mdmConfigKey := fmt.Sprintf(teamMDMConfigKey, teamID) - teamNameKey := fmt.Sprintf(teamNameByIdKey, teamID) ds.c.Delete(agentOptionsKey) ds.c.Delete(featuresKey) ds.c.Delete(mdmConfigKey) - ds.c.Delete(teamNameKey) return nil } -func (ds *cachedMysql) GetTeamName(ctx context.Context, teamID uint) (*string, error) { - key := fmt.Sprintf(teamNameByIdKey, teamID) - if x, found := ds.c.Get(key); found { - if teamName, ok := x.(*string); ok { - return teamName, nil - } - } - - teamName, err := ds.Datastore.GetTeamName(ctx, teamID) - if err != nil { - return nil, err - } - ds.c.Set(key, teamName, ds.scheduledQueriesExp) - return teamName, nil -} - func (ds *cachedMysql) ListScheduledQueriesForAgents(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { var teamIDVal uint if teamID != nil { diff --git a/server/datastore/cached_mysql/cached_mysql_test.go b/server/datastore/cached_mysql/cached_mysql_test.go index fcbde32d22..fb26735fb7 100644 --- a/server/datastore/cached_mysql/cached_mysql_test.go +++ b/server/datastore/cached_mysql/cached_mysql_test.go @@ -539,60 +539,6 @@ func TestCachedTeamMDMConfig(t *testing.T) { require.Error(t, err) } -func TestCachedGetTeamName(t *testing.T) { - t.Parallel() - - ctx := context.Background() - - mockedDS := new(mock.Store) - ds := New(mockedDS, WithScheduledQueriesExpiration(100*time.Millisecond)) - - team := fleet.Team{ - ID: 1, - CreatedAt: time.Now(), - Name: "test", - } - - deleted := false - mockedDS.GetTeamNameFunc = func(ctx context.Context, teamID uint) (*string, error) { - if deleted { - return nil, errors.New("not found") - } - return &team.Name, nil - } - mockedDS.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) { - return team, nil - } - mockedDS.DeleteTeamFunc = func(ctx context.Context, teamID uint) error { - deleted = true - return nil - } - - // updating updates the cache - result, err := ds.GetTeamName(ctx, 1) - require.NoError(t, err) - require.Equal(t, team.Name, *result) - - updatedTeam := &fleet.Team{ - ID: team.ID, - CreatedAt: team.CreatedAt, - Name: "test II", - } - _, err = ds.SaveTeam(ctx, updatedTeam) - require.NoError(t, err) - - result, err = ds.GetTeamName(ctx, team.ID) - require.NoError(t, err) - require.Equal(t, updatedTeam.Name, *result) - - // deleting updates the cache - err = ds.DeleteTeam(ctx, team.ID) - require.NoError(t, err) - - _, err = ds.GetTeamName(ctx, team.ID) - require.Error(t, err) -} - func TestCachedListScheduledQueriesForAgents(t *testing.T) { t.Parallel() @@ -606,14 +552,14 @@ func TestCachedListScheduledQueriesForAgents(t *testing.T) { { ID: 1, Name: "test", - ScheduleInterval: 100, + Interval: 100, AutomationsEnabled: true, TeamID: teamID, }, { ID: 2, Name: "test II", - ScheduleInterval: 100, + Interval: 100, AutomationsEnabled: true, TeamID: teamID, }, 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/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 128bdefd8b..ecba75d79e 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -299,20 +299,21 @@ func testSaveHostPackStatsDB(t *testing.T, ds *Datastore) { 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, }, } @@ -326,36 +327,38 @@ func testSaveHostPackStatsDB(t *testing.T, ds *Datastore) { 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(), @@ -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,6 +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})) + 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,36 +608,61 @@ func testHostsWithTeamPackStats(t *testing.T, ds *Datastore) { 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, }, } - packStats := []fleet.PackStats{ - {PackID: pack1.ID, PackName: pack1.Name, QueryStats: stats1}, + stats2 := []fleet.ScheduledQueryStats{ + { + 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, + }, } - err = ds.SaveHostPackStats(context.Background(), host.ID, packStats) + + packStats := []fleet.PackStats{ + {PackName: pack1.Name, QueryStats: stats1}, + {PackName: fmt.Sprintf("team-%d", team.ID), QueryStats: stats2}, + } + err = ds.SaveHostPackStats(context.Background(), host.TeamID, host.ID, packStats) require.NoError(t, err) host, err = ds.Host(context.Background(), host.ID) require.NoError(t, err) - require.Len(t, host.PackStats, 1) + require.Len(t, host.PackStats, 2) sort.Sort(packStatsSlice(host.PackStats)) - assert.Equal(t, host.PackStats[0].PackName, pack1.Name) - assert.ElementsMatch(t, host.PackStats[0].QueryStats, stats1) + 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) } type packStatsSlice []fleet.PackStats @@ -3436,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) @@ -3793,7 +3850,7 @@ func testHostsPackStatsMultipleHosts(t *testing.T, ds *Datastore) { hostPackStats := []fleet.PackStats{ {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) } @@ -3866,11 +3923,15 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { HostIDs: []uint{host1.ID, host2.ID}, }) require.NoError(t, err) - userQuery := test.NewQuery(t, ds, nil, "global-time", "select * from time", 0, true) + 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: userPack.ID, - QueryID: userQuery.ID, + QueryID: userQuery1.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), @@ -3882,7 +3943,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { userSQuery2, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For Darwin only", PackID: userPack.ID, - QueryID: userQuery.ID, + QueryID: userQuery2.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), @@ -3894,7 +3955,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { userSQuery3, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For Darwin and Linux", PackID: userPack.ID, - QueryID: userQuery.ID, + QueryID: userQuery3.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), @@ -3906,7 +3967,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { userSQuery4, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For All Platforms", PackID: userPack.ID, - QueryID: userQuery.ID, + QueryID: userQuery4.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), @@ -3918,7 +3979,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { userSQuery5, err := ds.NewScheduledQuery(context.Background(), &fleet.ScheduledQuery{ Name: "Scheduled Query For All Platforms v2", PackID: userPack.ID, - QueryID: userQuery.ID, + QueryID: userQuery5.ID, Interval: 30, Snapshot: ptr.Bool(true), Removed: ptr.Bool(true), @@ -3937,7 +3998,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { { ScheduledQueryName: userSQuery2.Name, ScheduledQueryID: userSQuery2.ID, - QueryName: userQuery.Name, + QueryName: userQuery2.Name, PackName: userPack.Name, PackID: userPack.ID, AverageMemory: 8001, @@ -3953,7 +4014,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { { ScheduledQueryName: userSQuery3.Name, ScheduledQueryID: userSQuery3.ID, - QueryName: userQuery.Name, + QueryName: userQuery3.Name, PackName: userPack.Name, PackID: userPack.ID, AverageMemory: 8002, @@ -3969,7 +4030,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { { ScheduledQueryName: userSQuery4.Name, ScheduledQueryID: userSQuery4.ID, - QueryName: userQuery.Name, + QueryName: userQuery4.Name, PackName: userPack.Name, PackID: userPack.ID, AverageMemory: 8003, @@ -3985,7 +4046,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { { ScheduledQueryName: userSQuery5.Name, ScheduledQueryID: userSQuery5.ID, - QueryName: userQuery.Name, + QueryName: userQuery5.Name, PackName: userPack.Name, PackID: userPack.ID, AverageMemory: 8003, @@ -4010,7 +4071,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { stats = append(stats, fleet.ScheduledQueryStats{ ScheduledQueryName: userSQuery1.Name, ScheduledQueryID: userSQuery1.ID, - QueryName: userQuery.Name, + QueryName: userQuery1.Name, PackName: userPack.Name, PackID: userPack.ID, AverageMemory: 8003, @@ -4028,7 +4089,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { hostPackStats := []fleet.PackStats{ {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 @@ -4057,7 +4118,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { { ScheduledQueryName: userSQuery1.Name, ScheduledQueryID: userSQuery1.ID, - QueryName: userQuery.Name, + QueryName: userQuery1.Name, PackName: userPack.Name, PackID: userPack.ID, AverageMemory: 0, @@ -4073,7 +4134,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { { ScheduledQueryName: userSQuery3.Name, ScheduledQueryID: userSQuery3.ID, - QueryName: userQuery.Name, + QueryName: userQuery3.Name, PackName: userPack.Name, PackID: userPack.ID, AverageMemory: 0, @@ -4089,7 +4150,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { { ScheduledQueryName: userSQuery4.Name, ScheduledQueryID: userSQuery4.ID, - QueryName: userQuery.Name, + QueryName: userQuery4.Name, PackName: userPack.Name, PackID: userPack.ID, AverageMemory: 0, @@ -4105,7 +4166,7 @@ func testHostsPackStatsForPlatform(t *testing.T, ds *Datastore) { { ScheduledQueryName: userSQuery5.Name, ScheduledQueryID: userSQuery5.ID, - QueryName: userQuery.Name, + QueryName: userQuery5.Name, PackName: userPack.Name, PackID: userPack.ID, AverageMemory: 0, @@ -5628,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. diff --git a/server/datastore/mysql/packs.go b/server/datastore/mysql/packs.go index fd17e09bb4..e93786304b 100644 --- a/server/datastore/mysql/packs.go +++ b/server/datastore/mysql/packs.go @@ -442,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 = ''` diff --git a/server/datastore/mysql/packs_test.go b/server/datastore/mysql/packs_test.go index a774e28269..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,10 +30,6 @@ 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}, @@ -421,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{ @@ -622,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))) } } }() @@ -674,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))) } } }() @@ -708,16 +584,6 @@ func testListForHostIncludesOnlyUserPacks(t *testing.T, ds *Datastore) { 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)) - _, err := ds.EnsureGlobalPack(ctx) - require.NoError(t, err) - - team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) - require.NoError(t, err) - - require.NoError(t, ds.AddHostsToTeam(ctx, &team.ID, []uint{h1.ID})) - _, err = ds.EnsureTeamPack(ctx, team.ID) - require.NoError(t, err) - packs, err := ds.ListPacksForHost(ctx, h1.ID) require.Nil(t, err) if assert.Len(t, packs, 1) { diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index f1b9c2b48b..1ea22ef2bf 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -81,9 +81,9 @@ func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries [] q.TeamIDStr(), q.Platform, q.MinOsqueryVersion, - q.ScheduleInterval, + q.Interval, q.AutomationsEnabled, - q.LoggingType, + q.Logging, ) if err != nil { return ctxerr.Wrap(ctx, err, "exec ApplyQueries insert") @@ -180,9 +180,9 @@ func (ds *Datastore) NewQuery( query.TeamIDStr(), query.Platform, query.MinOsqueryVersion, - query.ScheduleInterval, + query.Interval, query.AutomationsEnabled, - query.LoggingType, + query.Logging, ) if err != nil && isDuplicate(err) { @@ -229,9 +229,9 @@ func (ds *Datastore) SaveQuery(ctx context.Context, q *fleet.Query) error { q.TeamIDStr(), q.Platform, q.MinOsqueryVersion, - q.ScheduleInterval, + q.Interval, q.AutomationsEnabled, - q.LoggingType, + q.Logging, q.ID) if err != nil { return ctxerr.Wrap(ctx, err, "updating query") @@ -302,14 +302,21 @@ func (ds *Datastore) Query(ctx context.Context, id uint) (*fleet.Query, error) { q.created_at, q.updated_at, COALESCE(NULLIF(u.name, ''), u.email, '') AS author_name, - COALESCE(u.email, '') AS author_email + 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)) } @@ -355,7 +362,7 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions LEFT JOIN aggregated_stats ag ON (ag.id = q.id AND ag.global_stats = ? AND ag.type = ?) ` - args := []interface{}{false, aggregatedStatsTypeQuery} + args := []interface{}{false, aggregatedStatsTypeScheduledQuery} whereClauses := "WHERE saved = true" if opt.OnlyObserverCanRun { @@ -392,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 diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index 3dfe9028b7..d1e67446ef 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -54,11 +54,11 @@ func testQueriesApply(t *testing.T, ds *Datastore) { Description: "get the foos", Query: "select * from foo", ObserverCanRun: true, - ScheduleInterval: 10, + Interval: 10, Platform: "macos", MinOsqueryVersion: "5.2.1", AutomationsEnabled: true, - LoggingType: "differential", + Logging: "differential", }, { Name: "bar", @@ -254,11 +254,11 @@ func testQueriesSave(t *testing.T, ds *Datastore) { query.Query = "baz" query.ObserverCanRun = true query.TeamID = &team.ID - query.ScheduleInterval = 10 + query.Interval = 10 query.Platform = "macos" query.MinOsqueryVersion = "5.2.1" query.AutomationsEnabled = true - query.LoggingType = "differential" + query.Logging = "differential" err = ds.SaveQuery(context.Background(), query) require.NoError(t, err) @@ -307,7 +307,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { _, 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) @@ -620,17 +620,17 @@ func testListQueriesFiltersByTeamID(t *testing.T, ds *Datastore) { func testListQueriesFiltersByIsScheduled(t *testing.T, ds *Datastore) { q1, err := ds.NewQuery(context.Background(), &fleet.Query{ - Name: "query1", - Query: "select 1;", - Saved: true, - ScheduleInterval: 0, + 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, - ScheduleInterval: 10, + Interval: 10, AutomationsEnabled: false, }) require.NoError(t, err) @@ -638,7 +638,7 @@ func testListQueriesFiltersByIsScheduled(t *testing.T, ds *Datastore) { Name: "query3", Query: "select 1;", Saved: true, - ScheduleInterval: 20, + Interval: 20, AutomationsEnabled: true, }) require.NoError(t, err) @@ -686,18 +686,18 @@ func testListScheduledQueriesForAgents(t *testing.T, ds *Datastore) { teamIDStr = fmt.Sprintf("%d", *teamID) } _, err := ds.NewQuery(context.Background(), &fleet.Query{ - Name: fmt.Sprintf("%s query1", teamIDStr), - Query: "select 1;", - Saved: true, - ScheduleInterval: 0, - TeamID: teamID, + 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, - ScheduleInterval: 10, + Interval: 10, AutomationsEnabled: false, TeamID: teamID, }) @@ -706,7 +706,7 @@ func testListScheduledQueriesForAgents(t *testing.T, ds *Datastore) { Name: fmt.Sprintf("%s query3", teamIDStr), Query: "select 1;", Saved: true, - ScheduleInterval: 20, + Interval: 20, AutomationsEnabled: true, TeamID: teamID, }) @@ -715,7 +715,7 @@ func testListScheduledQueriesForAgents(t *testing.T, ds *Datastore) { Name: fmt.Sprintf("%s query4", teamIDStr), Query: "select 1;", Saved: true, - ScheduleInterval: 0, + Interval: 0, AutomationsEnabled: true, TeamID: teamID, }) 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 41179a8fc6..9231587f99 100644 --- a/server/datastore/mysql/scheduled_queries_test.go +++ b/server/datastore/mysql/scheduled_queries_test.go @@ -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/teams.go b/server/datastore/mysql/teams.go index 3fe764fe6b..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) { @@ -432,17 +418,3 @@ func (ds *Datastore) DeleteIntegrationsFromTeams(ctx context.Context, deletedInt } return rows.Err() } - -func (ds *Datastore) GetTeamName(ctx context.Context, teamID uint) (*string, error) { - stmt := `SELECT name FROM teams WHERE id = ?` - var teamName string - - if err := sqlx.GetContext(ctx, ds.reader(ctx), &teamName, stmt, teamID); err != nil { - if err == sql.ErrNoRows { - return nil, ctxerr.Wrap(ctx, notFound("Team").WithID(teamID)) - } - return nil, ctxerr.Wrap(ctx, err, "select team") - } - - return &teamName, nil -} diff --git a/server/datastore/mysql/teams_test.go b/server/datastore/mysql/teams_test.go index 858fa65acb..47273c9ea3 100644 --- a/server/datastore/mysql/teams_test.go +++ b/server/datastore/mysql/teams_test.go @@ -31,11 +31,9 @@ func TestTeams(t *testing.T) { {"Search", testTeamsSearch}, {"EnrollSecrets", testTeamsEnrollSecrets}, {"TeamAgentOptions", testTeamsAgentOptions}, - {"TeamsDeleteRename", testTeamsDeleteRename}, {"DeleteIntegrationsFromTeams", testTeamsDeleteIntegrationsFromTeams}, {"TeamsFeatures", testTeamsFeatures}, {"TeamsMDMConfig", testTeamsMDMConfig}, - {"GetTeamByName", testGetTeamByName}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -107,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} @@ -625,24 +594,3 @@ func testTeamsMDMConfig(t *testing.T, ds *Datastore) { }, mdm) }) } - -func testGetTeamByName(t *testing.T, ds *Datastore) { - ctx := context.Background() - - t.Run("team does not exists", func(t *testing.T) { - r, err := ds.GetTeamName(ctx, 123) - require.Nil(t, r) - require.Error(t, err) - }) - - t.Run("returns the team name", func(t *testing.T) { - team, err := ds.NewTeam(ctx, &fleet.Team{ - Name: "team1", - }) - require.NoError(t, err) - - result, err := ds.GetTeamName(ctx, team.ID) - require.NoError(t, err) - require.Equal(t, team.Name, *result) - }) -} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index ccace9adb6..70639dcbb3 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -146,12 +146,6 @@ type Datastore interface { // 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 @@ -400,8 +394,6 @@ type Datastore interface { SaveTeam(ctx context.Context, team *Team) (*Team, error) // Team retrieves the Team by ID. Team(ctx context.Context, tid uint) (*Team, error) - // GetTeamName retrieves the team name by their ID. - GetTeamName(ctx context.Context, teamID uint) (*string, error) // Team deletes the Team by ID. DeleteTeam(ctx context.Context, tid uint) error // TeamByName retrieves the Team by Name. @@ -592,7 +584,6 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // Aggregated Stats - UpdateScheduledQueryAggregatedStats(ctx context.Context) error UpdateQueryAggregatedStats(ctx context.Context) error /////////////////////////////////////////////////////////////////////////////// @@ -631,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 5d450789c2..7a6d515635 100644 --- a/server/fleet/queries.go +++ b/server/fleet/queries.go @@ -4,18 +4,47 @@ 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"` @@ -27,16 +56,19 @@ type Query struct { // 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. - ScheduleInterval uint `json:"interval" db:"schedule_interval"` + 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"` - // LoggingType the type of log output for this query - LoggingType string `json:"logging" db:"logging_type"` + // 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"` @@ -54,10 +86,19 @@ 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" } @@ -71,12 +112,12 @@ func (q *Query) TeamIDStr() string { } func (q *Query) GetSnapshot() *bool { - var loggingType string + var logging string if q != nil { - loggingType = q.LoggingType + logging = q.Logging } - switch loggingType { + switch logging { case "snapshot": return ptr.Bool(true) default: @@ -85,12 +126,12 @@ func (q *Query) GetSnapshot() *bool { } func (q *Query) GetRemoved() *bool { - var loggingType string + var logging string if q != nil { - loggingType = q.LoggingType + logging = q.Logging } - switch loggingType { + switch logging { case "differential": return ptr.Bool(true) case "differential_ignore_removals": @@ -112,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 } @@ -123,13 +169,16 @@ 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.ScheduleInterval, + Interval: q.Interval, Platform: &q.Platform, Version: &q.MinOsqueryVersion, Removed: q.GetRemoved(), @@ -149,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 { @@ -165,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" ) @@ -174,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) { @@ -194,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, + }, ) } @@ -224,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 9786ff5b12..8975a35132 100644 --- a/server/fleet/queries_test.go +++ b/server/fleet/queries_test.go @@ -18,15 +18,15 @@ func TestGetSnapshot(t *testing.T) { expected: nil, }, { - query: &Query{LoggingType: "snapshot"}, + query: &Query{Logging: "snapshot"}, expected: ptr.Bool(true), }, { - query: &Query{LoggingType: "differential"}, + query: &Query{Logging: "differential"}, expected: nil, }, { - query: &Query{LoggingType: "differential_ignore_removals"}, + query: &Query{Logging: "differential_ignore_removals"}, expected: nil, }, } @@ -45,15 +45,15 @@ func TestGetRemoved(t *testing.T) { expected: nil, }, { - query: &Query{LoggingType: "snapshot"}, + query: &Query{Logging: "snapshot"}, expected: nil, }, { - query: &Query{LoggingType: "differential"}, + query: &Query{Logging: "differential"}, expected: ptr.Bool(true), }, { - query: &Query{LoggingType: "differential_ignore_removals"}, + query: &Query{Logging: "differential_ignore_removals"}, expected: ptr.Bool(false), }, } 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 98cb462ba5..53a3293a36 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -253,17 +253,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 24cd9f45e2..7250ab6da9 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -110,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) @@ -302,8 +298,6 @@ type SaveTeamFunc func(ctx context.Context, team *fleet.Team) (*fleet.Team, erro type TeamFunc func(ctx context.Context, tid uint) (*fleet.Team, error) -type GetTeamNameFunc func(ctx context.Context, teamID uint) (*string, error) - type DeleteTeamFunc func(ctx context.Context, tid uint) error type TeamByNameFunc func(ctx context.Context, name string) (*fleet.Team, error) @@ -432,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) @@ -450,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) @@ -803,12 +795,6 @@ type DataStore struct { ListPacksForHostFunc ListPacksForHostFunc ListPacksForHostFuncInvoked bool - EnsureGlobalPackFunc EnsureGlobalPackFunc - EnsureGlobalPackFuncInvoked bool - - EnsureTeamPackFunc EnsureTeamPackFunc - EnsureTeamPackFuncInvoked bool - ApplyLabelSpecsFunc ApplyLabelSpecsFunc ApplyLabelSpecsFuncInvoked bool @@ -1091,9 +1077,6 @@ type DataStore struct { TeamFunc TeamFunc TeamFuncInvoked bool - GetTeamNameFunc GetTeamNameFunc - GetTeamNameFuncInvoked bool - DeleteTeamFunc DeleteTeamFunc DeleteTeamFuncInvoked bool @@ -1286,9 +1269,6 @@ type DataStore struct { CleanupCronStatsFunc CleanupCronStatsFunc CleanupCronStatsFuncInvoked bool - UpdateScheduledQueryAggregatedStatsFunc UpdateScheduledQueryAggregatedStatsFunc - UpdateScheduledQueryAggregatedStatsFuncInvoked bool - UpdateQueryAggregatedStatsFunc UpdateQueryAggregatedStatsFunc UpdateQueryAggregatedStatsFuncInvoked bool @@ -1959,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 @@ -2631,13 +2597,6 @@ func (s *DataStore) Team(ctx context.Context, tid uint) (*fleet.Team, error) { return s.TeamFunc(ctx, tid) } -func (s *DataStore) GetTeamName(ctx context.Context, teamID uint) (*string, error) { - s.mu.Lock() - s.GetTeamNameFuncInvoked = true - s.mu.Unlock() - return s.GetTeamNameFunc(ctx, teamID) -} - func (s *DataStore) DeleteTeam(ctx context.Context, tid uint) error { s.mu.Lock() s.DeleteTeamFuncInvoked = true @@ -3086,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 @@ -3149,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 784919c8ec..699867c340 100644 --- a/server/service/async/async_scheduled_query_stats_test.go +++ b/server/service/async/async_scheduled_query_stats_test.go @@ -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 255fbe9a7f..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 @@ -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 16da3b4e2f..f4572f43e2 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -443,24 +443,16 @@ func (svc *Service) GetClientConfig(ctx context.Context) (map[string]interface{} } if host.TeamID != nil { - teamName, err := svc.ds.GetTeamName(ctx, *host.TeamID) + teamQueries, err := svc.getScheduledQueries(ctx, host.TeamID) if err != nil { return nil, newOsqueryError("database error: " + err.Error()) } - - if teamName != 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: %s", *teamName) - packConfig[packName] = fleet.PackContent{ - Queries: teamQueries, - } + if len(teamQueries) > 0 { + packName := fmt.Sprintf("team-%d", *host.TeamID) + packConfig[packName] = fleet.PackContent{ + Queries: teamQueries, } } - } if len(packConfig) > 0 { diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 0b014ec45e..e01e0b4256 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -40,10 +40,6 @@ import ( func TestGetClientConfig(t *testing.T) { ds := new(mock.Store) - ds.GetTeamNameFunc = func(ctx context.Context, tid uint) (*string, error) { - teamName := "Alamo" - return &teamName, nil - } ds.TeamAgentOptionsFunc = func(ctx context.Context, teamID uint) (*json.RawMessage, error) { return nil, nil @@ -77,19 +73,19 @@ func TestGetClientConfig(t *testing.T) { { Query: "SELECT 1 FROM table_1", Name: "Some strings carry more weight than others", - ScheduleInterval: 10, + Interval: 10, Platform: "linux", MinOsqueryVersion: "5.12.2", - LoggingType: "snapshot", + Logging: "snapshot", TeamID: ptr.Uint(1), }, { - Query: "SELECT 1 FROM table_2", - Name: "You shall not pass", - ScheduleInterval: 20, - Platform: "macos", - LoggingType: "differential", - 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 } @@ -193,7 +189,7 @@ func TestGetClientConfig(t *testing.T) { "froobing":{"query":"select 'guacamole'","interval":60,"snapshot":true} } }, - "Team: Alamo": { + "team-1": { "queries": { "Some strings carry more weight than others": { "query": "SELECT 1 FROM table_1", @@ -2004,10 +2000,6 @@ func TestUpdateHostIntervals(t *testing.T) { svc, ctx := newTestService(t, ds, nil, nil) - ds.GetTeamNameFunc = func(ctx context.Context, tid uint) (*string, error) { - return nil, nil - } - ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { return nil, nil } @@ -2015,6 +2007,9 @@ func TestUpdateHostIntervals(t *testing.T) { 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 cc71149337..6bed566e1f 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 c2e849cfed..187a4e1bd8 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 e7009d8ff9..43f7384de8 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,20 +68,31 @@ 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 } @@ -86,6 +102,8 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions) ([]* queries, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{ ListOptions: opt, OnlyObserverCanRun: onlyShowObserverCanRun, + TeamID: teamID, + IsScheduled: scheduled, }) if err != nil { return nil, err @@ -135,12 +153,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 +166,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 +254,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 +273,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 +324,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 +336,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, nil, 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, nil, name); err != nil { + if err := svc.ds.DeleteQuery(ctx, teamID, name); err != nil { return err } @@ -348,17 +398,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, nil, 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 +449,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 +479,7 @@ func (svc *Service) DeleteQueries(ctx context.Context, ids []uint) (uint, error) } //////////////////////////////////////////////////////////////////////////////// -// Apply Query Spec +// Apply Query Specs //////////////////////////////////////////////////////////////////////////////// type applyQuerySpecsRequest struct { @@ -452,38 +502,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, nil, 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 +545,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 +578,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 +648,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, nil, 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 35f710661d..90ee225722 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -63,7 +63,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,31 +74,100 @@ 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, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) { - if name == authoredQueryName { - return &fleet.Query{ID: 99, AuthorID: ptr.Uint(teamMaintainer.ID)}, nil + 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 @@ -125,65 +194,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 +382,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/new_objects.go b/server/test/new_objects.go index f736c7dc51..407256527b 100644 --- a/server/test/new_objects.go +++ b/server/test/new_objects.go @@ -12,17 +12,19 @@ import ( "github.com/stretchr/testify/require" ) -func NewQuery(t *testing.T, ds fleet.Datastore, teamID *uint, 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, - TeamID: teamID, + Name: name, + Query: q, + AuthorID: authorPtr, + Saved: saved, + TeamID: teamID, + Interval: interval, + AutomationsEnabled: automationsEnabled, }) require.NoError(t, err) @@ -33,6 +35,10 @@ func NewQuery(t *testing.T, ds fleet.Datastore, teamID *uint, name, q string, au 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) From a7f6686c27c97e760fd74217fee2848ac06f13c0 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Wed, 26 Jul 2023 15:33:21 -0400 Subject: [PATCH 61/78] Don't leak Observer permissions between teams for queries. (#12979) If user is an observer in a team, only show the queries for which observer_can_run is set. --- server/service/queries.go | 18 +++------ server/service/queries_test.go | 68 ++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/server/service/queries.go b/server/service/queries.go index 43f7384de8..a911790e90 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -97,7 +97,7 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team } user := authz.UserFromContext(ctx) - onlyShowObserverCanRun := onlyShowObserverCanRunQueries(user) + onlyShowObserverCanRun := onlyShowObserverCanRunQueries(user, teamID) queries, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{ ListOptions: opt, @@ -112,20 +112,14 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team 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] } //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 90ee225722..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) { From 6f77911ffefa743eb3171483eabc85dbfff677cf Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Wed, 26 Jul 2023 17:13:27 -0400 Subject: [PATCH 62/78] Fix performance regression found in load testing (#12981) --- cmd/fleet/serve.go | 4 +- go.mod | 23 ++-- go.sum | 119 +++--------------- server/datastore/cached_mysql/cached_mysql.go | 23 ---- .../cached_mysql/cached_mysql_test.go | 35 ------ .../20230726115701_AddQueriesIndices.go | 26 ++++ server/datastore/mysql/schema.sql | 7 +- 7 files changed, 56 insertions(+), 181 deletions(-) create mode 100644 server/datastore/mysql/migrations/tables/20230726115701_AddQueriesIndices.go 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/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/datastore/cached_mysql/cached_mysql.go b/server/datastore/cached_mysql/cached_mysql.go index 80c2820d2d..850646352f 100644 --- a/server/datastore/cached_mysql/cached_mysql.go +++ b/server/datastore/cached_mysql/cached_mysql.go @@ -25,8 +25,6 @@ const ( defaultTeamFeaturesExpiration = 1 * time.Minute teamMDMConfigKey = "TeamMDMConfig:team:%d" defaultTeamMDMConfigExpiration = 1 * time.Minute - // scheduledQueriesForAgentsKey uses defaultScheduledQueriesExpiration for expiration. - scheduledQueriesForAgentsKey = "ScheduledQueriesAgents:team:%d" ) // cloner represents any type that can clone itself. Used by types to provide a more efficient clone method. @@ -322,24 +320,3 @@ func (ds *cachedMysql) DeleteTeam(ctx context.Context, teamID uint) error { return nil } - -func (ds *cachedMysql) ListScheduledQueriesForAgents(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { - var teamIDVal uint - if teamID != nil { - teamIDVal = *teamID - } - - key := fmt.Sprintf(scheduledQueriesForAgentsKey, teamIDVal) - if x, found := ds.c.Get(key); found { - if queries, ok := x.([]*fleet.Query); ok { - return queries, nil - } - } - - queries, err := ds.Datastore.ListScheduledQueriesForAgents(ctx, teamID) - if err != nil { - return nil, err - } - ds.c.Set(key, queries, ds.scheduledQueriesExp) - return queries, nil -} diff --git a/server/datastore/cached_mysql/cached_mysql_test.go b/server/datastore/cached_mysql/cached_mysql_test.go index fb26735fb7..0417987931 100644 --- a/server/datastore/cached_mysql/cached_mysql_test.go +++ b/server/datastore/cached_mysql/cached_mysql_test.go @@ -12,7 +12,6 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" - "github.com/fleetdm/fleet/v4/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -538,37 +537,3 @@ func TestCachedTeamMDMConfig(t *testing.T) { _, err = ds.TeamMDMConfig(context.Background(), testTeam.ID) require.Error(t, err) } - -func TestCachedListScheduledQueriesForAgents(t *testing.T) { - t.Parallel() - - ctx := context.Background() - - mockedDS := new(mock.Store) - ds := New(mockedDS, WithScheduledQueriesExpiration(100*time.Millisecond)) - - teamID := ptr.Uint(1) - scheduledQueries := []*fleet.Query{ - { - ID: 1, - Name: "test", - Interval: 100, - AutomationsEnabled: true, - TeamID: teamID, - }, - { - ID: 2, - Name: "test II", - Interval: 100, - AutomationsEnabled: true, - TeamID: teamID, - }, - } - mockedDS.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.Query, error) { - return scheduledQueries, nil - } - - result, err := ds.ListScheduledQueriesForAgents(ctx, teamID) - require.NoError(t, err) - test.QueryElementsMatch(t, result, scheduledQueries) -} 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/schema.sql b/server/datastore/mysql/schema.sql index 3ca3d2e285..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=200 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'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,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` ( @@ -1026,8 +1026,9 @@ CREATE TABLE `queries` ( `logging_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'snapshot', PRIMARY KEY (`id`), UNIQUE KEY `idx_team_id_name_unq` (`team_id_char`,`name`), + UNIQUE KEY `idx_name_team_id_unq` (`name`,`team_id_char`), KEY `author_id` (`author_id`), - KEY `fk_queries_team_id` (`team_id`), + 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; From e4cc0c309879778337f7dfaa9c8a853cd6b4e149 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 27 Jul 2023 09:29:09 -0400 Subject: [PATCH 63/78] Fixed format issues with fleetctl get queries (#12983) Output from `fleetctl get queries` should include the team the query is in and also the scheduling information. --- cmd/fleetctl/get.go | 108 +++++++++++++++++++++----- cmd/fleetctl/get_test.go | 160 ++++++++++++++++++++++++++++++--------- 2 files changed, 211 insertions(+), 57 deletions(-) diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index f512ea26cb..54083b927b 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -18,6 +18,7 @@ import ( "gopkg.in/guregu/null.v3" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service" "github.com/ghodss/yaml" "github.com/olekukonko/tablewriter" @@ -298,6 +299,50 @@ 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 numberInheritedQueries(client *service.Client, teamID *uint) (*int, error) { + if teamID != nil { + globalQueries, err := client.GetQueries(nil) + if err != nil { + return nil, fmt.Errorf("could not list global queries: %w", err) + } + return ptr.Int(len(globalQueries)), nil + } + return nil, nil +} + func getQueriesCommand() *cli.Command { return &cli.Command{ Name: "queries", @@ -323,11 +368,24 @@ func getQueriesCommand() *cli.Command { name := c.Args().First() 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 all queries + // if name wasn't provided, list either all global queries or all team queries... if name == "" { queries, err := client.GetQueries(teamID) if err != nil { @@ -359,17 +417,12 @@ func getQueriesCommand() *cli.Command { } if len(queries) == 0 { - fmt.Println("No queries found") - return nil - } - - var teamName string - if teamID != nil { - team, err := client.GetTeam(*teamID) - if err != nil { - return fmt.Errorf("get team: %w", err) + scope := "global" + if teamID != nil { + scope = "team" } - teamName = team.Name + fmt.Printf("No %s queries found.\n", scope) + return nil } if c.Bool(yamlFlagName) || c.Bool(jsonFlagName) { @@ -392,18 +445,21 @@ func getQueriesCommand() *cli.Command { } } 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) + // Need to determine the number of inherited queries if we are viewing the + // queries for a team + nInheritedQueries, err := numberInheritedQueries(client, teamID) + if err != nil { + return err + } + + printQueryTable(c, columns, rows, nInheritedQueries) } return nil } @@ -520,10 +576,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() } @@ -1021,6 +1075,18 @@ func getUserRolesCommand() *cli.Command { } } +func printQueryTable(c *cli.Context, columns []string, data [][]string, nInheritedQueries *int) { + table := defaultTable(c.App.Writer) + table.SetHeader(columns) + table.SetReflowDuringAutoWrap(false) + table.AppendBulk(data) + table.Render() + + if nInheritedQueries != nil { + fmt.Printf("Not showing %d inherited queries. To see global queries, run this command without the `--team` flag.\n", *nInheritedQueries) + } +} + 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 7a1f0afe23..5f0f7a3a68 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -1053,16 +1053,42 @@ func TestGetQueries(t *testing.T) { return nil, errors.New("invalid team ID") } - expectedGlobal := `+--------+-------------+-----------+ -| NAME | DESCRIPTION | QUERY | -+--------+-------------+-----------+ -| query1 | some desc | select 1; | -+--------+-------------+-----------+ -| query2 | some desc 2 | select 2; | -+--------+-------------+-----------+ -| query4 | some desc 4 | select 4; | -+--------+-------------+-----------+ + 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 | ++--------+-------------+-----------+-----------+--------------------------------+ ` + expectedYAMLGlobal := `--- apiVersion: v1 kind: query @@ -1111,12 +1137,21 @@ spec: {"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"}} ` - expectedTeam := `+--------+-------------+-----------+ -| NAME | DESCRIPTION | QUERY | -+--------+-------------+-----------+ -| query3 | some desc 3 | select 3; | -+--------+-------------+-----------+ + 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 @@ -1149,7 +1184,12 @@ spec: } 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.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { if tid == 1 { @@ -1339,11 +1379,19 @@ 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 @@ -1388,15 +1436,39 @@ 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 @@ -1498,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"})) } From 1dde10d5c34411599bb2bd07271e1c3fe882e0b8 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 27 Jul 2023 15:08:40 -0400 Subject: [PATCH 64/78] Print inherited message if no queries found (#12998) If the team has no queries, then fleetctl output should also show the number of inherited queries. --- cmd/fleetctl/get.go | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 54083b927b..94ed9f06c0 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -18,7 +18,6 @@ import ( "gopkg.in/guregu/null.v3" "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service" "github.com/ghodss/yaml" "github.com/olekukonko/tablewriter" @@ -332,15 +331,23 @@ func queryToTableRow(query fleet.Query, teamName string) []string { } } -func numberInheritedQueries(client *service.Client, teamID *uint) (*int, error) { +func printInheritedQueries(client *service.Client, teamID *uint) error { if teamID != nil { globalQueries, err := client.GetQueries(nil) if err != nil { - return nil, fmt.Errorf("could not list global queries: %w", err) + return fmt.Errorf("could not list global queries: %w", err) } - return ptr.Int(len(globalQueries)), nil + fmt.Printf("Not showing %d inherited queries. To see global queries, run this command without the `--team` flag.\n", len(globalQueries)) } - return nil, nil + return nil +} + +func printNoQueriesFound(teamID *uint) { + scope := "global" + if teamID != nil { + scope = "team" + } + fmt.Printf("No %s queries found.\n", scope) } func getQueriesCommand() *cli.Command { @@ -417,11 +424,10 @@ func getQueriesCommand() *cli.Command { } if len(queries) == 0 { - scope := "global" - if teamID != nil { - scope = "team" + printNoQueriesFound(teamID) + if err := printInheritedQueries(client, teamID); err != nil { + return err } - fmt.Printf("No %s queries found.\n", scope) return nil } @@ -452,14 +458,10 @@ func getQueriesCommand() *cli.Command { rows = append(rows, queryToTableRow(query, teamName)) } - // Need to determine the number of inherited queries if we are viewing the - // queries for a team - nInheritedQueries, err := numberInheritedQueries(client, teamID) - if err != nil { + printQueryTable(c, columns, rows) + if err := printInheritedQueries(client, teamID); err != nil { return err } - - printQueryTable(c, columns, rows, nInheritedQueries) } return nil } @@ -1075,16 +1077,12 @@ func getUserRolesCommand() *cli.Command { } } -func printQueryTable(c *cli.Context, columns []string, data [][]string, nInheritedQueries *int) { +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() - - if nInheritedQueries != nil { - fmt.Printf("Not showing %d inherited queries. To see global queries, run this command without the `--team` flag.\n", *nInheritedQueries) - } } func printTable(c *cli.Context, columns []string, data [][]string) { From 266e9bf2e05ef87a9b429d3bfa81d01f1e4cdd6e Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 27 Jul 2023 12:10:22 -0700 Subject: [PATCH 65/78] =?UTF-8?q?UI=20=E2=80=93=20Update=20platforms=20col?= =?UTF-8?q?umn=20to=20only=20display=20compatible=20platforms=20(#13003)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #12999 Screenshot 2023-07-27 at 11 59 01 AM # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/` - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- changes/12999-platforms-column | 1 + .../DataTable/PlatformCell/PlatformCell.tsx | 5 ++++- frontend/interfaces/schedulable_query.ts | 7 ++++++- .../ManageQueriesPage/ManageQueriesPage.tsx | 13 +++--------- .../components/QueriesTable/QueriesTable.tsx | 2 +- .../QueriesTable/QueriesTableConfig.tsx | 21 +++++++------------ 6 files changed, 22 insertions(+), 27 deletions(-) create mode 100644 changes/12999-platforms-column 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/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx index 43cbef73bf..61a875cd5f 100644 --- a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx +++ b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx @@ -1,6 +1,7 @@ import React from "react"; import Icon from "components/Icon"; import { SupportedPlatform } from "interfaces/platform"; +import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; interface IPlatformCellProps { platforms: SupportedPlatform[]; @@ -42,7 +43,9 @@ const PlatformCell = ({ platforms }: IPlatformCellProps): JSX.Element => { ) : null; }) ) : ( - --- + + {DEFAULT_EMPTY_CELL_VALUE} + )} ); diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index 6568689711..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 { SelectedPlatformString } from "./platform"; +import { SelectedPlatformString, SupportedPlatform } from "./platform"; // Query itself export interface ISchedulableQuery { @@ -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; diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 1884828cd8..58e124b908 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -16,12 +16,12 @@ import { performanceIndicator } from "utilities/helpers"; 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"; @@ -52,17 +52,10 @@ interface IManageQueriesPageProps { }; } -interface IEnhancedQuery extends ISchedulableQuery { - performance: string; - platforms: SupportedPlatform[] | typeof DEFAULT_EMPTY_CELL_VALUE[]; -} - -const getPlatforms = ( - queryString: string -): SupportedPlatform[] | typeof DEFAULT_EMPTY_CELL_VALUE[] => { +const getPlatforms = (queryString: string): SupportedPlatform[] => { const { platforms } = checkPlatformCompatibility(queryString); - return platforms || [DEFAULT_EMPTY_CELL_VALUE]; + return platforms ?? []; }; const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index abb55b687e..f5ad0b06f6 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -285,7 +285,7 @@ const QueriesTable = ({ variant: "text-icon", onActionButtonClick: onDeleteQueryClick, }} - selectedDropdownFilter={platform} + selectedDropdownFilter={!isInherited ? platform : undefined} /> ) : ( diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index b8ab7431ff..744a829bc6 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -9,7 +9,10 @@ import PATHS from "router/paths"; import permissionsUtils from "utilities/permissions"; import { IUser } from "interfaces/user"; import { secondsToDhms } from "utilities/helpers"; -import { ISchedulableQuery } from "interfaces/schedulable_query"; +import { + IEnhancedQuery, + ISchedulableQuery, +} from "interfaces/schedulable_query"; import { SupportedPlatform } from "interfaces/platform"; import Icon from "components/Icon"; @@ -48,7 +51,7 @@ interface IHeaderProps { } interface IRowProps { row: { - original: ISchedulableQuery; + original: IEnhancedQuery; getToggleRowSelectedProps: () => IGetToggleAllRowsSelectedProps; toggleRowSelected: () => void; }; @@ -108,9 +111,6 @@ const generateTableHeaders = ({ isInherited = false, }: IGenerateTableHeaders): IDataColumn[] => { const isOnlyObserver = permissionsUtils.isOnlyObserver(currentUser); - const isAnyTeamMaintainerOrTeamAdmin = permissionsUtils.isAnyTeamMaintainerOrTeamAdmin( - currentUser - ); const tableHeaders: IDataColumn[] = [ { @@ -163,18 +163,11 @@ const generateTableHeaders = ({ }, { title: "Platform", - Header: "Platform", + Header: "Compatible with", disableSortBy: true, accessor: "platforms", Cell: (cellProps: IPlatformCellProps): JSX.Element => { - // translate the SelectedPlatformString into an array of `SupportedPlatform`s - const platformIconsToRender = (cellProps.row.original.platform === "" - ? ["darwin", "windows", "linux", "chrome"] - : cellProps.row.original.platform - ?.split(",") - .filter((platform) => platform !== "")) as SupportedPlatform[]; - - return ; + return ; }, }, { From 1daf6f02c644fb35bfde5226d221876e15612558 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Thu, 27 Jul 2023 13:32:24 -0700 Subject: [PATCH 66/78] Revert "Merge front-end changes into 7765 Master Dev branch (#12905)" This reverts commit 7bff7447b049afe1d724b9e00c8f403a219234a3. --- changes/12636-merge-schedule-into-queries | 1 - changes/12645-manage-query-automations | 1 - changes/12646-new-query-editor | 1 - changes/12646-update-save-query-modal | 1 - frontend/__mocks__/scheduleableQueryMock.ts | 39 -- .../LogDestinationIndicator.stories.tsx | 17 - .../LogDestinationIndicator.tsx | 86 --- .../LogDestinationIndicator/index.ts | 1 - frontend/components/Modal/Modal.tsx | 1 - .../PlatformCompatibility.tsx | 10 +- .../PlatformCompatibility/_styles.scss | 1 - .../QueryFrequencyIndicator.stories.tsx | 18 - .../QueryFrequencyIndicator.tsx | 73 --- .../QueryFrequencyIndicator/_styles.scss | 14 - .../QueryFrequencyIndicator/index.ts | 1 - .../StatusIndicator.stories.tsx | 1 + .../StatusIndicator/StatusIndicator.tsx | 80 ++- .../DataTable/PillCell/PillCell.tests.tsx | 4 +- .../DataTable/PillCell/PillCell.tsx | 5 +- .../PlatformCell/PlatformCell.stories.tsx | 2 +- .../PlatformCell/PlatformCell.tests.tsx | 9 +- .../DataTable/PlatformCell/PlatformCell.tsx | 8 +- .../DataTable/TextCell/TextCell.tsx | 29 +- .../TableContainer/DataTable/_styles.scss | 8 +- .../buttons/RevealButton/RevealButton.tsx | 2 +- frontend/components/icons/Clock.tsx | 39 -- frontend/components/icons/Warning.tsx | 33 -- frontend/components/icons/index.ts | 4 - .../QueryTablePlatforms.tsx | 8 +- .../top_nav/SiteTopNav/SiteTopNav.tsx | 3 + .../components/top_nav/SiteTopNav/navItems.ts | 8 + frontend/context/policy.tsx | 10 +- frontend/context/query.tsx | 64 -- frontend/hooks/usePlatformCompatibility.tsx | 4 +- frontend/hooks/usePlatformSelector.tsx | 7 +- frontend/interfaces/osquery_table.ts | 6 +- frontend/interfaces/platform.ts | 28 +- frontend/interfaces/policy.ts | 8 +- frontend/interfaces/query.ts | 27 +- frontend/interfaces/schedulable_query.ts | 19 +- .../pages/DashboardPage/DashboardPage.tsx | 6 +- .../cards/HostsSummary/HostsSummary.tsx | 4 +- .../OperatingSystems/OperatingSystems.tsx | 6 +- .../hosts/ManageHostsPage/HostTableConfig.tsx | 1 + .../HostActionsDropdown.tests.tsx | 16 + .../HostDetailsPage/HostDetailsPage.tsx | 51 +- .../SelectQueryModal/SelectQueryModal.tsx | 6 +- .../details/cards/HostSummary/HostSummary.tsx | 1 + .../cards/Packs/PackTable/PackTableConfig.tsx | 1 + .../PoliciesTable/PoliciesTableConfig.tsx | 3 +- .../components/PolicyForm/PolicyForm.tsx | 8 +- .../SaveNewPolicyModal/SaveNewPolicyModal.tsx | 4 +- frontend/pages/policies/constants.ts | 4 +- .../ManageQueriesPage/ManageQueriesPage.tsx | 413 +++---------- .../queries/ManageQueriesPage/_styles.scss | 153 +++-- .../AutomationsModal.tsx | 19 - .../ManageAutomationsModal/_styles.scss | 65 --- .../ManageAutomationsModal/index.ts | 1 - .../components/QueriesTable/QueriesTable.tsx | 20 +- .../QueriesTable/QueriesTableConfig.tsx | 178 +++--- .../QueryAutomationsStatusIndicator.tsx | 44 -- .../_styles.scss | 21 - .../QueryAutomationsStatusIndicator/index.ts | 1 - .../pages/queries/QueryPage/QueryPage.tsx | 74 +-- .../NewQueryModal/NewQueryModal.tsx | 135 +++++ .../components/NewQueryModal/index.ts | 1 + .../components/QueryForm/QueryForm.tsx | 250 ++------ .../components/QueryForm/_styles.scss | 16 - .../SaveQueryModal/SaveQueryModal.tsx | 257 --------- .../components/SaveQueryModal/_styles.scss | 32 - .../components/SaveQueryModal/index.ts | 1 - .../queries/QueryPage/screens/QueryEditor.tsx | 48 +- .../ManageSchedulePage/ManageSchedulePage.tsx | 546 ++++++++++++++++++ .../schedule/ManageSchedulePage/_styles.scss | 122 ++++ .../PreviewDataModal/PreviewDataModal.tsx | 0 .../components/PreviewDataModal/_styles.scss | 14 + .../components/PreviewDataModal/index.ts | 0 .../RemoveScheduledQueryModal.tsx | 47 ++ .../RemoveScheduledQueryModal/index.ts | 1 + .../ScheduleEditorModal.tsx | 364 ++++++++++++ .../ScheduleEditorModal/_styles.scss | 26 + .../components/ScheduleEditorModal/index.ts | 1 + .../ScheduleTable/ScheduleTable.tsx | 224 +++++++ .../ScheduleTable/ScheduleTableConfig.tsx | 272 +++++++++ .../components/ScheduleTable/index.ts | 1 + .../schedule/ManageSchedulePage/index.ts | 1 + frontend/router/index.tsx | 11 +- frontend/router/paths.ts | 10 +- frontend/services/entities/hosts.ts | 6 +- .../services/entities/operating_systems.ts | 4 +- frontend/services/entities/queries.ts | 34 +- .../services/mock_service/mocks/config.ts | 21 - .../services/mock_service/mocks/responses.ts | 252 -------- frontend/styles/var/colors.scss | 1 - frontend/utilities/constants.ts | 37 +- frontend/utilities/osquery_tables.ts | 2 +- frontend/utilities/sql_tools.ts | 13 +- 97 files changed, 2369 insertions(+), 2162 deletions(-) delete mode 100644 changes/12636-merge-schedule-into-queries delete mode 100644 changes/12645-manage-query-automations delete mode 100644 changes/12646-new-query-editor delete mode 100644 changes/12646-update-save-query-modal delete mode 100644 frontend/__mocks__/scheduleableQueryMock.ts delete mode 100644 frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx delete mode 100644 frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx delete mode 100644 frontend/components/LogDestinationIndicator/index.ts delete mode 100644 frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx delete mode 100644 frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx delete mode 100644 frontend/components/QueryFrequencyIndicator/_styles.scss delete mode 100644 frontend/components/QueryFrequencyIndicator/index.ts delete mode 100644 frontend/components/icons/Clock.tsx delete mode 100644 frontend/components/icons/Warning.tsx delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator.tsx delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/_styles.scss delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/index.ts create mode 100644 frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx create mode 100644 frontend/pages/queries/QueryPage/components/NewQueryModal/index.ts delete mode 100644 frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx delete mode 100644 frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss delete mode 100644 frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts create mode 100644 frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx create mode 100644 frontend/pages/schedule/ManageSchedulePage/_styles.scss rename frontend/pages/{queries/ManageQueriesPage => schedule/ManageSchedulePage}/components/PreviewDataModal/PreviewDataModal.tsx (100%) create mode 100644 frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss rename frontend/pages/{queries/ManageQueriesPage => schedule/ManageSchedulePage}/components/PreviewDataModal/index.ts (100%) create mode 100644 frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx create mode 100644 frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts create mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx create mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss create mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts create mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx create mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx create mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts create mode 100644 frontend/pages/schedule/ManageSchedulePage/index.ts diff --git a/changes/12636-merge-schedule-into-queries b/changes/12636-merge-schedule-into-queries deleted file mode 100644 index bea20bbdda..0000000000 --- a/changes/12636-merge-schedule-into-queries +++ /dev/null @@ -1 +0,0 @@ -- Merged all functionality of the Schedule page into the Queries page diff --git a/changes/12645-manage-query-automations b/changes/12645-manage-query-automations deleted file mode 100644 index 0df3de75f3..0000000000 --- a/changes/12645-manage-query-automations +++ /dev/null @@ -1 +0,0 @@ -- 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 deleted file mode 100644 index 45a8b4427c..0000000000 --- a/changes/12646-new-query-editor +++ /dev/null @@ -1 +0,0 @@ -- Query editor includes frequency and other advanced options diff --git a/changes/12646-update-save-query-modal b/changes/12646-update-save-query-modal deleted file mode 100644 index 8c136872ea..0000000000 --- a/changes/12646-update-save-query-modal +++ /dev/null @@ -1 +0,0 @@ -- Update the save query modal to include scheduling-related fields. diff --git a/frontend/__mocks__/scheduleableQueryMock.ts b/frontend/__mocks__/scheduleableQueryMock.ts deleted file mode 100644 index edc82a61da..0000000000 --- a/frontend/__mocks__/scheduleableQueryMock.ts +++ /dev/null @@ -1,39 +0,0 @@ -// "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 deleted file mode 100644 index a39903f806..0000000000 --- a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 82de7b5d0b..0000000000 --- a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index 1d2d5a12d4..0000000000 --- a/frontend/components/LogDestinationIndicator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./LogDestinationIndicator"; diff --git a/frontend/components/Modal/Modal.tsx b/frontend/components/Modal/Modal.tsx index 0ec2c2149f..4044852d82 100644 --- a/frontend/components/Modal/Modal.tsx +++ b/frontend/components/Modal/Modal.tsx @@ -11,7 +11,6 @@ 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 f278db6f08..2d4ad7516a 100644 --- a/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx +++ b/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx @@ -1,13 +1,13 @@ import React from "react"; -import { OsqueryPlatform } from "interfaces/platform"; +import { IOsqueryPlatform } from "interfaces/platform"; import { PLATFORM_DISPLAY_NAMES } from "utilities/constants"; import TooltipWrapper from "components/TooltipWrapper"; import Icon from "components/Icon"; interface IPlatformCompatibilityProps { - compatiblePlatforms: OsqueryPlatform[] | null; + compatiblePlatforms: IOsqueryPlatform[] | null; error: Error | null; } @@ -18,13 +18,13 @@ const DISPLAY_ORDER = [ "Windows", "Linux", "ChromeOS", -] as OsqueryPlatform[]; +] as IOsqueryPlatform[]; const ERROR_NO_COMPATIBLE_TABLES = Error("no tables in query"); const formatPlatformsForDisplay = ( - compatiblePlatforms: OsqueryPlatform[] -): OsqueryPlatform[] => { + compatiblePlatforms: IOsqueryPlatform[] +): IOsqueryPlatform[] => { return compatiblePlatforms.map((str) => PLATFORM_DISPLAY_NAMES[str] || str); }; diff --git a/frontend/components/PlatformCompatibility/_styles.scss b/frontend/components/PlatformCompatibility/_styles.scss index 38b20a68a1..5a461d45e3 100644 --- a/frontend/components/PlatformCompatibility/_styles.scss +++ b/frontend/components/PlatformCompatibility/_styles.scss @@ -3,7 +3,6 @@ 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 deleted file mode 100644 index df2a8fc374..0000000000 --- a/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 94dfecbdda..0000000000 --- a/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index f5b5f74d01..0000000000 --- a/frontend/components/QueryFrequencyIndicator/_styles.scss +++ /dev/null @@ -1,14 +0,0 @@ -.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 deleted file mode 100644 index 4f84c00133..0000000000 --- a/frontend/components/QueryFrequencyIndicator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./QueryFrequencyIndicator"; diff --git a/frontend/components/StatusIndicator/StatusIndicator.stories.tsx b/frontend/components/StatusIndicator/StatusIndicator.stories.tsx index 24c1eba6cc..f430ad8109 100644 --- a/frontend/components/StatusIndicator/StatusIndicator.stories.tsx +++ b/frontend/components/StatusIndicator/StatusIndicator.stories.tsx @@ -8,6 +8,7 @@ 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 19d8b780d6..7bae7226c2 100644 --- a/frontend/components/StatusIndicator/StatusIndicator.tsx +++ b/frontend/components/StatusIndicator/StatusIndicator.tsx @@ -2,72 +2,58 @@ 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?: { - tooltipText: string | JSX.Element; + id: number; + tooltipText: string; position?: "top" | "bottom"; }; - customIndicatorType?: string; } -const generateIndicatorStateClassTag = ( - rawValue: string, - customIndicatorType?: string -): string => { +const generateClassTag = (rawValue: string): string => { if (rawValue === DEFAULT_EMPTY_CELL_VALUE) { return "indeterminate"; } - const prefix = customIndicatorType ? `${customIndicatorType}-` : ""; - return `${prefix}${rawValue.replace(" ", "-").toLowerCase()}`; + return rawValue.replace(" ", "-").toLowerCase(); }; const StatusIndicator = ({ value, tooltip, - customIndicatorType, }: IStatusIndicatorProps): JSX.Element => { - const indicatorStateClassTag = generateIndicatorStateClassTag( - value, - customIndicatorType - ); - const indicatorClassNames = classnames( + const classTag = generateClassTag(value); + const statusClassName = classnames( "status-indicator", - `status-indicator--${indicatorStateClassTag}`, - `status--${indicatorStateClassTag}` + `status-indicator--${classTag}`, + `status--${classTag}` ); - let indicatorContent; - if (tooltip) { - const tooltipId = uniqueId(); - indicatorContent = ( - <> - - {value} - - - {tooltip.tooltipText} - - - ); - } else { - indicatorContent = <>{value}; - } - return {indicatorContent}; + const indicatorContent = tooltip ? ( + <> + + {value} + + + {tooltip.tooltipText} + + + ) : ( + <>{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 2f6d5eb55e..da2c39d855 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx @@ -8,7 +8,9 @@ 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 8fc83621ca..528858b357 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx @@ -75,8 +75,9 @@ const PillCell = ({ case "Undetermined": return ( <> - To see performance impact, this query must have run with{" "} - automations on {hostDetails ? "this" : "at least one"} host. + To see performance
impact, this query must
run as a + scheduled query
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 90f07ef649..b49cbacc36 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: { - platforms: ["darwin", "windows", "linux"], + value: ["darwin", "windows", "linux"], }, }; diff --git a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx index 8a9129c7c0..683c4ea0b2 100644 --- a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx +++ b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx @@ -1,15 +1,14 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { getByTestId, render, screen, within } from "@testing-library/react"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; -import { SupportedPlatform } from "interfaces/platform"; import PlatformCell from "./PlatformCell"; -const PLATFORMS: SupportedPlatform[] = ["windows", "darwin", "linux", "chrome"]; +const PLATFORMS = ["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"); @@ -24,7 +23,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 61a875cd5f..5c4e895e7b 100644 --- a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx +++ b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx @@ -4,7 +4,7 @@ import { SupportedPlatform } from "interfaces/platform"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; interface IPlatformCellProps { - platforms: SupportedPlatform[]; + value: string[]; } const baseClass = "platform-cell"; @@ -16,7 +16,7 @@ const ICONS: Record = { chrome: "chrome", }; -const DISPLAY_ORDER: SupportedPlatform[] = [ +const DISPLAY_ORDER = [ "darwin", "windows", "linux", @@ -25,7 +25,9 @@ const DISPLAY_ORDER: SupportedPlatform[] = [ // "Invalid query", ]; -const PlatformCell = ({ platforms }: IPlatformCellProps): JSX.Element => { +const PlatformCell = ({ + value: platforms, +}: IPlatformCellProps): JSX.Element => { const orderedList = DISPLAY_ORDER.filter((platform) => platforms.includes(platform) ); diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx index 3678f09b4b..403ab3188c 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx @@ -1,6 +1,4 @@ -import { uniqueId } from "lodash"; import React from "react"; -import ReactTooltip from "react-tooltip"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; interface ITextCellProps { @@ -8,7 +6,6 @@ interface ITextCellProps { formatter?: (val: any) => JSX.Element | string; // string, number, or null greyed?: boolean; classes?: string; - emptyCellTooltipText?: JSX.Element | string; } const TextCell = ({ @@ -16,7 +13,6 @@ const TextCell = ({ formatter = (val) => val, // identity function if no formatter is provided greyed, classes = "w250", - emptyCellTooltipText, }: ITextCellProps): JSX.Element => { let val = value; @@ -26,32 +22,9 @@ 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) || renderEmptyCell()} + {formatter(val) || DEFAULT_EMPTY_CELL_VALUE} ); }; diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index 7ada8b5274..c1ce50aeb8 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -216,11 +216,10 @@ $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); @@ -235,9 +234,6 @@ $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 01b606f892..830e56cebf 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/icons/Clock.tsx b/frontend/components/icons/Clock.tsx deleted file mode 100644 index c739a19aa7..0000000000 --- a/frontend/components/icons/Clock.tsx +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index a3e1ce156c..0000000000 --- a/frontend/components/icons/Warning.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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 bda1176aa3..bb5fe334aa 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -51,8 +51,6 @@ 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"; @@ -112,8 +110,6 @@ 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 c517e21350..228bb18473 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 { OsqueryPlatform } from "interfaces/platform"; +import { IOsqueryPlatform } from "interfaces/platform"; import { PLATFORM_DISPLAY_NAMES } from "utilities/constants"; import Icon from "components/Icon"; interface IPLatformListItemProps { - platform: OsqueryPlatform; + platform: IOsqueryPlatform; } const baseClassListItem = "platform-list-item"; @@ -20,7 +20,7 @@ const PlatformListItem = ({ platform }: IPLatformListItemProps) => { }; // TODO: remove when freebsd is removed -type IPlatformsWithFreebsd = OsqueryPlatform | "freebsd"; +type IPlatformsWithFreebsd = IOsqueryPlatform | "freebsd"; interface IQueryTablePlatformsProps { platforms: IPlatformsWithFreebsd[]; @@ -38,7 +38,7 @@ const QueryTablePlatforms = ({ platforms }: IQueryTablePlatformsProps) => { return ( ); }); diff --git a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx index 7524230c86..5cea8246ed 100644 --- a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx +++ b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx @@ -39,10 +39,13 @@ 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 4b828087ed..203e14f597 100644 --- a/frontend/components/top_nav/SiteTopNav/navItems.ts +++ b/frontend/components/top_nav/SiteTopNav/navItems.ts @@ -77,6 +77,14 @@ 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 ad8c1f43b7..0562242aee 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 { SelectedPlatformString } from "interfaces/platform"; +import { IPlatformString } 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?: SelectedPlatformString | null; + lastEditedQueryPlatform?: IPlatformString | null; defaultPolicy?: boolean; } @@ -55,7 +55,7 @@ type InitialStateType = { lastEditedQueryBody: string; lastEditedQueryResolution: string; lastEditedQueryCritical: boolean; - lastEditedQueryPlatform: SelectedPlatformString | null; + lastEditedQueryPlatform: IPlatformString | 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: SelectedPlatformString | null) => void; + setLastEditedQueryPlatform: (value: IPlatformString | 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: SelectedPlatformString | null | undefined) => { + (lastEditedQueryPlatform: IPlatformString | null | undefined) => { dispatch({ type: ACTIONS.SET_LAST_EDITED_QUERY_INFO, lastEditedQueryPlatform, diff --git a/frontend/context/query.tsx b/frontend/context/query.tsx index 9c3b401c70..b7e2127b91 100644 --- a/frontend/context/query.tsx +++ b/frontend/context/query.tsx @@ -4,8 +4,6 @@ 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; @@ -18,19 +16,11 @@ 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; }; @@ -42,19 +32,11 @@ 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, }; @@ -95,22 +77,6 @@ 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; @@ -129,10 +95,6 @@ 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, @@ -165,32 +127,6 @@ 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 af4c51c452..2185117e4e 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 { OsqueryPlatform, SUPPORTED_PLATFORMS } from "interfaces/platform"; +import { IOsqueryPlatform, 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< - OsqueryPlatform[] | null + IOsqueryPlatform[] | null >(null); const [error, setError] = useState(null); diff --git a/frontend/hooks/usePlatformSelector.tsx b/frontend/hooks/usePlatformSelector.tsx index 912d4e4831..c1e91399d3 100644 --- a/frontend/hooks/usePlatformSelector.tsx +++ b/frontend/hooks/usePlatformSelector.tsx @@ -1,10 +1,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { forEach } from "lodash"; -import { - SelectedPlatformString, - SUPPORTED_PLATFORMS, -} from "interfaces/platform"; +import { IPlatformString, SUPPORTED_PLATFORMS } from "interfaces/platform"; import PlatformSelector from "components/PlatformSelector"; @@ -16,7 +13,7 @@ export interface IPlatformSelector { } const usePlatformSelector = ( - platformContext: SelectedPlatformString | null | undefined, + platformContext: IPlatformString | null | undefined, baseClass = "" ): IPlatformSelector => { const [checkDarwin, setCheckDarwin] = useState(false); diff --git a/frontend/interfaces/osquery_table.ts b/frontend/interfaces/osquery_table.ts index 36f5295e9d..289f5a43aa 100644 --- a/frontend/interfaces/osquery_table.ts +++ b/frontend/interfaces/osquery_table.ts @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import { OsqueryPlatform } from "./platform"; +import { IOsqueryPlatform } from "./platform"; export default PropTypes.shape({ columns: PropTypes.arrayOf( @@ -28,7 +28,7 @@ export interface IQueryTableColumn { hidden: boolean; required: boolean; index: boolean; - platforms?: OsqueryPlatform[]; + platforms?: IOsqueryPlatform[]; requires_user_context?: boolean; } @@ -36,7 +36,7 @@ export interface IOsQueryTable { name: string; description: string; url: string; - platforms: OsqueryPlatform[]; + platforms: IOsqueryPlatform[]; evented: boolean; cacheable: boolean; columns: IQueryTableColumn[]; diff --git a/frontend/interfaces/platform.ts b/frontend/interfaces/platform.ts index 434f07a61f..3dc3d12a42 100644 --- a/frontend/interfaces/platform.ts +++ b/frontend/interfaces/platform.ts @@ -1,4 +1,4 @@ -export type OsqueryPlatform = +export type IOsqueryPlatform = | "darwin" | "macOS" | "windows" @@ -8,17 +8,14 @@ export type OsqueryPlatform = | "chrome" | "ChromeOS"; -export type SupportedPlatform = "darwin" | "windows" | "linux" | "chrome"; +export type ISelectedPlatform = + | "all" + | "darwin" + | "windows" + | "linux" + | "chrome"; -export const SUPPORTED_PLATFORMS: SupportedPlatform[] = [ - "darwin", - "windows", - "linux", - "chrome", -]; -export type SelectedPlatform = SupportedPlatform | "all"; - -export type SelectedPlatformString = +export type IPlatformString = | "" | "darwin" | "windows" @@ -36,8 +33,15 @@ export type SelectedPlatformString = | "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 5183653883..e01fc8e746 100644 --- a/frontend/interfaces/policy.ts +++ b/frontend/interfaces/policy.ts @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import { SelectedPlatformString } from "interfaces/platform"; +import { IPlatformString } 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: SelectedPlatformString; + platform: IPlatformString; 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?: SelectedPlatformString; + platform?: IPlatformString; 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: SelectedPlatformString; + platform: IPlatformString; mdm_required?: boolean; } diff --git a/frontend/interfaces/query.ts b/frontend/interfaces/query.ts index d6a948cd25..cd3b6be1c7 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -1,22 +1,37 @@ +import PropTypes from "prop-types"; import { IFormField } from "./form_field"; -import { IPack } from "./pack"; -import { ISchedulableQuery } from "./schedulable_query"; -import { IScheduledQueryStats } from "./scheduled_query_stats"; +import packInterface, { IPack } from "./pack"; +import scheduledQueryStatsInterface, { + 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: ISchedulableQuery; + query: IQuery; } export interface IFleetQueriesResponse { - queries: ISchedulableQuery[]; + queries: IQuery[]; } export interface IQuery { diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index 9d86c98b85..a5dda7a305 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -12,7 +12,7 @@ export interface ISchedulableQuery { query: string; team_id: number | null; interval: number; - platform: SelectedPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted + platform: IPlatformString; // 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; @@ -51,10 +51,6 @@ 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 { @@ -64,7 +60,7 @@ export interface ICreateQueryRequestBody { observer_can_run?: boolean; team_id?: number; // global query if ommitted interval?: number; // default 0 means never run - platform?: SelectedPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted + platform?: IPlatformString; // 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; @@ -76,14 +72,9 @@ 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? @@ -91,7 +82,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 omitted + team_id?: number; // searches for a global query if ommitted } // Delete a query by id @@ -114,7 +105,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 ec4659c4a5..d1a9febd4b 100644 --- a/frontend/pages/DashboardPage/DashboardPage.tsx +++ b/frontend/pages/DashboardPage/DashboardPage.tsx @@ -21,7 +21,7 @@ import { IMdmSolution, IMdmSummaryResponse, } from "interfaces/mdm"; -import { SelectedPlatform } from "interfaces/platform"; +import { ISelectedPlatform } 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: SelectedPlatform) => { + onChange={(value: ISelectedPlatform) => { 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 a02f4861e4..463f0c0e26 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 { SelectedPlatform } from "interfaces/platform"; +import { ISelectedPlatform } 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?: SelectedPlatform; + selectedPlatform?: ISelectedPlatform; } const HostsSummary = ({ diff --git a/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx b/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx index cf7b085087..f07fabd96f 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 { SelectedPlatform } from "interfaces/platform"; +import { ISelectedPlatform } from "interfaces/platform"; import { getOSVersions, IGetOSVersionsQueryKey, @@ -26,7 +26,7 @@ import generateTableHeaders from "./OperatingSystemsTableConfig"; interface IOperatingSystemsCardProps { currentTeamId: number | undefined; - selectedPlatform: SelectedPlatform; + selectedPlatform: ISelectedPlatform; 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: SelectedPlatform): JSX.Element => ( +const EmptyOperatingSystems = (platform: ISelectedPlatform): 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 3ac219c2e0..6110452dbe 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx @@ -54,6 +54,22 @@ 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 a982fac447..f67a3f13d7 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -18,20 +18,16 @@ 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 { IQueryStats } from "interfaces/query_stats"; import Spinner from "components/Spinner"; import TabsWrapper from "components/TabsWrapper"; @@ -40,6 +36,7 @@ import InfoBanner from "components/InfoBanner"; import BackLink from "components/BackLink"; import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers"; +import permissions from "utilities/permissions"; import HostSummaryCard from "../cards/HostSummary"; import AboutCard from "../cards/About"; @@ -50,6 +47,8 @@ import SoftwareCard from "../cards/Software"; 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"; @@ -62,7 +61,6 @@ 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"; @@ -115,7 +113,9 @@ const HostDetailsPage = ({ const { config, + currentUser, isGlobalAdmin = false, + isGlobalObserver, isPremiumTier = false, isSandboxMode, isOnlyObserver, @@ -151,6 +151,7 @@ const HostDetailsPage = ({ const [refetchStartTime, setRefetchStartTime] = useState(null); const [showRefetchSpinner, setShowRefetchSpinner] = useState(false); + const [packsState, setPacksState] = useState(); const [schedule, setSchedule] = useState(); const [hostSoftware, setHostSoftware] = useState([]); const [usersState, setUsersState] = useState<{ username: string }[]>([]); @@ -158,17 +159,16 @@ const HostDetailsPage = ({ const [pathname, setPathname] = useState(""); const { data: fleetQueries, error: fleetQueriesError } = useQuery< - IListQueriesResponse, + IFleetQueriesResponse, Error, - ISchedulableQuery[], - IQueryKeyQueriesLoadAll[] - >([{ scope: "queries", teamId: undefined }], () => queryAPI.loadAll(), { + IQuery[] + >("fleet queries", () => queryAPI.loadAll(), { enabled: !!hostIdFromURL, refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, retry: false, - select: (data: IListQueriesResponse) => data.queries, + select: (data: IFleetQueriesResponse) => data.queries, }); const { data: teams } = useQuery( @@ -312,6 +312,7 @@ const HostDetailsPage = ({ }, { packs: [], schedule: [] } ); + setPacksState(packStatsByType.packs); setSchedule(packStatsByType.schedule); } }, @@ -483,9 +484,9 @@ const HostDetailsPage = ({ router.push(PATHS.NEW_QUERY + TAGGED_TEMPLATES.queryByHostRoute(host?.id)); }; - const onQueryHostSaved = (selectedQuery: ISchedulableQuery) => { + const onQueryHostSaved = (selectedQuery: IQuery) => { router.push( - PATHS.EDIT_QUERY(selectedQuery.id) + + PATHS.EDIT_QUERY(selectedQuery) + TAGGED_TEMPLATES.queryByHostRoute(host?.id) ); }; @@ -614,6 +615,23 @@ const HostDetailsPage = ({ host?.mdm.name === "Fleet" && host?.mdm.macos_settings?.disk_encryption === "action_required"; + /* Context team id might be different that host's team id + Observer plus must be checked against host's team id */ + const isGlobalOrHostsTeamObserverPlus = + currentUser && host?.team_id + ? permissions.isObserverPlus(currentUser, host.team_id) + : false; + + const isHostsTeamObserver = + currentUser && host?.team_id + ? permissions.isTeamObserver(currentUser, host.team_id) + : false; + + const canViewPacks = + !isGlobalObserver && + !isGlobalOrHostsTeamObserverPlus && + !isHostsTeamObserver; + const bootstrapPackageData = { status: host?.mdm.macos_setup?.bootstrap_package_status, details: host?.mdm.macos_setup?.details, @@ -726,6 +744,9 @@ const HostDetailsPage = ({ schedule={schedule} isLoading={isLoadingHost} /> + {canViewPacks && ( + + )}
void; onQueryHostCustom: () => void; - onQueryHostSaved: (selectedQuery: ISchedulableQuery) => void; - queries: ISchedulableQuery[] | []; + onQueryHostSaved: (selectedQuery: IQuery) => void; + queries: IQuery[] | []; 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 d3db64be7b..ad828e0f50 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -187,6 +187,7 @@ const HostSummary = ({ { ), }, diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx index 825f00db6f..30b08fb234 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx @@ -15,7 +15,6 @@ 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 { @@ -139,7 +138,7 @@ const generateTableHeaders = ( type="dark" effect="solid" id={`critical-tooltip-${cellProps.row.original.id}`} - backgroundColor={COLORS["tooltip-bg"]} + backgroundColor="#3e4771" > 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 1f8a9a2948..a1aa92ce72 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 { OsqueryPlatform, SelectedPlatformString } from "interfaces/platform"; +import { IOsqueryPlatform, IPlatformString } from "interfaces/platform"; import { DEFAULT_POLICIES } from "pages/policies/constants"; import Avatar from "components/Avatar"; @@ -226,7 +226,7 @@ const PolicyForm = ({ }); } - let selectedPlatforms: OsqueryPlatform[] = []; + let selectedPlatforms: IOsqueryPlatform[] = []; if (isEditMode || defaultPolicy) { selectedPlatforms = getSelectedPlatforms(); } else { @@ -234,9 +234,7 @@ const PolicyForm = ({ setSelectedPlatforms(selectedPlatforms); } - const newPlatformString = selectedPlatforms.join( - "," - ) as SelectedPlatformString; + const newPlatformString = selectedPlatforms.join(",") as IPlatformString; 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 e96617c986..d30d85f2b3 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 { SelectedPlatformString } from "interfaces/platform"; +import { IPlatformString } from "interfaces/platform"; import useDeepEffect from "hooks/useDeepEffect"; // @ts-ignore @@ -81,7 +81,7 @@ const SaveNewPolicyModal = ({ const newPlatformString = platformSelector .getSelectedPlatforms() - .join(",") as SelectedPlatformString; + .join(",") as IPlatformString; setLastEditedQueryPlatform(newPlatformString); const { valid: validName, errors: newErrors } = validatePolicyName(name); diff --git a/frontend/pages/policies/constants.ts b/frontend/pages/policies/constants.ts index 6bc9ded8a3..2352b0ab3a 100644 --- a/frontend/pages/policies/constants.ts +++ b/frontend/pages/policies/constants.ts @@ -1,7 +1,7 @@ import { IPolicyNew } from "interfaces/policy"; -import { SelectedPlatformString } from "interfaces/platform"; +import { IPlatformString } from "interfaces/platform"; -const DEFAULT_POLICY_PLATFORM: SelectedPlatformString = ""; +const DEFAULT_POLICY_PLATFORM: IPlatformString = ""; export const DEFAULT_POLICY = { id: 1, diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 58e124b908..c3321d3495 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -2,10 +2,10 @@ import React, { useContext, useCallback, useEffect, - useState, useMemo, + useState, } from "react"; -import { InjectedRouter } from "react-router"; +import { RouteProps, InjectedRouter } from "react-router"; import { useQuery } from "react-query"; import { pick } from "lodash"; @@ -27,26 +27,21 @@ 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; }; @@ -58,7 +53,7 @@ const getPlatforms = (queryString: string): SupportedPlatform[] => { return platforms ?? []; }; -const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { +const enhanceQuery = (q: IQuery) => { return { ...q, performance: performanceIndicator( @@ -75,101 +70,51 @@ 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 { - userTeams, - currentTeamId, - handleTeamChange, - teamIdForApi, - isRouteOk, - } = useTeamIdParam({ - location, - router, - includeAllTeams: true, - includeNoTeam: false, - }); - - const isAnyTeamSelected = currentTeamId !== -1; - + const [queriesList, setQueriesList] = useState( + null + ); 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: 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); - }), + data: fleetQueries, + error: fleetQueriesError, + isFetching: isFetchingFleetQueries, + refetch: refetchFleetQueries, + } = useQuery( + "fleet queries by platform", + () => fleetQueriesAPI.loadAll(), { refetchOnWindowFocus: false, - enabled: isRouteOk, - staleTime: 5000, + select: (data: IFleetQueriesResponse) => data.queries, } ); - // 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, - } - ); + const enhancedQueriesList = useMemo(() => { + const enhancedQueries = fleetQueries?.map((q: IQuery) => { + const query = enhanceQuery(q); + return query; + }); - const automatedQueryIds = useMemo(() => { - return curTeamEnhancedQueries - ? curTeamEnhancedQueries - .filter((query) => query.automations_enabled) - .map((query) => query.id) - : []; - }, [curTeamEnhancedQueries]); + return enhancedQueries || []; + }, [fleetQueries]); + + useEffect(() => { + if (!isFetchingFleetQueries && enhancedQueriesList) { + setQueriesList(enhancedQueriesList); + } + }, [enhancedQueriesList, isFetchingFleetQueries]); useEffect(() => { const path = location.pathname + location.search; @@ -178,7 +123,7 @@ const ManageQueriesPage = ({ } }, [location, filteredQueriesPath, setFilteredQueriesPath]); - const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY(currentTeamId)); + const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY); const toggleDeleteQueryModal = useCallback(() => { setShowDeleteQueryModal(!showDeleteQueryModal); @@ -189,229 +134,34 @@ const ManageQueriesPage = ({ setSelectedQueryIds(selectedTableQueryIds); }; - 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; + const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries"; + setIsUpdatingQueries(true); + const deleteQueries = selectedQueryIds.map((id) => + fleetQueriesAPI.destroy(id) + ); + try { - if (bulk) { - await queriesAPI.bulkDestroy(selectedQueryIds); - } else { - await queriesAPI.destroy(selectedQueryIds[0]); - } - renderFlash( - "success", - `Successfully deleted ${bulk ? "queries" : "query"}.` - ); - setResetSelectedRows(true); - refetchAllQueries(); + await Promise.all(deleteQueries).then(() => { + renderFlash("success", `Successfully deleted ${queryOrQueries}.`); + setResetSelectedRows(true); + refetchFleetQueries(); + }); + renderFlash("success", `Successfully deleted ${queryOrQueries}.`); } catch (errorResponse) { renderFlash( "error", - `There was an error deleting your ${ - bulk ? "queries" : "query" - }. Please try again later.` + `There was an error deleting your ${queryOrQueries}. Please try again later.` ); } finally { toggleDeleteQueryModal(); setIsUpdatingQueries(false); } - }, [refetchAllQueries, selectedQueryIds, toggleDeleteQueryModal]); + }, [refetchFleetQueries, selectedQueryIds, toggleDeleteQueryModal]); - const renderHeader = () => { - 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 = []; - updateAutomatedQueries.push( - turnOnAutomations.map((id: number) => - queriesAPI.update(id, { automations_enabled: true }) - ) - ); - updateAutomatedQueries.push( - turnOffAutomations.map((id: number) => - 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 && ( - - )} - - ); - }; + const isTableDataLoading = isFetchingFleetQueries || queriesList === null; return ( @@ -419,45 +169,52 @@ const ManageQueriesPage = ({
-
{renderHeader()}
+

+ Queries +

-
- {(isGlobalAdmin || isTeamAdmin) && ( - + {(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) && + !!fleetQueries?.length && ( +
+ +
)} - {(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) && - !!curTeamEnhancedQueries?.length && ( - <> - - - )} -
-

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

+

Manage queries to ask specific questions about your devices.

- {renderCurrentScopeQueriesTable()} - {isAnyTeamSelected && - globalEnhancedQueries && - globalEnhancedQueries?.length > 0 && - renderInheritedQueriesSection()} - {renderModals()} +
+ {isTableDataLoading && !fleetQueriesError && } + {!isTableDataLoading && fleetQueriesError ? ( + + ) : ( + + )} +
+ {showDeleteQueryModal && ( + + )}
); diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 722bd10239..d88be01d9c 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -56,17 +56,14 @@ &__action-button-container { display: flex; - gap: $pad-small; + align-items: flex-start; + } + + .form-field--dropdown { + margin: 0; } .queries-table { - .controls { - .form-field { - &--dropdown { - margin: 0; - } - } - } &__platform-dropdown { width: 159px; @@ -98,99 +95,99 @@ } .data-table-block { - .data-table { - &__wrapper { - overflow-x: scroll; - } - &__table { - thead { - .name__header { + .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; width: auto; } - .platforms__header { - width: $col-sm; + } + @media (min-width: $break-lg) { + .author_name__header { + width: $col-md; } .updated_at__header { - display: none; - width: 0; + display: table-cell; + width: auto; } - .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; + } + } + tbody { + .name__cell { + max-width: $col-lg; + + .children-wrapper { + display: flex; + gap: $pad-xsmall; + + .observer-can-run-tooltip { + font-weight: $regular; } } } - tbody { + + @media (max-width: $break-md) { .name__cell { - max-width: $col-lg; - - .children-wrapper { - display: flex; - gap: $pad-xsmall; - - .observer-can-run-tooltip { - font-weight: $regular; - } + .w400 { + max-width: calc(400px - 81px); } } - - @media (max-width: $break-md) { - .name__cell { - .w400 { - max-width: calc(400px - 81px); - } - } + } + .platforms__cell { + max-width: $col-md; + } + .author_name__cell { + display: none; + max-width: $col-md; + img, + div, + span { + display: flex; + align-items: center; } - .platforms__cell { - max-width: $col-md; + div { + padding-right: $pad-small; } + .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: 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; - } + 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 deleted file mode 100644 index ff49667d43..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx +++ /dev/null @@ -1,19 +0,0 @@ -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/_styles.scss b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss deleted file mode 100644 index 78a5f043f7..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss +++ /dev/null @@ -1,65 +0,0 @@ -.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 deleted file mode 100644 index c9128e3c2d..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ManageAutomationsModal"; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index f5ad0b06f6..be634f9973 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -39,9 +39,7 @@ interface IQueriesTableProps { query?: string; order_key?: string; order_direction?: "asc" | "desc"; - team_id?: string; }; - isInherited?: boolean; } const DEFAULT_SORT_DIRECTION = "asc"; @@ -90,9 +88,8 @@ const QueriesTable = ({ isOnlyObserver, isObserverPlus, isAnyTeamObserverPlus, - router, queryParams, - isInherited = false, + router, }: IQueriesTableProps): JSX.Element | null => { const { currentUser } = useContext(AppContext); @@ -145,7 +142,6 @@ const QueriesTable = ({ ) { newQueryParams.page = 0; } - newQueryParams.team_id = queryParams?.team_id; const locationPath = getNextLocationPath({ pathPrefix: PATHS.MANAGE_QUERIES, queryParams: newQueryParams, @@ -236,21 +232,19 @@ const QueriesTable = ({ }; const tableHeaders = useMemo( - () => currentUser && generateTableHeaders({ currentUser, isInherited }), - [currentUser, isInherited] + () => currentUser && generateTableHeaders({ currentUser }), + [currentUser] ); - const searchable = - !(queriesList?.length === 0 && searchQuery === "") && !isInherited; + const searchable = !(queriesList?.length === 0 && searchQuery === ""); return tableHeaders && !isLoading ? (
JSX.Element) | string; Cell: | ((props: ICellProps) => JSX.Element) - | ((props: IPlatformCellProps) => JSX.Element) - | ((props: IStringCellProps) => JSX.Element) - | ((props: INumberCellProps) => JSX.Element) - | ((props: IBoolCellProps) => JSX.Element); + | ((props: IPlatformCellProps) => JSX.Element); id?: string; title?: string; accessor?: string; @@ -101,14 +85,12 @@ 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); @@ -125,7 +107,7 @@ const generateTableHeaders = ({ Cell: (cellProps: ICellProps): JSX.Element => { return (
{cellProps.cell.value}
@@ -144,7 +126,7 @@ const generateTableHeaders = ({ type="dark" effect="solid" id={`observer-can-run-tooltip-${cellProps.row.original.id}`} - backgroundColor={COLORS["tooltip-bg"]} + backgroundColor="#3e4771" > Observers can run this query. @@ -152,10 +134,7 @@ const generateTableHeaders = ({ )} } - path={PATHS.EDIT_QUERY( - cellProps.row.original.id, - cellProps.row.original.team_id ?? undefined - )} + path={PATHS.EDIT_QUERY(cellProps.row.original)} /> ); }, @@ -171,29 +150,30 @@ const generateTableHeaders = ({ }, }, { - title: "Frequency", - Header: "Frequency", - disableSortBy: true, - accessor: "interval", - Cell: (cellProps: INumberCellProps): JSX.Element => { - const val = cellProps.cell.value - ? `Every ${secondsToDhms(cellProps.cell.value)}` - : undefined; + 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; return ( - - Assign a frequency and turn automations on to - collect data at an interval. - - } - /> + + + {author} + ); }, + sortType: "caseInsensitive", }, { - title: "Performance impact", Header: () => { return (
@@ -211,7 +191,7 @@ const generateTableHeaders = ({ }, disableSortBy: true, accessor: "performance", - Cell: (cellProps: IStringCellProps) => ( + Cell: (cellProps: ICellProps) => ( ), }, - { - title: "Automations", - Header: "Automations", - disableSortBy: true, - accessor: "automations_enabled", - Cell: (cellProps: IBoolCellProps): JSX.Element => { - return ( - - ); - }, - }, { title: "Last modified", Header: (cellProps) => ( @@ -243,7 +209,7 @@ const generateTableHeaders = ({ /> ), accessor: "updated_at", - Cell: (cellProps: INumberCellProps): JSX.Element => ( + Cell: (cellProps: ICellProps): 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: () => { - toggleAllRowsSelected(); + 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) + ); + } + } }, }; return ; @@ -278,9 +285,44 @@ const generateTableHeaders = ({ const checkboxProps = { value: checked, onChange: () => row.toggleRowSelected(), + disabled: + isAnyTeamMaintainerOrTeamAdmin && + row.original.author_id !== currentUser.id, }; - // v4.35.0 Any team admin or maintainer now can add, edit, delete their team's queries - return ; + // 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. + +
+ + ); }, disableHidden: true, }); diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator.tsx deleted file mode 100644 index 34edebdf79..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator.tsx +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index ba531246a7..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/_styles.scss +++ /dev/null @@ -1,21 +0,0 @@ -.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 deleted file mode 100644 index e2d4e68a35..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./QueryAutomationsStatusIndicator"; diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx index ca25321e57..75f636686a 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tsx @@ -12,10 +12,7 @@ import statusAPI from "services/entities/status"; import { IHost, IHostResponse } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { ITeam } from "interfaces/team"; -import { - IGetQueryResponse, - ISchedulableQuery, -} from "interfaces/schedulable_query"; +import { IQueryFormData, IQuery, IStoredQueryResponse } from "interfaces/query"; import { ITarget } from "interfaces/target"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; @@ -26,15 +23,12 @@ import CustomLink from "components/CustomLink"; import QueryEditor from "pages/queries/QueryPage/screens/QueryEditor"; import RunQuery from "pages/queries/QueryPage/screens/RunQuery"; -import useTeamIdParam from "hooks/useTeamIdParam"; interface IQueryPageProps { router: InjectedRouter; params: Params; location: { - pathname: string; - query: { host_ids: string; team_id?: string }; - search: string; + query: { host_ids: string }; }; } @@ -43,18 +37,9 @@ const baseClass = "query-page"; const QueryPage = ({ router, params: { id: paramsQueryId }, - location, + location: { query: URLQuerySearch }, }: 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 { @@ -72,10 +57,6 @@ const QueryPage = ({ setLastEditedQueryDescription, setLastEditedQueryBody, setLastEditedQueryObserverCanRun, - setLastEditedQueryFrequency, - setLastEditedQueryLoggingType, - setLastEditedQueryMinOsqueryVersion, - setLastEditedQueryPlatforms, } = useContext(QueryContext); const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false); @@ -97,23 +78,19 @@ const QueryPage = ({ isLoading: isStoredQueryLoading, data: storedQuery, error: storedQueryError, - } = useQuery( + } = useQuery( ["query", queryId], () => queryAPI.load(queryId as number), { enabled: !!queryId, refetchOnWindowFocus: false, - select: (data) => data.query, + select: (data: IStoredQueryResponse) => 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), } @@ -122,9 +99,9 @@ const QueryPage = ({ useQuery( "hostFromURL", () => - hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)), + hostAPI.loadHostDetails(parseInt(URLQuerySearch.host_ids as string, 10)), { - enabled: !!location.query.host_ids && !queryParamHostsAdded, + enabled: !!URLQuerySearch.host_ids && !queryParamHostsAdded, select: (data: IHostResponse) => data.host, onSuccess: (host) => { setTargetedHosts((prevHosts) => @@ -142,6 +119,10 @@ const QueryPage = ({ } ); + const { mutateAsync: createQuery } = useMutation((formData: IQueryFormData) => + queryAPI.create(formData) + ); + const detectIsFleetQueryRunnable = () => { statusAPI.live_query().catch(() => { setIsLiveQueryRunnable(false); @@ -150,18 +131,12 @@ const QueryPage = ({ useEffect(() => { detectIsFleetQueryRunnable(); - if (!queryId) { - setLastEditedQueryId(DEFAULT_QUERY.id); - setLastEditedQueryName(DEFAULT_QUERY.name); - setLastEditedQueryDescription(DEFAULT_QUERY.description); - setLastEditedQueryBody(DEFAULT_QUERY.query); - setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run); - setLastEditedQueryFrequency(DEFAULT_QUERY.interval); - setLastEditedQueryLoggingType(DEFAULT_QUERY.logging); - setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version); - setLastEditedQueryPlatforms(DEFAULT_QUERY.platform); - } - }, [queryId]); + setLastEditedQueryId(DEFAULT_QUERY.id); + setLastEditedQueryName(DEFAULT_QUERY.name); + setLastEditedQueryDescription(DEFAULT_QUERY.description); + setLastEditedQueryBody(DEFAULT_QUERY.query); + setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run); + }, []); useEffect(() => { setShowOpenSchemaActionText(!isSidebarOpen); @@ -204,23 +179,22 @@ const QueryPage = ({ const goToQueryEditor = useCallback(() => setStep(QUERIES_PAGE_STEPS[1]), []); const renderScreen = () => { - const step1Props = { + const step1Opts = { router, baseClass, queryIdForEdit: queryId, - teamNameForQuery, - apiTeamIdForQuery, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, storedQueryError, + createQuery, onOsqueryTableSelect, goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]), onOpenSchemaSidebar, renderLiveQueryWarning, }; - const step2Props = { + const step2Opts = { baseClass, queryId, selectedTargets, @@ -237,7 +211,7 @@ const QueryPage = ({ setTargetsTotalCount, }; - const step3Props = { + const step3Opts = { queryId, selectedTargets, storedQuery, @@ -248,11 +222,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 new file mode 100644 index 0000000000..836450c456 --- /dev/null +++ b/frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx @@ -0,0 +1,135 @@ +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 new file mode 100644 index 0000000000..acf83db4a9 --- /dev/null +++ b/frontend/pages/queries/QueryPage/components/NewQueryModal/index.ts @@ -0,0 +1 @@ +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 aaa32d89d3..a3f7b80ad0 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -1,35 +1,17 @@ -import React, { - useState, - useContext, - useEffect, - KeyboardEvent, - useCallback, -} from "react"; +import React, { useState, useContext, useEffect, KeyboardEvent } from "react"; import { InjectedRouter } from "react-router"; -import { pull, size } from "lodash"; +import { 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 { - ISchedulableQuery, - ICreateQueryRequestBody, - QueryLoggingOption, -} from "interfaces/schedulable_query"; -import { SelectedPlatformString } from "interfaces/platform"; +import { IQuery, IQueryFormData } from "interfaces/query"; import queryAPI from "services/entities/queries"; import { IAceEditor } from "react-ace/lib/types"; @@ -41,12 +23,10 @@ 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 SaveQueryModal from "../SaveQueryModal"; +import NewQueryModal from "../NewQueryModal"; import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png"; const baseClass = "query-form"; @@ -54,17 +34,15 @@ const baseClass = "query-form"; interface IQueryFormProps { router: InjectedRouter; queryIdForEdit: number | null; - apiTeamIdForQuery?: number; - teamNameForQuery?: string; showOpenSchemaActionText: boolean; - storedQuery: ISchedulableQuery | undefined; + storedQuery: IQuery | undefined; isStoredQueryLoading: boolean; isQuerySaving: boolean; isQueryUpdating: boolean; - saveQuery: (formData: ICreateQueryRequestBody) => void; + onCreateQuery: (formData: IQueryFormData) => void; onOsqueryTableSelect: (tableName: string) => void; goToSelectTargets: () => void; - onUpdate: (formData: ICreateQueryRequestBody) => void; + onUpdate: (formData: IQueryFormData) => void; onOpenSchemaSidebar: () => void; renderLiveQueryWarning: () => JSX.Element | null; backendValidators: { [key: string]: string }; @@ -85,14 +63,12 @@ const validateQuerySQL = (query: string) => { const QueryForm = ({ router, queryIdForEdit, - apiTeamIdForQuery, - teamNameForQuery, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, isQuerySaving, isQueryUpdating, - saveQuery, + onCreateQuery, onOsqueryTableSelect, goToSelectTargets, onUpdate, @@ -108,18 +84,10 @@ const QueryForm = ({ lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, - lastEditedQueryFrequency, - lastEditedQueryPlatforms, - lastEditedQueryMinOsqueryVersion, - lastEditedQueryLoggingType, setLastEditedQueryName, setLastEditedQueryDescription, setLastEditedQueryBody, setLastEditedQueryObserverCanRun, - setLastEditedQueryFrequency, - setLastEditedQueryPlatforms, - setLastEditedQueryMinOsqueryVersion, - setLastEditedQueryLoggingType, } = useContext(QueryContext); const { @@ -136,14 +104,13 @@ const QueryForm = ({ const savedQueryMode = !!queryIdForEdit; const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined - const [showSaveQueryModal, setShowSaveQueryModal] = useState(false); + const [isSaveModalOpen, setIsSaveModalOpen] = 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; @@ -166,7 +133,7 @@ const QueryForm = ({ } debounceSQL(lastEditedQueryBody); - }, [lastEditedQueryBody, lastEditedQueryId, isStoredQueryLoading]); + }, [lastEditedQueryBody, lastEditedQueryId]); const hasTeamMaintainerPermissions = savedQueryMode ? isAnyTeamMaintainerOrTeamAdmin && @@ -175,10 +142,6 @@ const QueryForm = ({ storedQuery.author_id === currentUser.id : isAnyTeamMaintainerOrTeamAdmin; - const toggleSaveQueryModal = () => { - setShowSaveQueryModal(!showSaveQueryModal); - }; - const onLoad = (editor: IAceEditor) => { editor.setOptions({ enableLinking: true, @@ -210,50 +173,6 @@ 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 ) => { @@ -273,21 +192,17 @@ 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: ISchedulableQuery }) => { + .then((response: { query: IQuery }) => { setIsSaveAsNewLoading(false); - router.push(PATHS.EDIT_QUERY(response.query.id)); + router.push(PATHS.EDIT_QUERY(response.query)); renderFlash("success", `Successfully added query.`); }) .catch((createError: { data: IApiError }) => { @@ -297,16 +212,11 @@ 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: ISchedulableQuery }) => { + .then((response: { query: IQuery }) => { setIsSaveAsNewLoading(false); - router.push(PATHS.EDIT_QUERY(response.query.id)); + router.push(PATHS.EDIT_QUERY(response.query)); renderFlash( "success", `Successfully added query as "Copy of ${lastEditedQueryName}".` @@ -318,19 +228,9 @@ 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", - `A query called "Copy of ${lastEditedQueryName}" already exists for ${teamErrorText}.` + `"Copy of ${lastEditedQueryName}" already exists. Please rename your query and try again.` ); } setIsSaveAsNewLoading(false); @@ -360,17 +260,13 @@ const QueryForm = ({ if (valid) { if (!savedQueryMode) { - setShowSaveQueryModal(true); + setIsSaveModalOpen(true); } else { onUpdate({ name: lastEditedQueryName, description: lastEditedQueryDescription, query: lastEditedQueryBody, observer_can_run: lastEditedQueryObserverCanRun, - interval: lastEditedQueryFrequency, - platform: lastEditedQueryPlatforms, - min_osquery_version: lastEditedQueryMinOsqueryVersion, - logging: lastEditedQueryLoggingType, }); } } @@ -549,7 +445,7 @@ const QueryForm = ({ variant="blue-green" onClick={goToSelectTargets} > - Live query + Run query
)} @@ -586,72 +482,21 @@ const QueryForm = ({ {renderPlatformCompatibility()} {savedQueryMode && ( -
-
- - 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 && ( -
- - - -
- )} -
+ <> + + 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. +

+ )} {renderLiveQueryWarning()}
@@ -687,11 +530,9 @@ const QueryForm = ({ className="save-loading" variant="brand" onClick={promptSaveQuery()} - // Button disabled for team maintainer/admins viewing global queries disabled={ isAnyTeamMaintainerOrTeamAdmin && - !storedQuery?.team_id && - !!queryIdForEdit + !hasTeamMaintainerPermissions } isLoading={isQueryUpdating} > @@ -700,15 +541,16 @@ const QueryForm = ({
{" "} <> - You can only save changes -
to a team level query. + You can only save +
changes to a query if you +
are the author.
@@ -719,16 +561,16 @@ const QueryForm = ({ variant="blue-green" onClick={goToSelectTargets} > - Live query + Run query
- {showSaveQueryModal && ( - diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss b/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss index 5a9ab0d49f..c53d661326 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss +++ b/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss @@ -154,22 +154,6 @@ 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 deleted file mode 100644 index d559aa9866..0000000000 --- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx +++ /dev/null @@ -1,257 +0,0 @@ -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 deleted file mode 100644 index a4d1337350..0000000000 --- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss +++ /dev/null @@ -1,32 +0,0 @@ -.save-query-modal { - .fleet-checkbox { - display: flex; - align-items: center; - } - - .help-text { - margin-top: $pad-small; - margin-bottom: $pad-large; - font-weight: $regular; - font-size: 0.75rem; - color: $ui-fleet-black-75; - } - - &__form-field { - &--frequency { - margin-bottom: 0; - } - &--platform { - margin-bottom: 0; - margin-top: $pad-large; - } - } - - &__observer-can-run-wrapper { - margin-bottom: 0; - } - - &__advanced-options-toggle { - font-weight: $xbold; - } -} diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts b/frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts deleted file mode 100644 index fd4708d1b7..0000000000 --- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./SaveQueryModal"; diff --git a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx index 7b94956efd..75321ef8c9 100644 --- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx +++ b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx @@ -7,10 +7,7 @@ import queryAPI from "services/entities/queries"; import { AppContext } from "context/app"; import { QueryContext } from "context/query"; import { NotificationContext } from "context/notification"; -import { - ICreateQueryRequestBody, - ISchedulableQuery, -} from "interfaces/schedulable_query"; +import { IQueryFormData, IQuery } from "interfaces/query"; import PATHS from "router/paths"; import debounce from "utilities/debounce"; import deepDifference from "utilities/deep_difference"; @@ -22,12 +19,16 @@ interface IQueryEditorProps { router: InjectedRouter; baseClass: string; queryIdForEdit: number | null; - teamNameForQuery?: string; - apiTeamIdForQuery?: number; - storedQuery: ISchedulableQuery | undefined; + storedQuery: IQuery | undefined; storedQueryError: Error | null; showOpenSchemaActionText: boolean; isStoredQueryLoading: boolean; + createQuery: UseMutateAsyncFunction< + { query: IQuery }, + unknown, + IQueryFormData, + unknown + >; onOsqueryTableSelect: (tableName: string) => void; goToSelectTargets: () => void; onOpenSchemaSidebar: () => void; @@ -38,12 +39,11 @@ const QueryEditor = ({ router, baseClass, queryIdForEdit, - teamNameForQuery, - apiTeamIdForQuery, storedQuery, storedQueryError, showOpenSchemaActionText, isStoredQueryLoading, + createQuery, onOsqueryTableSelect, goToSelectTargets, onOpenSchemaSidebar, @@ -59,10 +59,6 @@ const QueryEditor = ({ lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, - lastEditedQueryFrequency, - lastEditedQueryLoggingType, - lastEditedQueryPlatforms, - lastEditedQueryMinOsqueryVersion, } = useContext(QueryContext); const [isQuerySaving, setIsQuerySaving] = useState(false); @@ -81,35 +77,29 @@ const QueryEditor = ({ [key: string]: string; }>({}); - const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => { + const onSaveQueryFormSubmit = debounce(async (formData: IQueryFormData) => { setIsQuerySaving(true); try { - const { query } = await queryAPI.create(formData); - router.push(PATHS.EDIT_QUERY(query.id)); + const { query }: { query: IQuery } = await createQuery(formData); + router.push(PATHS.EDIT_QUERY(query)); renderFlash("success", "Query created!"); setBackendValidators({}); } catch (createError: any) { + console.error(createError); if (createError.data.errors[0].reason.includes("already exists")) { - const teamErrorText = - teamNameForQuery && apiTeamIdForQuery !== 0 - ? `the ${teamNameForQuery} team` - : "all teams"; - setBackendValidators({ - name: `A query with that name already exists for ${teamErrorText}.`, - }); + setBackendValidators({ name: "A query with this name already exists" }); } else { renderFlash( "error", "Something went wrong creating your query. Please try again." ); - setBackendValidators({}); } } finally { setIsQuerySaving(false); } }); - const onUpdateQuery = async (formData: ICreateQueryRequestBody) => { + const onUpdateQuery = async (formData: IQueryFormData) => { if (!queryIdForEdit) { return false; } @@ -121,10 +111,6 @@ const QueryEditor = ({ lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, - lastEditedQueryFrequency, - lastEditedQueryPlatforms, - lastEditedQueryLoggingType, - lastEditedQueryMinOsqueryVersion, }); try { @@ -163,14 +149,12 @@ 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 new file mode 100644 index 0000000000..f93847b34b --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/_styles.scss @@ -0,0 +1,122 @@ +.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/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx similarity index 100% rename from frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx rename to frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx diff --git a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss new file mode 100644 index 0000000000..f965ee2b20 --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss @@ -0,0 +1,14 @@ +.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/queries/ManageQueriesPage/components/PreviewDataModal/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts similarity index 100% rename from frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts rename to frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts diff --git a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx new file mode 100644 index 0000000000..fa08aeb3a3 --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000000..90280fc7bd --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts @@ -0,0 +1 @@ +export { default } from "./RemoveScheduledQueryModal"; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx new file mode 100644 index 0000000000..f634f803c5 --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx @@ -0,0 +1,364 @@ +/* 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 new file mode 100644 index 0000000000..5682c40509 --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss @@ -0,0 +1,26 @@ +.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 new file mode 100644 index 0000000000..2840b8d26c --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts @@ -0,0 +1 @@ +export { default } from "./ScheduleEditorModal"; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx new file mode 100644 index 0000000000..e5acf57f5d --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx @@ -0,0 +1,224 @@ +/** + * 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 new file mode 100644 index 0000000000..c4001ab957 --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx @@ -0,0 +1,272 @@ +/* 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 new file mode 100644 index 0000000000..fb4310e446 --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts @@ -0,0 +1 @@ +export { default } from "./ScheduleTable"; diff --git a/frontend/pages/schedule/ManageSchedulePage/index.ts b/frontend/pages/schedule/ManageSchedulePage/index.ts new file mode 100644 index 0000000000..ab51b9b30f --- /dev/null +++ b/frontend/pages/schedule/ManageSchedulePage/index.ts @@ -0,0 +1 @@ +export { default } from "./ManageSchedulePage"; diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index 3d704fd00e..d0e716d5a8 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -33,6 +33,7 @@ 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"; @@ -170,8 +171,8 @@ const routes = ( - + @@ -205,6 +206,14 @@ const routes = ( + + + + + + + + diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index 164f0328db..8afb68c524 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -1,3 +1,4 @@ +import { IQuery } from "../interfaces/query"; import { IPolicy } from "../interfaces/policy"; import URL_PREFIX from "./url_prefix"; @@ -44,10 +45,8 @@ export default { EDIT_LABEL: (labelId: number): string => { return `${URL_PREFIX}/labels/${labelId}`; }, - EDIT_QUERY: (queryId: number, teamId?: number): string => { - return `${URL_PREFIX}/queries/${queryId}${ - teamId ? `?team_id=${teamId}` : "" - }`; + EDIT_QUERY: (query: IQuery): string => { + return `${URL_PREFIX}/queries/${query.id}`; }, EDIT_POLICY: (policy: IPolicy): string => { return `${URL_PREFIX}/policies/${policy.id}${ @@ -111,8 +110,7 @@ export default { MANAGE_POLICIES: `${URL_PREFIX}/policies/manage`, NEW_LABEL: `${URL_PREFIX}/labels/new`, NEW_POLICY: `${URL_PREFIX}/policies/new`, - NEW_QUERY: (teamId?: number) => - `${URL_PREFIX}/queries/new${teamId ? `?team_id=${teamId}` : ""}`, + NEW_QUERY: `${URL_PREFIX}/queries/new`, 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 b8abf7061b..6361eee1a2 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -8,7 +8,7 @@ import { reconcileMutuallyExclusiveHostParams, reconcileMutuallyInclusiveHostParams, } from "utilities/url"; -import { SelectedPlatform } from "interfaces/platform"; +import { ISelectedPlatform } from "interfaces/platform"; import { ISoftware } from "interfaces/software"; import { FileVaultProfileStatus, @@ -127,7 +127,7 @@ const getSortParams = (sortOptions?: ISortOption[]) => { }; }; -const createMdmParams = (platform?: SelectedPlatform, teamId?: number) => { +const createMdmParams = (platform?: ISelectedPlatform, teamId?: number) => { if (platform === "all") { return buildQueryStringFromParams({ team_id: teamId }); } @@ -328,7 +328,7 @@ export default { return sendRequest("GET", HOST_MDM(id)); }, - getMdmSummary: (platform?: SelectedPlatform, teamId?: number) => { + getMdmSummary: (platform?: ISelectedPlatform, 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 c8a0b44d70..9650814fb4 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 { OsqueryPlatform } from "interfaces/platform"; +import { IOsqueryPlatform } 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?: OsqueryPlatform; + platform?: IOsqueryPlatform; teamId?: number; } diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 89765f6481..1d47ab2360 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -1,22 +1,20 @@ /* 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: (createQueryRequestBody: ICreateQueryRequestBody) => { + create: ({ description, name, query, observer_can_run }: IQueryFormData) => { const { QUERIES } = endpoints; - return sendRequest("POST", QUERIES, createQueryRequestBody); + return sendRequest("POST", QUERIES, { + description, + name, + query, + observer_can_run, + }); }, destroy: (id: string | number) => { const { QUERIES } = endpoints; @@ -24,26 +22,16 @@ 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: (teamId?: number) => { + loadAll: () => { const { QUERIES } = endpoints; - const queryString = buildQueryStringFromParams({ team_id: teamId }); - const path = `${QUERIES}`; - return sendRequest( - "GET", - queryString ? path.concat(`?${queryString}`) : path - ); + return sendRequest("GET", QUERIES); }, run: async ({ query, @@ -74,7 +62,7 @@ export default { throw new Error(getError(response as AxiosResponse)); } }, - update: (id: number, updateParams: IModifyQueryRequestBody) => { + update: (id: number, updateParams: IQueryFormData) => { 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 50ebcbf1f9..ab7c5583ec 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -22,17 +22,6 @@ 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 @@ -42,16 +31,6 @@ 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 524a638ed2..39ccc7f329 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -364,260 +364,8 @@ 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", - }, - { - 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 a16b2fa847..652b967043 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -11,7 +11,6 @@ $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 8034a9ba8a..78a79ede80 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -1,7 +1,6 @@ import URL_PREFIX from "router/url_prefix"; -import { OsqueryPlatform } from "interfaces/platform"; +import { IOsqueryPlatform } 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`; @@ -24,11 +23,7 @@ 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" }, @@ -52,10 +47,6 @@ 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" }, @@ -99,26 +90,20 @@ export const QUERIES_PAGE_STEPS = { 3: "RUN", }; -export const DEFAULT_QUERY: ISchedulableQuery = { +export const DEFAULT_QUERY = { 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 = { @@ -156,7 +141,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", @@ -201,8 +186,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 }, @@ -213,12 +198,10 @@ export const PLATFORM_DROPDOWN_OPTIONS: IPlatformDropdownOptions[] = [ ]; // Schedules does not support ChromeOS -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 SCHEDULE_PLATFORM_DROPDOWN_OPTIONS: IPlatformDropdownOptions[] = PLATFORM_DROPDOWN_OPTIONS.slice( + 0, + -1 +); export const PLATFORM_NAME_TO_LABEL_NAME = { all: "", diff --git a/frontend/utilities/osquery_tables.ts b/frontend/utilities/osquery_tables.ts index f71bbeb0ed..ea0de23953 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 -// OsqueryPlatform for platform names, instead of just any strings. +// IOsqueryPlatform 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 16d7c40b00..949b4ebb79 100644 --- a/frontend/utilities/sql_tools.ts +++ b/frontend/utilities/sql_tools.ts @@ -3,10 +3,9 @@ import sqliteParser from "sqlite-parser"; import { intersection, isPlainObject } from "lodash"; import { osqueryTables } from "utilities/osquery_tables"; import { - OsqueryPlatform, + IOsqueryPlatform, MACADMINS_EXTENSION_TABLES, SUPPORTED_PLATFORMS, - SupportedPlatform, } from "interfaces/platform"; type IAstNode = Record; @@ -25,10 +24,10 @@ interface ISqlCteNode { // TODO: Is it ever possible that osquery_tables.json would be missing name or platforms? interface IOsqueryTable { name: string; - platforms: OsqueryPlatform[]; + platforms: IOsqueryPlatform[]; } -type IPlatformDictionay = Record; +type IPlatformDictionay = Record; const platformsByTableDictionary: IPlatformDictionay = (osqueryTables as IOsqueryTable[]).reduce( (dictionary: IPlatformDictionay, osqueryTable) => { @@ -65,9 +64,7 @@ const _visit = ( } }; -const filterCompatiblePlatforms = ( - sqlTables: string[] -): SupportedPlatform[] => { +const filterCompatiblePlatforms = (sqlTables: string[]): IOsqueryPlatform[] => { 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 } @@ -125,7 +122,7 @@ const parseSqlTables = ( const checkPlatformCompatibility = ( sqlString: string, includeCteTables = false -): { platforms: SupportedPlatform[] | null; error: Error | null } => { +): { platforms: IOsqueryPlatform[] | null; error: Error | null } => { let sqlTables: string[] | undefined; try { sqlTables = parseSqlTables(sqlString, includeCteTables); From 4cab838864f204958b72fad44df1af5d21ab0c30 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Thu, 27 Jul 2023 13:44:40 -0700 Subject: [PATCH 67/78] Revert "Fleet UI: New manage query automations modal (#12747)" This reverts commit e13644d664d0eea072cfe6549ec5bf390cf72b13. --- changes/12645-manage-query-automations | 1 - frontend/__mocks__/scheduleableQueryMock.ts | 12 +- .../LogDestinationIndicator.stories.tsx | 17 -- .../LogDestinationIndicator.tsx | 86 -------- .../LogDestinationIndicator/index.ts | 1 - frontend/components/Modal/Modal.tsx | 1 - .../QueryFrequencyIndicator.stories.tsx | 18 -- .../QueryFrequencyIndicator.tsx | 73 ------- .../QueryFrequencyIndicator/_styles.scss | 14 -- .../QueryFrequencyIndicator/index.ts | 1 - frontend/components/icons/Clock.tsx | 39 ---- frontend/components/icons/Warning.tsx | 33 --- frontend/components/icons/index.ts | 4 - frontend/interfaces/query.ts | 6 +- .../ManageQueriesPage/ManageQueriesPage.tsx | 116 ++-------- .../ManageAutomationsModal.tsx | 199 +----------------- .../ManageAutomationsModal/_styles.scss | 65 ------ .../PreviewDataModal/PreviewDataModal.tsx | 61 ------ .../components/PreviewDataModal/index.ts | 1 - .../QueriesTable/QueriesTableConfig.tsx | 1 - .../services/mock_service/mocks/config.ts | 6 +- .../services/mock_service/mocks/responses.ts | 123 +---------- frontend/styles/var/colors.scss | 1 - 23 files changed, 33 insertions(+), 846 deletions(-) delete mode 100644 changes/12645-manage-query-automations delete mode 100644 frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx delete mode 100644 frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx delete mode 100644 frontend/components/LogDestinationIndicator/index.ts delete mode 100644 frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx delete mode 100644 frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx delete mode 100644 frontend/components/QueryFrequencyIndicator/_styles.scss delete mode 100644 frontend/components/QueryFrequencyIndicator/index.ts delete mode 100644 frontend/components/icons/Clock.tsx delete mode 100644 frontend/components/icons/Warning.tsx delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts diff --git a/changes/12645-manage-query-automations b/changes/12645-manage-query-automations deleted file mode 100644 index 0df3de75f3..0000000000 --- a/changes/12645-manage-query-automations +++ /dev/null @@ -1 +0,0 @@ -- Users able to manage schedulable queries (new feature) with automations modal diff --git a/frontend/__mocks__/scheduleableQueryMock.ts b/frontend/__mocks__/scheduleableQueryMock.ts index edc82a61da..e437edc121 100644 --- a/frontend/__mocks__/scheduleableQueryMock.ts +++ b/frontend/__mocks__/scheduleableQueryMock.ts @@ -10,7 +10,7 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { description: "A test query", query: "SELECT * FROM users", team_id: null, - interval: 43200, // Every 12 hours + interval: 3600, platform: "darwin,windows,linux", min_osquery_version: "", automations_enabled: true, @@ -22,11 +22,11 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { 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, + system_time_p50: 1, + system_time_p95: 1, + user_time_p50: 1, + user_time_p95: 1, + total_executions: 3, }, }; diff --git a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx deleted file mode 100644 index a39903f806..0000000000 --- a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 82de7b5d0b..0000000000 --- a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index 1d2d5a12d4..0000000000 --- a/frontend/components/LogDestinationIndicator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./LogDestinationIndicator"; diff --git a/frontend/components/Modal/Modal.tsx b/frontend/components/Modal/Modal.tsx index 0ec2c2149f..4044852d82 100644 --- a/frontend/components/Modal/Modal.tsx +++ b/frontend/components/Modal/Modal.tsx @@ -11,7 +11,6 @@ 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/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx b/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx deleted file mode 100644 index df2a8fc374..0000000000 --- a/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 94dfecbdda..0000000000 --- a/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index f5b5f74d01..0000000000 --- a/frontend/components/QueryFrequencyIndicator/_styles.scss +++ /dev/null @@ -1,14 +0,0 @@ -.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 deleted file mode 100644 index 4f84c00133..0000000000 --- a/frontend/components/QueryFrequencyIndicator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./QueryFrequencyIndicator"; diff --git a/frontend/components/icons/Clock.tsx b/frontend/components/icons/Clock.tsx deleted file mode 100644 index c739a19aa7..0000000000 --- a/frontend/components/icons/Clock.tsx +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index a3e1ce156c..0000000000 --- a/frontend/components/icons/Warning.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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 bda1176aa3..bb5fe334aa 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -51,8 +51,6 @@ 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"; @@ -112,8 +110,6 @@ 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/interfaces/query.ts b/frontend/interfaces/query.ts index d6a948cd25..96a8efa4d1 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -1,6 +1,5 @@ import { IFormField } from "./form_field"; import { IPack } from "./pack"; -import { ISchedulableQuery } from "./schedulable_query"; import { IScheduledQueryStats } from "./scheduled_query_stats"; export interface IQueryFormData { @@ -8,15 +7,14 @@ export interface IQueryFormData { name?: string | number | boolean | undefined; query?: string | number | boolean | undefined; observer_can_run?: string | number | boolean | undefined; - automations_enabled?: boolean; } export interface IStoredQueryResponse { - query: ISchedulableQuery; + query: IQuery; } export interface IFleetQueriesResponse { - queries: ISchedulableQuery[]; + queries: IQuery[]; } export interface IQuery { diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 58e124b908..449ad6c34d 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -32,8 +32,7 @@ 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"; +import ManageAutomationsModal from "./components/ManageAutomationsModal"; const baseClass = "manage-queries-page"; interface IManageQueriesPageProps { @@ -85,7 +84,6 @@ const ManageQueriesPage = ({ filteredQueriesPath, isPremiumTier, isSandboxMode, - config, } = useContext(AppContext); const { setResetSelectedRows } = useContext(TableContext); @@ -108,13 +106,11 @@ const ManageQueriesPage = ({ const [selectedQueryIds, setSelectedQueryIds] = useState([]); const [showDeleteQueryModal, setShowDeleteQueryModal] = useState(false); + const [isUpdatingQueries, setIsUpdatingQueries] = 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: curTeamEnhancedQueries, @@ -163,14 +159,6 @@ const ManageQueriesPage = ({ } ); - const automatedQueryIds = useMemo(() => { - return curTeamEnhancedQueries - ? curTeamEnhancedQueries - .filter((query) => query.automations_enabled) - .map((query) => query.id) - : []; - }, [curTeamEnhancedQueries]); - useEffect(() => { const path = location.pathname + location.search; if (filteredQueriesPath !== path) { @@ -184,6 +172,10 @@ const ManageQueriesPage = ({ setShowDeleteQueryModal(!showDeleteQueryModal); }, [showDeleteQueryModal, setShowDeleteQueryModal]); + const toggleManageAutomationsModal = useCallback(() => { + setShowManageAutomationsModal(!showManageAutomationsModal); + }, [showManageAutomationsModal, setShowManageAutomationsModal]); + const onDeleteQueryClick = (selectedTableQueryIds: number[]) => { toggleDeleteQueryModal(); setSelectedQueryIds(selectedTableQueryIds); @@ -194,25 +186,6 @@ const ManageQueriesPage = ({ 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); @@ -339,52 +312,6 @@ const ManageQueriesPage = ({ ); }; - 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 = []; - updateAutomatedQueries.push( - turnOnAutomations.map((id: number) => - queriesAPI.update(id, { automations_enabled: true }) - ) - ); - updateAutomatedQueries.push( - turnOffAutomations.map((id: number) => - 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 ( <> @@ -396,18 +323,7 @@ const ManageQueriesPage = ({ /> )} {showManageAutomationsModal && ( - - )} - {showPreviewDataModal && ( - + )} ); @@ -425,7 +341,7 @@ const ManageQueriesPage = ({
{(isGlobalAdmin || isTeamAdmin) && ( - + )}
diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index 0e9d5daae1..cb6abd9cb8 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -1,206 +1,19 @@ -import React, { useState, useEffect } from "react"; -import { omit } from "lodash"; +import React from "react"; 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"; +const baseClass = "automations-modal"; interface IManageAutomationsModalProps { - isUpdatingAutomations: boolean; - handleSubmit: (formData: any) => void; // TODO - onCancel: () => void; - togglePreviewDataModal: () => void; - availableQueries?: ISchedulableQuery[]; - automatedQueryIds: number[]; - logDestination: string; + onExit: () => void; } -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, + onExit, }: 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?   - -

-
-
-
- -
-
- - -
-
-
+ +
); }; diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss deleted file mode 100644 index 78a5f043f7..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss +++ /dev/null @@ -1,65 +0,0 @@ -.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/PreviewDataModal/PreviewDataModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx deleted file mode 100644 index 3156c73288..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* This component is used for creating and editing both global and team scheduled queries */ - -import React from "react"; -import { syntaxHighlight } from "utilities/helpers"; - -import Modal from "components/Modal"; -import Button from "components/buttons/Button"; -import TooltipWrapper from "components/TooltipWrapper"; - -const baseClass = "preview-data-modal"; - -interface IPreviewDataModalProps { - onCancel: () => void; -} - -const PreviewDataModal = ({ - onCancel, -}: IPreviewDataModalProps): JSX.Element => { - const json = { - action: "snapshot", - snapshot: [ - { - remote_address: "0.0.0.0", - remote_port: "0", - cmdline: "/usr/sbin/syslogd", - }, - ], - name: "xxxxxxx", - hostIdentifier: "xxxxxxx", - calendarTime: "xxx xxx x xx:xx:xx xxxx UTC", - unixTime: "xxxxxxxxx", - epoch: "xxxxxxxxx", - counter: "x", - numerics: "x", - }; - - return ( - -
-

- - The data sent to your configured log destination will look similar - to the following JSON: - -

-
-
-        
-
- -
-
-
- ); -}; - -export default PreviewDataModal; diff --git a/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts deleted file mode 100644 index 48fca40136..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./PreviewDataModal"; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 744a829bc6..3af8a75a7f 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -193,7 +193,6 @@ const generateTableHeaders = ({ }, }, { - title: "Performance impact", Header: () => { return (
diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index 50ebcbf1f9..15a3b1a6b4 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -28,11 +28,7 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { "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, + "queries?team_id=43": RESPONSES.teamQueries, }, POST: { // request body is ISelectedTargets diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index 524a638ed2..fabc92bacb 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -431,60 +431,6 @@ const globalQueries = { 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: "", @@ -504,33 +450,6 @@ const globalQueries = { 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, - }, - }, ], }; @@ -540,34 +459,6 @@ const teamQueries = { 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", - }, - { - 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;", @@ -585,14 +476,14 @@ const teamQueries = { author_email: "jacob@fleetdm.com", packs: [], stats: { - system_time_p50: 4, + system_time_p50: 1, // system_time_p95: null, - user_time_p50: 10, + user_time_p50: 1, // user_time_p95: null, total_executions: 1, }, performance: "Undetermined", - platforms: ["darwin"], + platforms: ["windows", "darwin", "linux"], }, ], }; @@ -600,11 +491,7 @@ const teamQueries = { 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, @@ -614,10 +501,6 @@ export default { globalQuery1, globalQuery2, globalQuery3, - globalQuery4, - globalQuery5, - globalQuery6, teamQueries, teamQuery1, - teamQuery2, }; diff --git a/frontend/styles/var/colors.scss b/frontend/styles/var/colors.scss index a16b2fa847..652b967043 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -11,7 +11,6 @@ $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; From feddda299bd09886a2e7237b6d2e6ab96866e6eb Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:10:45 -0400 Subject: [PATCH 68/78] Fleet UI: New manage query automations modal (#12747) --- changes/12645-manage-query-automations | 1 + frontend/__mocks__/scheduleableQueryMock.ts | 12 +- .../LogDestinationIndicator.stories.tsx | 17 ++ .../LogDestinationIndicator.tsx | 86 ++++++++ .../LogDestinationIndicator/index.ts | 1 + frontend/components/Modal/Modal.tsx | 1 + .../QueryFrequencyIndicator.stories.tsx | 18 ++ .../QueryFrequencyIndicator.tsx | 73 +++++++ .../QueryFrequencyIndicator/_styles.scss | 13 ++ .../QueryFrequencyIndicator/index.ts | 1 + frontend/components/icons/Clock.tsx | 39 ++++ frontend/components/icons/Warning.tsx | 33 +++ frontend/components/icons/index.ts | 4 + frontend/interfaces/query.ts | 6 +- .../ManageQueriesPage/ManageQueriesPage.tsx | 116 +++++++++-- .../ManageAutomationsModal.tsx | 196 +++++++++++++++++- .../ManageAutomationsModal/_styles.scss | 48 +++++ .../PreviewDataModal/PreviewDataModal.tsx | 61 ++++++ .../components/PreviewDataModal/index.ts | 1 + .../QueriesTable/QueriesTableConfig.tsx | 1 + .../services/mock_service/mocks/config.ts | 6 +- .../services/mock_service/mocks/responses.ts | 124 ++++++++++- frontend/styles/var/colors.scss | 1 + 23 files changed, 826 insertions(+), 33 deletions(-) create mode 100644 changes/12645-manage-query-automations create mode 100644 frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx create mode 100644 frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx create mode 100644 frontend/components/LogDestinationIndicator/index.ts create mode 100644 frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx create mode 100644 frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx create mode 100644 frontend/components/QueryFrequencyIndicator/_styles.scss create mode 100644 frontend/components/QueryFrequencyIndicator/index.ts create mode 100644 frontend/components/icons/Clock.tsx create mode 100644 frontend/components/icons/Warning.tsx create mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss create mode 100644 frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx create mode 100644 frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts 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/frontend/__mocks__/scheduleableQueryMock.ts b/frontend/__mocks__/scheduleableQueryMock.ts index e437edc121..edc82a61da 100644 --- a/frontend/__mocks__/scheduleableQueryMock.ts +++ b/frontend/__mocks__/scheduleableQueryMock.ts @@ -10,7 +10,7 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { description: "A test query", query: "SELECT * FROM users", team_id: null, - interval: 3600, + interval: 43200, // Every 12 hours platform: "darwin,windows,linux", min_osquery_version: "", automations_enabled: true, @@ -22,11 +22,11 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { observer_can_run: false, packs: [], stats: { - system_time_p50: 1, - system_time_p95: 1, - user_time_p50: 1, - user_time_p95: 1, - total_executions: 3, + system_time_p50: 28.1053, + system_time_p95: 397.6667, + user_time_p50: 29.9412, + user_time_p95: 251.4615, + total_executions: 5746, }, }; 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/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..fb6624429f --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/_styles.scss @@ -0,0 +1,13 @@ +.query-frequency-indicator { + width: 100px; + display: flex; + 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/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/interfaces/query.ts b/frontend/interfaces/query.ts index 96a8efa4d1..d6a948cd25 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -1,5 +1,6 @@ import { IFormField } from "./form_field"; import { IPack } from "./pack"; +import { ISchedulableQuery } from "./schedulable_query"; import { IScheduledQueryStats } from "./scheduled_query_stats"; export interface IQueryFormData { @@ -7,14 +8,15 @@ export interface IQueryFormData { 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/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 449ad6c34d..58e124b908 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -32,7 +32,8 @@ 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"; +import ManageAutomationsModal from "./components/ManageAutomationsModal/ManageAutomationsModal"; +import PreviewDataModal from "./components/PreviewDataModal/PreviewDataModal"; const baseClass = "manage-queries-page"; interface IManageQueriesPageProps { @@ -84,6 +85,7 @@ const ManageQueriesPage = ({ filteredQueriesPath, isPremiumTier, isSandboxMode, + config, } = useContext(AppContext); const { setResetSelectedRows } = useContext(TableContext); @@ -106,11 +108,13 @@ const ManageQueriesPage = ({ const [selectedQueryIds, setSelectedQueryIds] = useState([]); const [showDeleteQueryModal, setShowDeleteQueryModal] = useState(false); - const [isUpdatingQueries, setIsUpdatingQueries] = 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: curTeamEnhancedQueries, @@ -159,6 +163,14 @@ const ManageQueriesPage = ({ } ); + const automatedQueryIds = useMemo(() => { + return curTeamEnhancedQueries + ? curTeamEnhancedQueries + .filter((query) => query.automations_enabled) + .map((query) => query.id) + : []; + }, [curTeamEnhancedQueries]); + useEffect(() => { const path = location.pathname + location.search; if (filteredQueriesPath !== path) { @@ -172,10 +184,6 @@ const ManageQueriesPage = ({ setShowDeleteQueryModal(!showDeleteQueryModal); }, [showDeleteQueryModal, setShowDeleteQueryModal]); - const toggleManageAutomationsModal = useCallback(() => { - setShowManageAutomationsModal(!showManageAutomationsModal); - }, [showManageAutomationsModal, setShowManageAutomationsModal]); - const onDeleteQueryClick = (selectedTableQueryIds: number[]) => { toggleDeleteQueryModal(); setSelectedQueryIds(selectedTableQueryIds); @@ -186,6 +194,25 @@ const ManageQueriesPage = ({ 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); @@ -312,6 +339,52 @@ const ManageQueriesPage = ({ ); }; + 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 = []; + updateAutomatedQueries.push( + turnOnAutomations.map((id: number) => + queriesAPI.update(id, { automations_enabled: true }) + ) + ); + updateAutomatedQueries.push( + turnOffAutomations.map((id: number) => + 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 ( <> @@ -323,7 +396,18 @@ const ManageQueriesPage = ({ /> )} {showManageAutomationsModal && ( - + + )} + {showPreviewDataModal && ( + )} ); @@ -341,7 +425,7 @@ const ManageQueriesPage = ({
{(isGlobalAdmin || isTeamAdmin) && ( + <> + + )}
diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index cb6abd9cb8..b5cfea5899 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -1,19 +1,203 @@ -import React from "react"; +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"; -const baseClass = "automations-modal"; +import { ISchedulableQuery } from "interfaces/schedulable_query"; +interface IFrequencyIndicator { + frequency: number; + checked: boolean; +} interface IManageAutomationsModalProps { - onExit: () => void; + 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 = ({ - onExit, + isUpdatingAutomations, + automatedQueryIds, + handleSubmit, + onCancel, + togglePreviewDataModal, + availableQueries, + logDestination, }: IManageAutomationsModalProps): JSX.Element => { + // TODO: Error handling, if any + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + + const { queryItems, updateQueryItems } = useCheckboxListStateManagement( + availableQueries || [], + 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?   + +
+
+
+ +
+
+ + +
+
+
); }; 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..296b36f7aa --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss @@ -0,0 +1,48 @@ +.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; + } + } + + .fleet-checkbox { + height: 20px; + + &__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/PreviewDataModal/PreviewDataModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx new file mode 100644 index 0000000000..3156c73288 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx @@ -0,0 +1,61 @@ +/* This component is used for creating and editing both global and team scheduled queries */ + +import React from "react"; +import { syntaxHighlight } from "utilities/helpers"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; +import TooltipWrapper from "components/TooltipWrapper"; + +const baseClass = "preview-data-modal"; + +interface IPreviewDataModalProps { + onCancel: () => void; +} + +const PreviewDataModal = ({ + onCancel, +}: IPreviewDataModalProps): JSX.Element => { + const json = { + action: "snapshot", + snapshot: [ + { + remote_address: "0.0.0.0", + remote_port: "0", + cmdline: "/usr/sbin/syslogd", + }, + ], + name: "xxxxxxx", + hostIdentifier: "xxxxxxx", + calendarTime: "xxx xxx x xx:xx:xx xxxx UTC", + unixTime: "xxxxxxxxx", + epoch: "xxxxxxxxx", + counter: "x", + numerics: "x", + }; + + return ( + +
+

+ + The data sent to your configured log destination will look similar + to the following JSON: + +

+
+
+        
+
+ +
+
+
+ ); +}; + +export default PreviewDataModal; diff --git a/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts new file mode 100644 index 0000000000..48fca40136 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts @@ -0,0 +1 @@ +export { default } from "./PreviewDataModal"; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 3af8a75a7f..744a829bc6 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -193,6 +193,7 @@ const generateTableHeaders = ({ }, }, { + title: "Performance impact", Header: () => { return (
diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index 15a3b1a6b4..50ebcbf1f9 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -28,7 +28,11 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { "queries/2": RESPONSES.globalQuery2, "queries/3": RESPONSES.globalQuery3, "queries/4": RESPONSES.teamQuery1, - "queries?team_id=43": RESPONSES.teamQueries, + "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 diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index fabc92bacb..d20d275f8f 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -431,6 +431,60 @@ const globalQueries = { 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: "", @@ -450,6 +504,33 @@ const globalQueries = { 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, + }, + }, ], }; @@ -459,6 +540,35 @@ const teamQueries = { 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;", @@ -476,14 +586,14 @@ const teamQueries = { author_email: "jacob@fleetdm.com", packs: [], stats: { - system_time_p50: 1, + system_time_p50: 4, // system_time_p95: null, - user_time_p50: 1, + user_time_p50: 10, // user_time_p95: null, total_executions: 1, }, performance: "Undetermined", - platforms: ["windows", "darwin", "linux"], + platforms: ["darwin"], }, ], }; @@ -491,7 +601,11 @@ const teamQueries = { 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, @@ -501,6 +615,10 @@ export default { 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; From 0b503a61823621acaa2c499933598049588364b1 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Thu, 20 Jul 2023 16:31:36 -0700 Subject: [PATCH 69/78] style fixes --- .../QueryFrequencyIndicator/_styles.scss | 1 + .../ManageAutomationsModal.tsx | 21 ++++++++----------- .../ManageAutomationsModal/_styles.scss | 17 +++++++++++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/frontend/components/QueryFrequencyIndicator/_styles.scss b/frontend/components/QueryFrequencyIndicator/_styles.scss index fb6624429f..f5b5f74d01 100644 --- a/frontend/components/QueryFrequencyIndicator/_styles.scss +++ b/frontend/components/QueryFrequencyIndicator/_styles.scss @@ -1,6 +1,7 @@ .query-frequency-indicator { width: 100px; display: flex; + align-items: center; padding: 8px 12px; .icon { diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index b5cfea5899..107267c8e3 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -11,10 +11,6 @@ import LogDestinationIndicator from "components/LogDestinationIndicator/LogDesti import { ISchedulableQuery } from "interfaces/schedulable_query"; -interface IFrequencyIndicator { - frequency: number; - checked: boolean; -} interface IManageAutomationsModalProps { isUpdatingAutomations: boolean; handleSubmit: (formData: any) => void; // TODO @@ -163,14 +159,15 @@ const ManageAutomationsModal = ({
- Automations currently run on macOS, Windows, and Linux hosts. -
- Interested in query automations for your Chromebooks?   - +

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

+

+ Interested in query automations for your Chromebooks?   + +

diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss index 296b36f7aa..78a5f043f7 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss @@ -26,8 +26,25 @@ } } + &__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; From 8b5f7fbae9a5cd36edd2e45f10e9738f297769c4 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 24 Jul 2023 10:46:52 -0400 Subject: [PATCH 70/78] Fleet UI: Queries default to alpha order (#12924) --- .../ManageAutomationsModal/ManageAutomationsModal.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index 107267c8e3..0e9d5daae1 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -66,8 +66,14 @@ const ManageAutomationsModal = ({ // 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( - availableQueries || [], + sortedAvailableQueries, automatedQueryIds || [] ); From 63c7c8a3ae081addaec891fe41818d851a115236 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 27 Jul 2023 16:55:07 -0400 Subject: [PATCH 71/78] If no global queries, show proper message at the end (#13006) If no global queries, show proper message at the end --- cmd/fleetctl/get.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 94ed9f06c0..4700715349 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -331,23 +331,29 @@ func queryToTableRow(query fleet.Query, teamName string) []string { } } -func printInheritedQueries(client *service.Client, teamID *uint) error { +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) } - fmt.Printf("Not showing %d inherited queries. To see global queries, run this command without the `--team` flag.\n", len(globalQueries)) + + 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 printNoQueriesFound(teamID *uint) { - scope := "global" +func printNoQueriesFoundMsg(teamID *uint) { if teamID != nil { - scope = "team" + fmt.Println("No team queries found.") + return } - fmt.Printf("No %s queries found.\n", scope) + fmt.Println("No global queries found.") + fmt.Println("To see team queries, run this command with the --team flag.") } func getQueriesCommand() *cli.Command { @@ -424,8 +430,8 @@ func getQueriesCommand() *cli.Command { } if len(queries) == 0 { - printNoQueriesFound(teamID) - if err := printInheritedQueries(client, teamID); err != nil { + printNoQueriesFoundMsg(teamID) + if err := printInheritedQueriesMsg(client, teamID); err != nil { return err } return nil @@ -459,7 +465,7 @@ func getQueriesCommand() *cli.Command { } printQueryTable(c, columns, rows) - if err := printInheritedQueries(client, teamID); err != nil { + if err := printInheritedQueriesMsg(client, teamID); err != nil { return err } } From 38b44076713c9acdd481ac53fb0d69eea54cc1cd Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 27 Jul 2023 14:32:58 -0700 Subject: [PATCH 72/78] =?UTF-8?q?UI=20=E2=80=93=C2=A0prevent=20vertical=20?= =?UTF-8?q?scrolling=20for=20single=20queries=20(#12985)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #12976 Prevent vertical scrolling when only a single query is present while maintaining horizontal scrolling in presence of queries with long names: Screenshot 2023-07-26 at 2 26 03 PM - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- .../forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx | 1 - frontend/pages/queries/ManageQueriesPage/_styles.scss | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) 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/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 722bd10239..2eb5f294fc 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -101,6 +101,7 @@ .data-table { &__wrapper { overflow-x: scroll; + overflow-y: hidden; } &__table { thead { From 791adf19ad4b1e8b9865e15acd34cfc984c546ae Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 27 Jul 2023 14:44:03 -0700 Subject: [PATCH 73/78] UI - Add timeout before refetching queries (#13009) ## Addresses #13007 Avoid race condition by waiting before queries refetch after updating automations Co-authored-by: Jacob Shandling --- frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 58e124b908..37527b4a52 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -368,7 +368,8 @@ const ManageQueriesPage = ({ try { await Promise.all(updateAutomatedQueries).then(() => { renderFlash("success", `Successfully updated query automations.`); - refetchAllQueries(); + // allow time for backend to update before refetching + setTimeout(refetchAllQueries, 10); }); } catch (errorResponse) { renderFlash( From 40e8f83829d9f1d3088903abeef300d2de6b7642 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Fri, 28 Jul 2023 12:41:05 -0400 Subject: [PATCH 74/78] Always use writer node when listing queries. (#13024) Always use writer node when listing queries. --- .../pages/queries/ManageQueriesPage/ManageQueriesPage.tsx | 3 +-- server/datastore/mysql/queries.go | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 37527b4a52..58e124b908 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -368,8 +368,7 @@ const ManageQueriesPage = ({ try { await Promise.all(updateAutomatedQueries).then(() => { renderFlash("success", `Successfully updated query automations.`); - // allow time for backend to update before refetching - setTimeout(refetchAllQueries, 10); + refetchAllQueries(); }); } catch (errorResponse) { renderFlash( diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index 1ea22ef2bf..f757d2f442 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -388,7 +388,7 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions sql = appendListOptionsToSQL(sql, &opt.ListOptions) results := []*fleet.Query{} - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, args...); err != nil { + if err := sqlx.SelectContext(ctx, ds.writer(ctx), &results, sql, args...); err != nil { return nil, ctxerr.Wrap(ctx, err, "listing queries") } @@ -434,7 +434,7 @@ func (ds *Datastore) loadPacksForQueries(ctx context.Context, queries []*fleet.Q fleet.Pack }{} - err = sqlx.SelectContext(ctx, ds.reader(ctx), &rows, query, args...) + err = sqlx.SelectContext(ctx, ds.writer(ctx), &rows, query, args...) if err != nil { return ctxerr.Wrap(ctx, err, "selecting load packs for queries") } From 37203dbad2f2e98a5a253cf0b105dbc895a2696d Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:27:12 -0700 Subject: [PATCH 75/78] UI - Restore packs logic to host details (#13031) ## Addresses ##13032 ### Restore packs logic per product decision Screenshot 2023-07-28 at 12 12 48 PM - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- .../HostDetailsPage/HostDetailsPage.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index a982fac447..3e6e35d811 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -18,12 +18,14 @@ import { IHost, IDeviceMappingResponse, IMacadminsResponse, + IPackStats, IHostResponse, IHostMdmData, IPackStats, } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { IHostPolicy } from "interfaces/policy"; +import { IQueryStats } from "interfaces/query_stats"; import { ISoftware } from "interfaces/software"; import { ITeam } from "interfaces/team"; import { @@ -40,6 +42,7 @@ import InfoBanner from "components/InfoBanner"; import BackLink from "components/BackLink"; import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers"; +import permissions from "utilities/permissions"; import HostSummaryCard from "../cards/HostSummary"; import AboutCard from "../cards/About"; @@ -50,6 +53,7 @@ import SoftwareCard from "../cards/Software"; import UsersCard from "../cards/Users"; import PoliciesCard from "../cards/Policies"; import ScheduleCard from "../cards/Schedule"; +import PacksCard from "../cards/Packs"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; import OSPolicyModal from "./modals/OSPolicyModal"; import UnenrollMdmModal from "./modals/UnenrollMdmModal"; @@ -115,7 +119,9 @@ const HostDetailsPage = ({ const { config, + currentUser, isGlobalAdmin = false, + isGlobalObserver, isPremiumTier = false, isSandboxMode, isOnlyObserver, @@ -152,6 +158,7 @@ const HostDetailsPage = ({ const [refetchStartTime, setRefetchStartTime] = useState(null); const [showRefetchSpinner, setShowRefetchSpinner] = useState(false); const [schedule, setSchedule] = useState(); + const [packsState, setPacksState] = useState(); const [hostSoftware, setHostSoftware] = useState([]); const [usersState, setUsersState] = useState<{ username: string }[]>([]); const [usersSearchString, setUsersSearchString] = useState(""); @@ -313,6 +320,7 @@ const HostDetailsPage = ({ { packs: [], schedule: [] } ); setSchedule(packStatsByType.schedule); + setPacksState(packStatsByType.packs); } }, onError: (error) => handlePageError(error), @@ -614,6 +622,23 @@ const HostDetailsPage = ({ host?.mdm.name === "Fleet" && host?.mdm.macos_settings?.disk_encryption === "action_required"; + /* Context team id might be different that host's team id + Observer plus must be checked against host's team id */ + const isGlobalOrHostsTeamObserverPlus = + currentUser && host?.team_id + ? permissions.isObserverPlus(currentUser, host.team_id) + : false; + + const isHostsTeamObserver = + currentUser && host?.team_id + ? permissions.isTeamObserver(currentUser, host.team_id) + : false; + + const canViewPacks = + !isGlobalObserver && + !isGlobalOrHostsTeamObserverPlus && + !isHostsTeamObserver; + const bootstrapPackageData = { status: host?.mdm.macos_setup?.bootstrap_package_status, details: host?.mdm.macos_setup?.details, @@ -726,6 +751,9 @@ const HostDetailsPage = ({ schedule={schedule} isLoading={isLoadingHost} /> + {canViewPacks && ( + + )} Date: Fri, 28 Jul 2023 13:01:50 -0700 Subject: [PATCH 76/78] UI - Fix a synchronicity issue in automations API patches (#13035) ## Addresses #13007 ## Fix a synchronicity issue in automations API patches Screenshot 2023-07-28 at 12 49 21 PM - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- .../hosts/details/HostDetailsPage/HostDetailsPage.tsx | 2 -- .../queries/ManageQueriesPage/ManageQueriesPage.tsx | 10 +++++----- server/datastore/mysql/queries.go | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 3e6e35d811..f121702357 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -18,7 +18,6 @@ import { IHost, IDeviceMappingResponse, IMacadminsResponse, - IPackStats, IHostResponse, IHostMdmData, IPackStats, @@ -33,7 +32,6 @@ import { IQueryKeyQueriesLoadAll, ISchedulableQuery, } from "interfaces/schedulable_query"; -import { IQueryStats } from "interfaces/query_stats"; import Spinner from "components/Spinner"; import TabsWrapper from "components/TabsWrapper"; diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 58e124b908..348952b6b9 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -353,14 +353,14 @@ const ManageQueriesPage = ({ ); // Update query automations using queries/{id} manage_automations parameter - const updateAutomatedQueries = []; - updateAutomatedQueries.push( - turnOnAutomations.map((id: number) => + const updateAutomatedQueries: Promise[] = []; + turnOnAutomations.map((id: number) => + updateAutomatedQueries.push( queriesAPI.update(id, { automations_enabled: true }) ) ); - updateAutomatedQueries.push( - turnOffAutomations.map((id: number) => + turnOffAutomations.map((id: number) => + updateAutomatedQueries.push( queriesAPI.update(id, { automations_enabled: false }) ) ); diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index f757d2f442..1ea22ef2bf 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -388,7 +388,7 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions sql = appendListOptionsToSQL(sql, &opt.ListOptions) results := []*fleet.Query{} - if err := sqlx.SelectContext(ctx, ds.writer(ctx), &results, sql, args...); err != nil { + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, args...); err != nil { return nil, ctxerr.Wrap(ctx, err, "listing queries") } @@ -434,7 +434,7 @@ func (ds *Datastore) loadPacksForQueries(ctx context.Context, queries []*fleet.Q fleet.Pack }{} - err = sqlx.SelectContext(ctx, ds.writer(ctx), &rows, query, args...) + err = sqlx.SelectContext(ctx, ds.reader(ctx), &rows, query, args...) if err != nil { return ctxerr.Wrap(ctx, err, "selecting load packs for queries") } From 0c0ff35a37b8e4b28911c3b7c804b08294d9cc84 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Fri, 28 Jul 2023 13:28:44 -0700 Subject: [PATCH 77/78] changes --- changes/7765-combine-schedules-and-queries | 1 + ...hedules-and-queries => 7765-queries-schedules-schema-updates} | 0 2 files changed, 1 insertion(+) create mode 100644 changes/7765-combine-schedules-and-queries rename changes/{7765-combined-schedules-and-queries => 7765-queries-schedules-schema-updates} (100%) 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-combined-schedules-and-queries b/changes/7765-queries-schedules-schema-updates similarity index 100% rename from changes/7765-combined-schedules-and-queries rename to changes/7765-queries-schedules-schema-updates From 84c22e57c639b05c4173657eb9562d94ba8d9db3 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Fri, 28 Jul 2023 13:57:14 -0700 Subject: [PATCH 78/78] revert doc to allow full feature merge --- docs/Using Fleet/manage-access.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/Using Fleet/manage-access.md b/docs/Using Fleet/manage-access.md index 3d260d3f2a..b5e30ab384 100644 --- a/docs/Using Fleet/manage-access.md +++ b/docs/Using Fleet/manage-access.md @@ -10,12 +10,12 @@ Users with the admin role receive all permissions. ### Maintainer -Maintainers can manage most entities in Fleet, like queries, policies and labels. +Maintainers can manage most entities in Fleet, like queries, policies, labels and schedules. Unlike admins, maintainers cannot edit higher level settings like application configuration, teams or users. ### Observer -The Observer role is a read-only role. It can access most entities in Fleet, like queries, policies, labels, application configuration, teams, etc. +The Observer role is a read-only role. It can access most entities in Fleet, like queries, policies, labels, schedules, application configuration, teams, etc. They can also run queries configured with the `observer_can_run` flag set to `true`. ### Observer+ @@ -51,6 +51,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. | Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) against all hosts | | ✅ | ✅ | ✅ | | | Create, edit, and delete queries | | | ✅ | ✅ | ✅ | | View all queries\** | ✅ | ✅ | ✅ | ✅ | | +| Add, edit, and remove queries from all schedules | | | ✅ | ✅ | ✅ | | Create, edit, view, and delete packs | | | ✅ | ✅ | ✅ | | View all policies | ✅ | ✅ | ✅ | ✅ | | | Filter hosts using policies | ✅ | ✅ | ✅ | ✅ | | @@ -99,11 +100,11 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. Users in Fleet either have team access or global access. -Users with team access only have access to the [hosts](https://fleetdm.com/docs/using-fleet/rest-api#hosts), [software](https://fleetdm.com/docs/using-fleet/rest-api#software), and [policies](https://fleetdm.com/docs/using-fleet/rest-api#policies) assigned to +Users with team access only have access to the [hosts](https://fleetdm.com/docs/using-fleet/rest-api#hosts), [software](https://fleetdm.com/docs/using-fleet/rest-api#software), [schedules](https://fleetdm.com/docs/using-fleet/fleet-ui#schedule-a-query) , and [policies](https://fleetdm.com/docs/using-fleet/rest-api#policies) assigned to their team. Users with global access have access to all -[hosts](https://fleetdm.com/docs/using-fleet/rest-api#hosts), [software](https://fleetdm.com/docs/using-fleet/rest-api#software), [queries](https://fleetdm.com/docs/using-fleet/rest-api#queries), and [policies](https://fleetdm.com/docs/using-fleet/rest-api#policies). Check out [the user permissions +[hosts](https://fleetdm.com/docs/using-fleet/rest-api#hosts), [software](https://fleetdm.com/docs/using-fleet/rest-api#software), [queries](https://fleetdm.com/docs/using-fleet/rest-api#queries), [schedules](https://fleetdm.com/docs/using-fleet/fleet-ui#schedule-a-query) , and [policies](https://fleetdm.com/docs/using-fleet/rest-api#policies). Check out [the user permissions table](#user-permissions) above for global user permissions. Users can be a member of multiple teams in Fleet. @@ -119,10 +120,11 @@ Users that are members of multiple teams can be assigned different roles for eac | Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ | ✅ | | | Filter hosts by software | ✅ | ✅ | ✅ | ✅ | | | Filter software | ✅ | ✅ | ✅ | ✅ | | -| Run global and team queries designated "**observer can run**" as live queries against hosts | ✅ | ✅ | ✅ | ✅ | | +| Run queries designated "**observer can run**" as live queries against hosts | ✅ | ✅ | ✅ | ✅ | | | Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) | | ✅ | ✅ | ✅ | | -| Create, edit, and delete team queries | | | ✅ | ✅ | ✅ | +| Create, edit, and delete only **self authored** queries | | | ✅ | ✅ | ✅ | | View all queries\** | ✅ | ✅ | ✅ | ✅ | | +| Add, edit, and remove queries from the schedule | | | ✅ | ✅ | ✅ | | View policies | ✅ | ✅ | ✅ | ✅ | | | View global (inherited) policies | ✅ | ✅ | ✅ | ✅ | | | Run global (inherited) policies as a live policy | | | ✅ | ✅ | |