From 825a36b0ba1939ff646ab495f84325bc8aedf532 Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:59:00 -0500 Subject: [PATCH 01/19] Update digital-experience.rituals.yml (#22521) --- handbook/demand/README.md | 14 -------------- handbook/digital-experience/README.md | 14 ++++++++++++++ .../digital-experience.rituals.yml | 10 ++++++++++ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/handbook/demand/README.md b/handbook/demand/README.md index 69dd7a2d843f..9d3f6eb5d561 100644 --- a/handbook/demand/README.md +++ b/handbook/demand/README.md @@ -31,20 +31,6 @@ Fleet's public relations firm is directly responsible for the accuracy of event 2. Update the workbook with the latest location, dates, and CFP deadlines from the website. -### Respond to a "Contact us" submission - -1. Check the [_from-prospective-customers](https://fleetdm.slack.com/archives/C01HE9GQW6B) Slack channel for "Contact us" submissions. -2. Mark submission as seen with the "πŸ‘€" emoji. -3. Within 4 business hours, use the [best practices template (private Google doc)](https://docs.google.com/document/d/1D02k0tc5v-sEJ4uahAouuqnvZ6phxA_gP-IqmkBdMTE/edit) to respond to general asks. -4. Answer any technical questions to the best of your ability. If you are unable to answer a technical/product question, ask a Solutions Consultant in `#help-solutions-consulting`. If an SC is unavailable, post in `#g-mdm`or `#g-endpoint-ops`and notify @on-call. -5. log in to [Salesforce](https://fleetdm.lightning.force.com/lightning/o/Lead/list?filterName=00B4x00000DtaRDEAZ) and search the lead list by first name and match the corresponding email to find the right lead. -6. Enrich each lead with company information and buying situation. -7. If a lead is completed or out of ICP, update the lead status in Salesforce to "Closed" or "Disqualified". If within ICP at-mention the [Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team) in the [#g-digital-experience](https://fleetdm.slack.com/archives/C058S8PFSK0) Slack channel and move lead to their name in SFDC. -8. Mark the Slack message as complete with the "βœ…" emoji. - -> For any support-related questions, forward the submission to [Fleet's support team](https://docs.google.com/document/d/1tE-NpNfw1icmU2MjYuBRib0VWBPVAdmq4NiCrpuI0F0/edit#heading=h.wqalwz1je6rq). - - ### Begin or modify an advertising campaign Any new ads or changes to current running ads are approved in ["πŸ¦’πŸ—£ Design review (#g-digital-experience)"](https://app.zenhub.com/workspaces/-g-digital-experience-6451748b4eb15200131d4bab/board?sprints=none). diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md index 34f77f2b9fe3..b634a973ec1f 100644 --- a/handbook/digital-experience/README.md +++ b/handbook/digital-experience/README.md @@ -765,6 +765,20 @@ Fleet has several brand fronts that need to be updated from time to time. Check - The current [brand imagery](https://www.figma.com/design/1J2yxqH8Q7u8V7YTtA1iej/Social-media-(logos%2C-covers%2C-banners)?node-id=3962-65895). Check this [Loom video](https://www.loom.com/share/4432646cc9614046aaa4a74da1c0adb5?sid=2f84779f-f0bd-4055-be69-282c5a16f5c5) for more info. +### Respond to a "Contact us" submission + +1. Check the [_from-prospective-customers](https://fleetdm.slack.com/archives/C01HE9GQW6B) Slack channel for "Contact us" submissions. +2. Mark submission as seen with the "πŸ‘€" emoji. +3. Within 4 business hours, use the [best practices template (private Google doc)](https://docs.google.com/document/d/1D02k0tc5v-sEJ4uahAouuqnvZ6phxA_gP-IqmkBdMTE/edit) to respond to general asks. +4. Answer any technical questions to the best of your ability. If you are unable to answer a technical/product question, ask a Solutions Consultant in `#help-solutions-consulting`. If an SC is unavailable, post in `#g-mdm`or `#g-endpoint-ops`and notify @on-call. +5. log in to [Salesforce](https://fleetdm.lightning.force.com/lightning/o/Lead/list?filterName=00B4x00000DtaRDEAZ) and search the lead list by first name and match the corresponding email to find the right lead. +6. Enrich each lead with company information and buying situation. +7. If a lead is completed or out of ICP, update the lead status in Salesforce to "Closed" or "Disqualified". If within ICP at-mention the [Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team) in the [#g-digital-experience](https://fleetdm.slack.com/archives/C058S8PFSK0) Slack channel and move lead to their name in SFDC. +8. Mark the Slack message as complete with the "βœ…" emoji. + +> For any support-related questions, forward the submission to [Fleet's support team](https://docs.google.com/document/d/1tE-NpNfw1icmU2MjYuBRib0VWBPVAdmq4NiCrpuI0F0/edit#heading=h.wqalwz1je6rq). + + ## Rituals - Note: Some rituals (⏰) are especially time-sensitive and require attention multiple times (3+) per day. Set reminders for the following times (CT): diff --git a/handbook/digital-experience/digital-experience.rituals.yml b/handbook/digital-experience/digital-experience.rituals.yml index c36e75db31ec..f7f1f8d5aedc 100644 --- a/handbook/digital-experience/digital-experience.rituals.yml +++ b/handbook/digital-experience/digital-experience.rituals.yml @@ -9,6 +9,16 @@ autoIssue: labels: [ "#g-digital-experience" ] repo: "fleet" +- + task: "Confirm consultant hours" + startedOn: "2024-09-30" + frequency: "Weekly" + description: "Perform step three in β€œInform managers about hours worked” responsibility" + moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#inform-managers-about-hours-worked" + dri: "SFriendLee" + autoIssue: + labels: [ "#g-digital-experience" ] + repo: "fleet" - task: "Prep 1:1s for OKR planning" startedOn: "2024-09-09" From 658431e17fd76f7cdda9f3d3bec9d0a31fc0e052 Mon Sep 17 00:00:00 2001 From: Tim Lee Date: Mon, 30 Sep 2024 15:39:17 -0600 Subject: [PATCH 02/19] Query optimization on Hosts query stats (#22417) --- changes/22094-query-optimization | 1 + server/datastore/mysql/hosts.go | 23 +++- ...40930171917_AddScheduleAutomationsIndex.go | 33 ++++++ ...171917_AddScheduleAutomationsIndex_test.go | 104 ++++++++++++++++++ server/datastore/mysql/schema.sql | 6 +- tools/seed_data/queries/seed_queries.go | 74 +++++++++++++ .../seed_data/{ => vulnerabilities}/README.md | 0 .../{ => vulnerabilities}/seed_vuln_data.go | 0 .../{ => vulnerabilities}/software-macos.csv | 0 .../{ => vulnerabilities}/software-ubuntu.csv | 0 .../{ => vulnerabilities}/software-win.csv | 0 11 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 changes/22094-query-optimization create mode 100644 server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex.go create mode 100644 server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex_test.go create mode 100644 tools/seed_data/queries/seed_queries.go rename tools/seed_data/{ => vulnerabilities}/README.md (100%) rename tools/seed_data/{ => vulnerabilities}/seed_vuln_data.go (100%) rename tools/seed_data/{ => vulnerabilities}/software-macos.csv (100%) rename tools/seed_data/{ => vulnerabilities}/software-ubuntu.csv (100%) rename tools/seed_data/{ => vulnerabilities}/software-win.csv (100%) diff --git a/changes/22094-query-optimization b/changes/22094-query-optimization new file mode 100644 index 000000000000..bd779b451346 --- /dev/null +++ b/changes/22094-query-optimization @@ -0,0 +1 @@ +- Increased performance for Host details and Fleet Desktop, particularly in environments using high volumes of live queries \ No newline at end of file diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index ee23749de80a..6026dcd9b140 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -403,13 +403,17 @@ func loadHostPackStatsDB(ctx context.Context, db sqlx.QueryerContext, hid uint, return ps, nil } +// loadHostScheduledQueryStatsDB will load all the scheduled query stats for the given host. +// The filter is split into two statements joined by a UNION ALL to take advantage of indexes. +// Using an OR in the WHERE clause causes a full table scan which causes issues with a large +// queries table due to the high volume of live queries (created by zero trust workflows) 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 } - sqlQuery := ` + baseQuery := ` SELECT q.id, q.name, @@ -442,12 +446,19 @@ func loadHostScheduledQueryStatsDB(ctx context.Context, db sqlx.QueryerContext, SUM(stats.wall_time) AS wall_time FROM scheduled_query_stats stats WHERE stats.host_id = ? GROUP BY stats.scheduled_query_id) as sqs ON (q.id = sqs.scheduled_query_id) LEFT JOIN query_results qr ON (q.id = qr.query_id AND qr.host_id = ?) + ` + + filter1 := ` WHERE (q.platform = '' OR q.platform IS NULL OR FIND_IN_SET(?, q.platform) != 0) - AND q.schedule_interval > 0 + AND q.is_scheduled = 1 AND (q.automations_enabled IS TRUE OR (q.discard_data IS FALSE AND q.logging_type = ?)) AND (q.team_id IS NULL OR q.team_id = ?) - OR EXISTS ( + GROUP BY q.id + ` + + filter2 := ` + WHERE EXISTS ( SELECT 1 FROM query_results WHERE query_results.query_id = q.id AND query_results.host_id = ? @@ -455,6 +466,8 @@ func loadHostScheduledQueryStatsDB(ctx context.Context, db sqlx.QueryerContext, GROUP BY q.id ` + sqlQuery := baseQuery + filter1 + " UNION ALL " + baseQuery + filter2 + args := []interface{}{ pastDate, hid, @@ -462,8 +475,12 @@ func loadHostScheduledQueryStatsDB(ctx context.Context, db sqlx.QueryerContext, fleet.PlatformFromHost(hostPlatform), fleet.LoggingSnapshot, teamID_, + pastDate, + hid, + hid, hid, } + var stats []fleet.QueryStats if err := sqlx.SelectContext(ctx, db, &stats, sqlQuery, args...); err != nil { return nil, ctxerr.Wrap(ctx, err, "load query stats") diff --git a/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex.go b/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex.go new file mode 100644 index 000000000000..592e8349df32 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex.go @@ -0,0 +1,33 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20240930171917, Down_20240930171917) +} + +func Up_20240930171917(tx *sql.Tx) error { + _, err := tx.Exec(` + ALTER TABLE queries + ADD COLUMN is_scheduled BOOLEAN GENERATED ALWAYS AS (schedule_interval > 0) STORED NOT NULL + `) + if err != nil { + return fmt.Errorf("error creating generated column is_scheduled: %w", err) + } + + _, err = tx.Exec(` + CREATE INDEX idx_queries_schedule_automations ON queries (is_scheduled, automations_enabled) + `) + if err != nil { + return fmt.Errorf("error creating index idx_queries_schedule_automations: %w", err) + } + + return nil +} + +func Down_20240930171917(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex_test.go b/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex_test.go new file mode 100644 index 000000000000..27f7d814c52b --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex_test.go @@ -0,0 +1,104 @@ +package tables + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUp_20240930171917(t *testing.T) { + db := applyUpToPrev(t) + + // + // Insert data to test the migration + // + // ... + + // Apply current migration. + applyNext(t, db) + + // Assert the index was created. + rows, err := db.Query("SHOW INDEX FROM queries WHERE Key_name = 'idx_queries_schedule_automations'") + require.NoError(t, err) + defer rows.Close() + + var indexCount int + for rows.Next() { + indexCount++ + } + + require.NoError(t, rows.Err()) + require.Greater(t, indexCount, 0) + + // + // Assert the index is used when there are rows in the queries table + // (wrong index is used when there are no rows in the queries table) + // + + stmtPrefix := "INSERT INTO `queries` (`saved`, `name`, `description`, `query`, `author_id`, `observer_can_run`, `team_id`, `team_id_char`, `platform`, `min_osquery_version`, `schedule_interval`, `automations_enabled`, `logging_type`, `discard_data`) VALUES " + stmtSuffix := ";" + + var valueStrings []string + var valueArgs []interface{} + + // Generate 10 records + for i := 0; i < 10; i++ { + queryID := i + 1 + valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + valueArgs = append(valueArgs, 0, fmt.Sprintf("query_%d", queryID), "", "SELECT * FROM processes;", 1, 0, nil, "", "", "", 0, 0, "snapshot", 0) + } + + // Disable foreign key checks to improve performance + _, err = db.Exec("SET FOREIGN_KEY_CHECKS=0") + require.NoError(t, err) + + // Construct and execute the batch insert + stmt := stmtPrefix + strings.Join(valueStrings, ",") + stmtSuffix + _, err = db.Exec(stmt, valueArgs...) + require.NoError(t, err) + + // Re-enable foreign key checks + _, err = db.Exec(`SET FOREIGN_KEY_CHECKS=1`) + require.NoError(t, err) + + result := struct { + ID int `db:"id"` + SelectType string `db:"select_type"` + Table string `db:"table"` + Type string `db:"type"` + PossibleKeys *string `db:"possible_keys"` + Key *string `db:"key"` + KeyLen *int `db:"key_len"` + Ref *string `db:"ref"` + Rows int `db:"rows"` + Filtered float64 `db:"filtered"` + Extra *string `db:"Extra"` + Partitions *string `db:"partitions"` + }{} + + // Query based on loadHostScheduledQueryStatsDB in server/datastore/mysql/hosts.go + err = db.Get(&result, ` + EXPLAIN + SELECT + q.id + FROM + queries q + WHERE (q.platform = '' + OR q.platform IS NULL + OR FIND_IN_SET('darwin', q.platform) != 0) + AND q.is_scheduled = 1 + AND(q.automations_enabled IS TRUE + OR(q.discard_data IS FALSE + AND q.logging_type = 'snapshot')) + AND(q.team_id IS NULL + OR q.team_id = 0) + GROUP BY + q.id +`) + require.NoError(t, err) + + // Assert the correct index is used + require.Equal(t, *result.Key, "idx_queries_schedule_automations") +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 3a5eef4003e5..58a5ec1eb4d3 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -1038,9 +1038,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=314 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=315 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20240927081858,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20240927081858,1,'2020-01-01 01:01:01'),(314,20240930171917,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( @@ -1459,11 +1459,13 @@ CREATE TABLE `queries` ( `automations_enabled` tinyint unsigned NOT NULL DEFAULT '0', `logging_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'snapshot', `discard_data` tinyint(1) NOT NULL DEFAULT '1', + `is_scheduled` tinyint(1) GENERATED ALWAYS AS ((`schedule_interval` > 0)) STORED NOT NULL, 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 `idx_team_id_saved_auto_interval` (`team_id`,`saved`,`automations_enabled`,`schedule_interval`), + KEY `idx_queries_schedule_automations` (`is_scheduled`,`automations_enabled`), 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 ) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/tools/seed_data/queries/seed_queries.go b/tools/seed_data/queries/seed_queries.go new file mode 100644 index 000000000000..857fe4b2b4f7 --- /dev/null +++ b/tools/seed_data/queries/seed_queries.go @@ -0,0 +1,74 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "strings" + + _ "github.com/go-sql-driver/mysql" +) + +const ( + batchSize = 1000 + totalRecords = 1000000 +) + +func main() { + // MySQL connection details from your Docker Compose file + user := "fleet" + password := "insecure" + host := "localhost" // Assuming you are running this script on the same host as Docker + port := "3306" + database := "fleet" + + // Construct the MySQL DSN (Data Source Name) + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, password, host, port, database) + + // Open MySQL connection + db, err := sql.Open("mysql", dsn) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + // Disable foreign key checks to improve performance + _, err = db.Exec("SET FOREIGN_KEY_CHECKS=0") + if err != nil { + log.Fatal(err) + } + + // Prepare the insert statement + stmtPrefix := "INSERT INTO `queries` (`saved`, `name`, `description`, `query`, `author_id`, `observer_can_run`, `team_id`, `team_id_char`, `platform`, `min_osquery_version`, `schedule_interval`, `automations_enabled`, `logging_type`, `discard_data`) VALUES " + stmtSuffix := ";" + + // Insert records in batches + for batch := 0; batch < totalRecords/batchSize; batch++ { + var valueStrings []string + var valueArgs []interface{} + + // Generate batch of 1000 records + for i := 0; i < batchSize; i++ { + queryID := batch*batchSize + i + 1 + valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + valueArgs = append(valueArgs, 0, fmt.Sprintf("query_%d", queryID), "", "SELECT * FROM processes;", 1, 0, nil, "", "", "", 0, 0, "snapshot", 0) + } + + // Construct and execute the batch insert + stmt := stmtPrefix + strings.Join(valueStrings, ",") + stmtSuffix + _, err := db.Exec(stmt, valueArgs...) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Inserted batch %d/%d\n", batch+1, totalRecords/batchSize) + } + + // Re-enable foreign key checks + _, err = db.Exec("SET FOREIGN_KEY_CHECKS=1") + if err != nil { + log.Fatal(err) + } + + fmt.Println("Finished inserting 1 million records.") +} diff --git a/tools/seed_data/README.md b/tools/seed_data/vulnerabilities/README.md similarity index 100% rename from tools/seed_data/README.md rename to tools/seed_data/vulnerabilities/README.md diff --git a/tools/seed_data/seed_vuln_data.go b/tools/seed_data/vulnerabilities/seed_vuln_data.go similarity index 100% rename from tools/seed_data/seed_vuln_data.go rename to tools/seed_data/vulnerabilities/seed_vuln_data.go diff --git a/tools/seed_data/software-macos.csv b/tools/seed_data/vulnerabilities/software-macos.csv similarity index 100% rename from tools/seed_data/software-macos.csv rename to tools/seed_data/vulnerabilities/software-macos.csv diff --git a/tools/seed_data/software-ubuntu.csv b/tools/seed_data/vulnerabilities/software-ubuntu.csv similarity index 100% rename from tools/seed_data/software-ubuntu.csv rename to tools/seed_data/vulnerabilities/software-ubuntu.csv diff --git a/tools/seed_data/software-win.csv b/tools/seed_data/vulnerabilities/software-win.csv similarity index 100% rename from tools/seed_data/software-win.csv rename to tools/seed_data/vulnerabilities/software-win.csv From 937627f4eab1cbc0591b3edcd40bdb41ddb04d72 Mon Sep 17 00:00:00 2001 From: Tim Lee Date: Mon, 30 Sep 2024 16:58:00 -0600 Subject: [PATCH 03/19] Windows Battery Status (#22455) --- changes/19619-win-battery | 1 + .../Contributing/Understanding-host-vitals.md | 16 +++- server/service/osquery_test.go | 1 + server/service/osquery_utils/queries.go | 75 ++++++++++++++++--- server/service/osquery_utils/queries_test.go | 39 +++++++++- 5 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 changes/19619-win-battery diff --git a/changes/19619-win-battery b/changes/19619-win-battery new file mode 100644 index 000000000000..124e58114048 --- /dev/null +++ b/changes/19619-win-battery @@ -0,0 +1 @@ +- Windows host details now include battery status \ No newline at end of file diff --git a/docs/Contributing/Understanding-host-vitals.md b/docs/Contributing/Understanding-host-vitals.md index 3cdabf2fde06..3aa2078574f1 100644 --- a/docs/Contributing/Understanding-host-vitals.md +++ b/docs/Contributing/Understanding-host-vitals.md @@ -3,7 +3,7 @@ Following is a summary of the detail queries hardcoded in Fleet used to populate the device details: -## battery +## battery_macos - Platforms: darwin @@ -12,6 +12,20 @@ Following is a summary of the detail queries hardcoded in Fleet used to populate SELECT serial_number, cycle_count, health FROM battery; ``` +## battery_windows + +- Platforms: windows + +- Discovery query: +```sql +SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = 'battery' +``` + +- Query: +```sql +SELECT serial_number, cycle_count, designed_capacity, max_capacity FROM battery +``` + ## chromeos_profile_user_info - Platforms: chrome diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 0697c7de69ce..ff913ac46f30 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -1062,6 +1062,7 @@ func verifyDiscovery(t *testing.T, queries, discovery map[string]string) { hostDetailQueryPrefix + "orbit_info": {}, hostDetailQueryPrefix + "software_vscode_extensions": {}, hostDetailQueryPrefix + "software_macos_firefox": {}, + hostDetailQueryPrefix + "battery_windows": {}, } for name := range queries { require.NotEmpty(t, discovery[name]) diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index f13454b2d110..5c93b5d2d680 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -555,7 +555,7 @@ var extraDetailQueries = map[string]DetailQuery{ DirectIngestFunc: directIngestChromeProfiles, Discovery: discoveryTable("google_chrome_profiles"), }, - "battery": { + "battery_macos": { Query: `SELECT serial_number, cycle_count, health FROM battery;`, Platforms: []string{"darwin"}, DirectIngestFunc: directIngestBattery, @@ -563,6 +563,12 @@ var extraDetailQueries = map[string]DetailQuery{ // osquery table on darwin (https://osquery.io/schema/5.3.0#battery), it is // always present. }, + "battery_windows": { + Query: `SELECT serial_number, cycle_count, designed_capacity, max_capacity FROM battery`, + Platforms: []string{"windows"}, + DirectIngestFunc: directIngestBattery, + Discovery: discoveryTable("battery"), // added to Windows in v5.12.1 (https://github.com/osquery/osquery/releases/tag/5.12.1) + }, "os_windows": { // This query is used to populate the `operating_systems` and `host_operating_system` // tables. Separately, the `hosts` table is populated via the `os_version` and @@ -1297,23 +1303,70 @@ func directIngestChromeProfiles(ctx context.Context, logger log.Logger, host *fl func directIngestBattery(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error { mapping := make([]*fleet.HostBattery, 0, len(rows)) for _, row := range rows { - cycleCount, err := strconv.ParseInt(EmptyToZero(row["cycle_count"]), 10, 64) + cycleCount, err := strconv.Atoi(EmptyToZero(row["cycle_count"])) if err != nil { return err } - mapping = append(mapping, &fleet.HostBattery{ - HostID: host.ID, - SerialNumber: row["serial_number"], - CycleCount: int(cycleCount), - // database type is VARCHAR(40) and since there isn't a - // canonical list of strings we can get for health, we - // truncate the value just in case. - Health: fmt.Sprintf("%.40s", row["health"]), - }) + + switch host.Platform { + case "darwin": + mapping = append(mapping, &fleet.HostBattery{ + HostID: host.ID, + SerialNumber: row["serial_number"], + CycleCount: cycleCount, + // database type is VARCHAR(40) and since there isn't a + // canonical list of strings we can get for health, we + // truncate the value just in case. + Health: fmt.Sprintf("%.40s", row["health"]), + }) + case "windows": + health, err := generateWindowsBatteryHealth(row["designed_capacity"], row["max_capacity"]) + if err != nil { + level.Error(logger).Log("op", "directIngestBattery", "hostID", host.ID, "err", err) + } + + mapping = append(mapping, &fleet.HostBattery{ + HostID: host.ID, + SerialNumber: row["serial_number"], + CycleCount: cycleCount, + Health: health, + }) + } } return ds.ReplaceHostBatteries(ctx, host.ID, mapping) } +const ( + batteryStatusUnknown = "Unknown" + batteryStatusDegraded = "Check Battery" + batteryStatusGood = "Good" + batteryDegradedThreshold = 80 +) + +func generateWindowsBatteryHealth(designedCapacity, maxCapacity string) (string, error) { + if designedCapacity == "" || maxCapacity == "" { + return batteryStatusUnknown, fmt.Errorf("missing battery capacity values, designed: %s, max: %s", designedCapacity, maxCapacity) + } + + designed, err := strconv.ParseInt(designedCapacity, 10, 64) + if err != nil { + return batteryStatusUnknown, err + } + + max, err := strconv.ParseInt(maxCapacity, 10, 64) + if err != nil { + return batteryStatusUnknown, err + } + + health := float64(max) / float64(designed) * 100 + + if health < batteryDegradedThreshold { + return batteryStatusDegraded, nil + } + + return batteryStatusGood, nil +} + func directIngestWindowsUpdateHistory( ctx context.Context, logger log.Logger, diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index 8a2931447083..3593ff20f1a5 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -279,7 +279,8 @@ func TestGetDetailQueries(t *testing.T) { "mdm_windows", "munki_info", "google_chrome_profiles", - "battery", + "battery_macos", + "battery_windows", "os_windows", "os_unix_like", "os_chrome", @@ -296,7 +297,7 @@ func TestGetDetailQueries(t *testing.T) { sortedKeysCompare(t, queriesNoConfig, baseQueries) queriesWithoutWinOSVuln := GetDetailQueries(context.Background(), config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}}, nil, nil) - require.Len(t, queriesWithoutWinOSVuln, 25) + require.Len(t, queriesWithoutWinOSVuln, 26) queriesWithUsers := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true}) qs := append(baseQueries, "users", "users_chrome", "scheduled_query_stats") @@ -984,7 +985,8 @@ func TestDirectIngestBattery(t *testing.T) { } host := fleet.Host{ - ID: 1, + ID: 1, + Platform: "darwin", } err := directIngestBattery(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{ @@ -994,6 +996,37 @@ func TestDirectIngestBattery(t *testing.T) { require.NoError(t, err) require.True(t, ds.ReplaceHostBatteriesFuncInvoked) + + ds.ReplaceHostBatteriesFunc = func(ctx context.Context, id uint, mappings []*fleet.HostBattery) error { + require.Equal(t, mappings, []*fleet.HostBattery{ + {HostID: uint(2), SerialNumber: "a", CycleCount: 2, Health: batteryStatusGood}, + {HostID: uint(2), SerialNumber: "b", CycleCount: 3, Health: batteryStatusDegraded}, + {HostID: uint(2), SerialNumber: "c", CycleCount: 4, Health: batteryStatusUnknown}, + {HostID: uint(2), SerialNumber: "d", CycleCount: 5, Health: batteryStatusUnknown}, + {HostID: uint(2), SerialNumber: "e", CycleCount: 6, Health: batteryStatusUnknown}, + {HostID: uint(2), SerialNumber: "f", CycleCount: 7, Health: batteryStatusUnknown}, + }) + return nil + } + + // reset the ds flag + ds.ReplaceHostBatteriesFuncInvoked = false + + host = fleet.Host{ + ID: 2, + Platform: "windows", + } + + err = directIngestBattery(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{ + {"serial_number": "a", "cycle_count": "2", "designed_capacity": "3000", "max_capacity": "2400"}, // max_capacity >= 80% + {"serial_number": "b", "cycle_count": "3", "designed_capacity": "3000", "max_capacity": "2399"}, // max_capacity < 50% + {"serial_number": "c", "cycle_count": "4", "designed_capacity": "3000", "max_capacity": ""}, // missing max_capacity + {"serial_number": "d", "cycle_count": "5", "designed_capacity": "", "max_capacity": ""}, // missing designed_capacity and max_capacity + {"serial_number": "e", "cycle_count": "6", "designed_capacity": "", "max_capacity": "2000"}, // missing designed_capacity + {"serial_number": "f", "cycle_count": "7", "designed_capacity": "foo", "max_capacity": "bar"}, // invalid designed_capacity and max_capacity + }) + require.NoError(t, err) + require.True(t, ds.ReplaceHostBatteriesFuncInvoked) } func TestDirectIngestOSWindows(t *testing.T) { From 3f249fd11b3e9020d0b23af9b0cd85013168a5da Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:25:59 -0400 Subject: [PATCH 04/19] Nonpunctual passcode ddm (#22531) # Checklist for submitter Adds passcode DDM declaration to workstations. --- .../macos-passcode-settings.json | 12 ++++++++++++ it-and-security/teams/workstations.yml | 1 + 2 files changed, 13 insertions(+) create mode 100644 it-and-security/lib/configuration-profiles/macos-passcode-settings.json diff --git a/it-and-security/lib/configuration-profiles/macos-passcode-settings.json b/it-and-security/lib/configuration-profiles/macos-passcode-settings.json new file mode 100644 index 000000000000..e68b0a46c64f --- /dev/null +++ b/it-and-security/lib/configuration-profiles/macos-passcode-settings.json @@ -0,0 +1,12 @@ +{ + "Type": "com.apple.configuration.passcode.settings", + "Identifier": "com.fleetdm.config.passcode.settings", + "Payload": { + "RequireComplexPasscode": true, + "MinimumLength": 10, + "MinimumComplexCharacters": 1, + "MaximumFailedAttempts": 11, + "MaximumGracePeriodInMinutes": 1, + "MaximumInactivityInMinutes": 15 + } +} diff --git a/it-and-security/teams/workstations.yml b/it-and-security/teams/workstations.yml index 0ad266f905d9..f586b2b5d724 100644 --- a/it-and-security/teams/workstations.yml +++ b/it-and-security/teams/workstations.yml @@ -38,6 +38,7 @@ controls: - path: ../lib/configuration-profiles/macos-password.mobileconfig - path: ../lib/configuration-profiles/macos-prevent-autologon.mobileconfig - path: ../lib/configuration-profiles/macos-secure-terminal-keyboard.mobileconfig + - path: ../lib/configuration-profiles/macos-passcode-settings.json macos_setup: bootstrap_package: "" enable_end_user_authentication: true From cfd4159487075811c7134daea3fe178203836c5d Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:50:30 -0400 Subject: [PATCH 05/19] Update macos-passcode-settings.json (#22533) Cuz JD is dum. --- .../lib/configuration-profiles/macos-passcode-settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/it-and-security/lib/configuration-profiles/macos-passcode-settings.json b/it-and-security/lib/configuration-profiles/macos-passcode-settings.json index e68b0a46c64f..d433a91826fe 100644 --- a/it-and-security/lib/configuration-profiles/macos-passcode-settings.json +++ b/it-and-security/lib/configuration-profiles/macos-passcode-settings.json @@ -2,7 +2,7 @@ "Type": "com.apple.configuration.passcode.settings", "Identifier": "com.fleetdm.config.passcode.settings", "Payload": { - "RequireComplexPasscode": true, + "RequireAlphanumericPasscode": true, "MinimumLength": 10, "MinimumComplexCharacters": 1, "MaximumFailedAttempts": 11, From 80f0fd8889a7b8ed66b92e1762e6e5fda085d427 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Tue, 1 Oct 2024 10:26:16 -0400 Subject: [PATCH 06/19] fix: reset token team assignments to defaults (#22326) > Related issue: #22198 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- changes/22198-defaults | 2 + cmd/fleetctl/apply_test.go | 91 +++++++++++++++++++ cmd/fleetctl/gitops_test.go | 80 ++++++++++++++++ .../VppTable/TeamsCell/TeamsCell.tsx | 4 +- server/service/appconfig.go | 42 ++++++++- server/service/appconfig_test.go | 78 ++++++++++++++++ server/service/integration_mdm_test.go | 47 ++++++++-- server/service/mail_test.go | 12 +++ server/service/service_appconfig_test.go | 12 +++ 9 files changed, 357 insertions(+), 11 deletions(-) create mode 100644 changes/22198-defaults diff --git a/changes/22198-defaults b/changes/22198-defaults new file mode 100644 index 000000000000..ec243e9a48e4 --- /dev/null +++ b/changes/22198-defaults @@ -0,0 +1,2 @@ +- Fixes a bug where removing a VPP or ABM token from a GitOps YAML file would leave the team + assignments unchanged. \ No newline at end of file diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index f35b39dc84f1..bd80bb84347c 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -657,6 +657,18 @@ func TestApplyAppConfig(t *testing.T) { return []*fleet.TeamSummary{{Name: "team1", ID: 1}}, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{{OrganizationName: t.Name()}}, nil + } + name := writeTmpYml(t, `--- apiVersion: v1 kind: config @@ -782,6 +794,18 @@ func TestApplyAppConfigDryRunIssue(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + // first, set the default app config's agent options as set after fleetctl setup name := writeTmpYml(t, `--- apiVersion: v1 @@ -914,6 +938,18 @@ func TestApplyAppConfigDeprecatedFields(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + name := writeTmpYml(t, `--- apiVersion: v1 kind: config @@ -1316,6 +1352,14 @@ func TestApplyAsGitOps(t *testing.T) { return []*fleet.ABMToken{{ID: 1}}, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + // Apply global config. name := writeTmpYml(t, `--- apiVersion: v1 @@ -1873,6 +1917,18 @@ func TestCanApplyIntervalsInNanoseconds(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + name := writeTmpYml(t, `--- apiVersion: v1 kind: config @@ -1908,6 +1964,18 @@ func TestCanApplyIntervalsUsingDurations(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + name := writeTmpYml(t, `--- apiVersion: v1 kind: config @@ -2091,6 +2159,18 @@ func TestApplyMacosSetup(t *testing.T) { return []*fleet.ABMToken{{ID: 1}}, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + return ds } @@ -2764,6 +2844,17 @@ func TestApplySpecs(t *testing.T) { ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error { return nil } + + // VPP/AMB + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } } cases := []struct { diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 1a054482f618..5a688b2fd91b 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -83,6 +83,18 @@ func TestGitOpsBasicGlobalFree(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) @@ -238,6 +250,18 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) { return nil, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) @@ -591,6 +615,17 @@ func TestGitOpsFullGlobal(t *testing.T) { return nil } + // Needed for checking tokens + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + const ( fleetServerURL = "https://fleet.example.com" orgName = "GitOps Test" @@ -1079,6 +1114,18 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) { return nil, 0, nil, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + globalFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) @@ -1345,6 +1392,18 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) { return nil, 0, nil, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + globalFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) @@ -1604,6 +1663,18 @@ func TestGitOpsFullGlobalAndTeam(t *testing.T) { return team, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + apnsCert, apnsKey, err := mysql.GenerateTestCertBytes() require.NoError(t, err) crt, key, err := apple_mdm.NewSCEPCACertKey() @@ -2234,6 +2305,15 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) { return nil, 0, nil, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } t.Setenv("FLEET_SERVER_URL", fleetServerURL) t.Setenv("ORG_NAME", orgName) diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/TeamsCell/TeamsCell.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/TeamsCell/TeamsCell.tsx index 57a21e7e48b1..570d7af8ed0f 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/TeamsCell/TeamsCell.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/TeamsCell/TeamsCell.tsx @@ -17,7 +17,7 @@ const generateCell = (teams: ITokenTeam[] | null) => { } if (teams.length === 0) { - return ; + return ; } let text = ""; @@ -83,7 +83,7 @@ const TeamsCell = ({ teams, className }: ITeamsCellProps) => { } if (teams.length === 0) { - return ; + return ; } if (teams.length === 1) { diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 17b83dcbbfa1..22998d24d601 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -545,15 +545,55 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } + // Reset teams for ABM tokens that exist in Fleet but aren't present in the config being passed + tokensInCfg := make(map[string]struct{}) + for _, t := range newAppConfig.MDM.AppleBusinessManager.Value { + tokensInCfg[t.OrganizationName] = struct{}{} + } + + toks, err := svc.ds.ListABMTokens(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "listing ABM tokens") + } + for _, tok := range toks { + if _, ok := tokensInCfg[tok.OrganizationName]; !ok { + tok.MacOSDefaultTeamID = nil + tok.IOSDefaultTeamID = nil + tok.IPadOSDefaultTeamID = nil + if err := svc.ds.SaveABMToken(ctx, tok); err != nil { + return nil, ctxerr.Wrap(ctx, err, "saving ABM token assignments") + } + } + } + if (appConfig.MDM.AppleBusinessManager.Set && appConfig.MDM.AppleBusinessManager.Valid) || appConfig.MDM.DeprecatedAppleBMDefaultTeam != "" { for _, tok := range abmAssignments { - fmt.Println(tok.EncryptedToken) if err := svc.ds.SaveABMToken(ctx, tok); err != nil { return nil, ctxerr.Wrap(ctx, err, "saving ABM token assignments") } } } + // Reset teams for VPP tokens that exist in Fleet but aren't present in the config being passed + clear(tokensInCfg) + + for _, t := range newAppConfig.MDM.VolumePurchasingProgram.Value { + tokensInCfg[t.Location] = struct{}{} + } + + vppToks, err := svc.ds.ListVPPTokens(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "listing VPP tokens") + } + for _, tok := range vppToks { + if _, ok := tokensInCfg[tok.Location]; !ok { + tok.Teams = nil + if _, err := svc.ds.UpdateVPPTokenTeams(ctx, tok.ID, nil); err != nil { + return nil, ctxerr.Wrap(ctx, err, "saving VPP token teams") + } + } + } + if appConfig.MDM.VolumePurchasingProgram.Set && appConfig.MDM.VolumePurchasingProgram.Valid { for tokenID, tokenTeams := range vppAssignments { if _, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, tokenTeams); err != nil { diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 0fb0d318d26f..2c6e6bc10849 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -51,6 +51,18 @@ func TestAppConfigAuth(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + testCases := []struct { name string user *fleet.User @@ -647,6 +659,18 @@ func TestModifyAppConfigSMTPConfigured(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + // Disable SMTP. newAppConfig := fleet.AppConfig{ SMTPSettings: &fleet.SMTPSettings{ @@ -751,6 +775,18 @@ func TestTransparencyURL(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + ac, err := svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, tt.initialURL, ac.FleetDesktop.TransparencyURL) @@ -800,6 +836,18 @@ func TestTransparencyURLDowngradeLicense(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + ac, err := svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, "https://example.com/transparency", ac.FleetDesktop.TransparencyURL) @@ -1090,6 +1138,15 @@ func TestMDMAppleConfig(t *testing.T) { depStorage.StoreAssignerProfileFunc = func(ctx context.Context, name string, profileUUID string) error { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{{OrganizationName: t.Name()}}, nil + } ac, err := svc.AppConfigObfuscated(ctx) require.NoError(t, err) @@ -1168,6 +1225,15 @@ func TestModifyAppConfigSMTPSSOAgentOptions(t *testing.T) { ) error { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } // Not sending smtp_settings, sso_settings or agent_settings will do nothing. b := []byte(`{}`) @@ -1297,6 +1363,18 @@ func TestModifyEnableAnalytics(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + ac, err := svc.AppConfigObfuscated(ctx) require.NoError(t, err) require.Equal(t, tt.initialEnabled, ac.ServerSettings.EnableAnalytics) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index ce134a2bd5fe..6eee1327bb2b 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -879,6 +879,26 @@ func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() { require.Equal(t, tm.Name, tok.MacOSTeam.Name) require.Equal(t, tm.Name, tok.IOSTeam.Name) require.Equal(t, tm.Name, tok.IPadOSTeam.Name) + + // Reset the teams via app config + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "apple_business_manager": [] + } + }`), http.StatusOK, &acResp) + + tokensResp = listABMTokensResponse{} + s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp) + tok = s.getABMTokenByName(tmOrgName, tokensResp.Tokens) + require.NotNil(t, tok) + require.False(t, tok.TermsExpired) + require.Equal(t, "abc", tok.AppleID) + require.Equal(t, tmOrgName, tok.OrganizationName) + require.Equal(t, s.server.URL+"/mdm/apple/mdm", tok.MDMServerURL) + require.Equal(t, fleet.TeamNameNoTeam, tok.MacOSTeam.Name) + require.Equal(t, fleet.TeamNameNoTeam, tok.IOSTeam.Name) + require.Equal(t, fleet.TeamNameNoTeam, tok.IPadOSTeam.Name) } func (s *integrationMDMTestSuite) getABMTokenByName(orgName string, tokens []*fleet.ABMToken) *fleet.ABMToken { @@ -10532,6 +10552,25 @@ func (s *integrationMDMTestSuite) TestVPPApps() { var resPatchVPP patchVPPTokensTeamsResponse s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP) + // Reset the token's teams by omitting the token from app config + acResp := appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "volume_purchasing_program": null } + }`), http.StatusOK, &acResp) + + resp = getVPPTokensResponse{} + s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp) + require.NoError(t, resp.Err) + require.Len(t, resp.Tokens, 1) + require.Equal(t, orgName, resp.Tokens[0].OrgName) + require.Equal(t, location, resp.Tokens[0].Location) + require.Equal(t, expTime, resp.Tokens[0].RenewDate) + require.Empty(t, resp.Tokens[0].Teams) + + // Add the team back + resPatchVPP = patchVPPTokensTeamsResponse{} + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP) + // Get list of VPP apps from "Apple" // We're passing team 1 here, but we haven't added any app store apps to that team, so we get // back all available apps in our VPP location. @@ -10801,14 +10840,6 @@ func (s *integrationMDMTestSuite) TestVPPApps() { s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", vppRes.Token.ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP) - // mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { - // _, err := q.ExecContext(context.Background(), "UPDATE vpp_tokens SET renew_at = ? WHERE organization_name = ?", time.Now().Add(-1*time.Hour), "badtoken") - // return err - // }) - - // r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", mdmHost.ID, errTitleID), &installSoftwareRequest{}, http.StatusUnprocessableEntity) - // require.Contains(t, extractServerErrorText(r.Body), "VPP token expired") - // Disable the token s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", vppRes.Token.ID), patchVPPTokensTeamsRequest{}, http.StatusOK, &resPatchVPP) diff --git a/server/service/mail_test.go b/server/service/mail_test.go index be041e7e6ec6..c83168301cac 100644 --- a/server/service/mail_test.go +++ b/server/service/mail_test.go @@ -86,6 +86,18 @@ func TestMailService(t *testing.T) { return invite, nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + ctx = test.UserContext(ctx, test.UserAdmin) // (1) Modifying the app config `sender_address` field to trigger a test e-mail send. diff --git a/server/service/service_appconfig_test.go b/server/service/service_appconfig_test.go index b6318c584b39..d83479e5f156 100644 --- a/server/service/service_appconfig_test.go +++ b/server/service/service_appconfig_test.go @@ -373,6 +373,18 @@ func TestModifyAppConfigPatches(t *testing.T) { return nil } + ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error { + return nil + } + + ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + return []*fleet.VPPTokenDB{}, nil + } + + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{}, nil + } + configJSON := []byte(`{"org_info": { "org_name": "Acme", "org_logo_url": "somelogo.jpg" }}`) ctx = test.UserContext(ctx, test.UserAdmin) From f7fc22d766bafdfe8923b767d1fcb43e78aa0b3d Mon Sep 17 00:00:00 2001 From: George Karr Date: Tue, 1 Oct 2024 10:37:19 -0500 Subject: [PATCH 07/19] Adding changes for Fleet v4.57.1 (#22537) --- CHANGELOG.md | 9 +++++++++ charts/fleet/Chart.yaml | 2 +- charts/fleet/values.yaml | 2 +- infrastructure/dogfood/terraform/aws/variables.tf | 2 +- infrastructure/dogfood/terraform/gcp/variables.tf | 2 +- terraform/addons/vuln-processing/variables.tf | 4 ++-- terraform/byo-vpc/byo-db/byo-ecs/variables.tf | 4 ++-- terraform/byo-vpc/byo-db/variables.tf | 4 ++-- terraform/byo-vpc/example/main.tf | 2 +- terraform/byo-vpc/variables.tf | 4 ++-- terraform/example/main.tf | 4 ++-- terraform/variables.tf | 4 ++-- tools/fleetctl-npm/package.json | 2 +- 13 files changed, 27 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f4dd2cee3e..056966f521be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Fleet 4.57.1 (Oct 01, 2024) + +### Bug fixes + +* Improved performance of SQL queries used to determine MDM profile status for Apple hosts. +* Ensured request timeouts for software installer edits were just as high as for initial software installer uploads. +* Fixed an issue with the migration that added support for multiple VPP tokens, which would happen if a token was removed prior to upgrading Fleet. +* Fixed a "no rows" error when adding a software installer that matched an existing title's name and source but not its bundle ID. + ## Fleet 4.57.0 (Sep 23, 2024) **Endpoint Operations** diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index c23438bf22aa..f9abb8e29901 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -8,7 +8,7 @@ version: v6.2.0 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.57.0 +appVersion: v4.57.1 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 03539df9da98..15eda9b9e8ce 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -3,7 +3,7 @@ hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy imageRepository: fleetdm/fleet -imageTag: v4.57.0 # Version of Fleet to deploy +imageTag: v4.57.1 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf index 2020de2f8306..d61f8819ec4b 100644 --- a/infrastructure/dogfood/terraform/aws/variables.tf +++ b/infrastructure/dogfood/terraform/aws/variables.tf @@ -56,7 +56,7 @@ variable "database_name" { variable "fleet_image" { description = "the name of the container image to run" - default = "fleetdm/fleet:v4.57.0" + default = "fleetdm/fleet:v4.57.1" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index 906a58c153f8..a8877ab5e00f 100644 --- a/infrastructure/dogfood/terraform/gcp/variables.tf +++ b/infrastructure/dogfood/terraform/gcp/variables.tf @@ -68,7 +68,7 @@ variable "redis_mem" { } variable "image" { - default = "fleetdm/fleet:v4.57.0" + default = "fleetdm/fleet:v4.57.1" } variable "software_installers_bucket_name" { diff --git a/terraform/addons/vuln-processing/variables.tf b/terraform/addons/vuln-processing/variables.tf index 8d296903fdc4..11c5bb870715 100644 --- a/terraform/addons/vuln-processing/variables.tf +++ b/terraform/addons/vuln-processing/variables.tf @@ -24,7 +24,7 @@ variable "fleet_config" { vuln_processing_cpu = optional(number, 2048) vuln_data_stream_mem = optional(number, 1024) vuln_data_stream_cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.57.0") + image = optional(string, "fleetdm/fleet:v4.57.1") family = optional(string, "fleet-vuln-processing") sidecars = optional(list(any), []) extra_environment_variables = optional(map(string), {}) @@ -82,7 +82,7 @@ variable "fleet_config" { vuln_processing_cpu = 2048 vuln_data_stream_mem = 1024 vuln_data_stream_cpu = 512 - image = "fleetdm/fleet:v4.57.0" + image = "fleetdm/fleet:v4.57.1" family = "fleet-vuln-processing" sidecars = [] extra_environment_variables = {} diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf index 27565cb90fa8..51dc799bc5b7 100644 --- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf +++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf @@ -16,7 +16,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.57.0") + image = optional(string, "fleetdm/fleet:v4.57.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -119,7 +119,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.57.0" + image = "fleetdm/fleet:v4.57.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index 041ff9d0f861..b19d74931ec5 100644 --- a/terraform/byo-vpc/byo-db/variables.tf +++ b/terraform/byo-vpc/byo-db/variables.tf @@ -77,7 +77,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.57.0") + image = optional(string, "fleetdm/fleet:v4.57.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -205,7 +205,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.57.0" + image = "fleetdm/fleet:v4.57.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf index 3176d07def1f..7f75b975ad42 100644 --- a/terraform/byo-vpc/example/main.tf +++ b/terraform/byo-vpc/example/main.tf @@ -17,7 +17,7 @@ provider "aws" { } locals { - fleet_image = "fleetdm/fleet:v4.57.0" + fleet_image = "fleetdm/fleet:v4.57.1" domain_name = "example.com" } diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index ce2a81f88c41..2c8fdb1d1742 100644 --- a/terraform/byo-vpc/variables.tf +++ b/terraform/byo-vpc/variables.tf @@ -170,7 +170,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.57.0") + image = optional(string, "fleetdm/fleet:v4.57.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -298,7 +298,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.57.0" + image = "fleetdm/fleet:v4.57.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/example/main.tf b/terraform/example/main.tf index 2b2112517925..bf8f569a36b8 100644 --- a/terraform/example/main.tf +++ b/terraform/example/main.tf @@ -63,8 +63,8 @@ module "fleet" { fleet_config = { # To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror - # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.57.0" - image = "fleetdm/fleet:v4.57.0" # override default to deploy the image you desire + # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.57.1" + image = "fleetdm/fleet:v4.57.1" # override default to deploy the image you desire # See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling # memory and cpu. autoscaling = { diff --git a/terraform/variables.tf b/terraform/variables.tf index 7dc798cf63d8..de37b0020f25 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -218,7 +218,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.57.0") + image = optional(string, "fleetdm/fleet:v4.57.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -346,7 +346,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.57.0" + image = "fleetdm/fleet:v4.57.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index 96a4dcd08170..13ac3172e426 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.57.0", + "version": "v4.57.1", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" From f8f24e0a80322cf0c6d541761a2583ebeb8b0f3f Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Tue, 1 Oct 2024 13:02:13 -0300 Subject: [PATCH 08/19] Add support to upload RPM packages (#22502) #22473 - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [x] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. --------- Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Co-authored-by: Ian Littman --- changes/20537-add-rpm-support | 1 + cmd/fleetctl/gitops_test.go | 4 +- ee/server/service/software_installers.go | 4 +- frontend/interfaces/package_type.ts | 2 +- frontend/interfaces/software.ts | 2 +- .../PackageAdvancedOptions.tsx | 1 + .../components/PackageForm/PackageForm.tsx | 4 +- .../InstallSoftwareModal.tsx | 1 + frontend/utilities/file/fileUtils.tests.ts | 23 +++-- frontend/utilities/file/fileUtils.ts | 5 +- .../utilities/software_install_scripts.ts | 4 + .../utilities/software_uninstall_scripts.ts | 4 + go.mod | 2 + go.sum | 4 + orbit/pkg/installer/installer.go | 10 +- pkg/file/file.go | 6 ++ pkg/file/management.go | 15 +++ pkg/file/management_test.go | 11 +- pkg/file/rpm.go | 33 ++++++ pkg/file/rpm_test.go | 94 ++++++++++++++++++ pkg/file/scripts/README.md | 8 +- pkg/file/scripts/install_rpm.sh | 3 + pkg/file/scripts/remove_rpm.sh | 6 ++ pkg/file/scripts/uninstall_rpm.sh | 4 + .../testdata/scripts/install_rpm.sh.golden | 3 + .../testdata/scripts/remove_rpm.sh.golden | 6 ++ .../testdata/scripts/uninstall_rpm.sh.golden | 4 + server/datastore/mysql/software_installers.go | 1 + .../mysql/software_installers_test.go | 16 ++- server/fleet/software_installer.go | 4 +- server/service/integration_enterprise_test.go | 71 +++++++++++++ .../testdata/software-installers/README.md | 3 +- .../testdata/software-installers/ruby.rpm | Bin 0 -> 40422 bytes 33 files changed, 328 insertions(+), 31 deletions(-) create mode 100644 changes/20537-add-rpm-support create mode 100644 pkg/file/rpm.go create mode 100644 pkg/file/rpm_test.go create mode 100644 pkg/file/scripts/install_rpm.sh create mode 100644 pkg/file/scripts/remove_rpm.sh create mode 100644 pkg/file/scripts/uninstall_rpm.sh create mode 100644 pkg/file/testdata/scripts/install_rpm.sh.golden create mode 100644 pkg/file/testdata/scripts/remove_rpm.sh.golden create mode 100644 pkg/file/testdata/scripts/uninstall_rpm.sh.golden create mode 100644 server/service/testdata/software-installers/ruby.rpm diff --git a/changes/20537-add-rpm-support b/changes/20537-add-rpm-support new file mode 100644 index 000000000000..2238298b2413 --- /dev/null +++ b/changes/20537-add-rpm-support @@ -0,0 +1 @@ +* Added support for uploading RPM packages. diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 5a688b2fd91b..295172612d74 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -1727,7 +1727,7 @@ func TestGitOpsTeamSofwareInstallers(t *testing.T) { wantErr string }{ {"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."}, - {"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."}, + {"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb or .rpm."}, {"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MiB"}, {"testdata/gitops/team_software_installer_valid.yml", ""}, {"testdata/gitops/team_software_installer_valid_apply.yml", ""}, @@ -1782,7 +1782,7 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) { wantErr string }{ {"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."}, - {"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."}, + {"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb or .rpm."}, {"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MiB"}, {"testdata/gitops/no_team_software_installer_valid.yml", ""}, {"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."}, diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index ac4461a592b2..a2dea22138bb 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -1054,7 +1054,7 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f if err != nil { if errors.Is(err, file.ErrUnsupportedType) { return "", &fleet.BadRequestError{ - Message: "Couldn't edit software. File type not supported. The file should be .pkg, .msi, .exe or .deb.", + Message: "Couldn't edit software. File type not supported. The file should be .pkg, .msi, .exe, .deb or .rpm.", InternalErr: ctxerr.Wrap(ctx, err, "extracting metadata from installer"), } } @@ -1517,7 +1517,7 @@ func packageExtensionToPlatform(ext string) string { requiredPlatform = "windows" case ".pkg": requiredPlatform = "darwin" - case ".deb": + case ".deb", ".rpm": requiredPlatform = "linux" default: return "" diff --git a/frontend/interfaces/package_type.ts b/frontend/interfaces/package_type.ts index 8afb43cef382..b8b83b93fe9d 100644 --- a/frontend/interfaces/package_type.ts +++ b/frontend/interfaces/package_type.ts @@ -1,4 +1,4 @@ -const unixPackageTypes = ["pkg", "deb"] as const; +const unixPackageTypes = ["pkg", "deb", "rpm"] as const; const windowsPackageTypes = ["msi", "exe"] as const; export const packageTypes = [ ...unixPackageTypes, diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index a3e633a8bb6b..0992aad99a05 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -270,7 +270,7 @@ export interface ISoftwareInstallResults { // ISoftwareInstallerType defines the supported installer types for // software uploaded by the IT admin. -export type ISoftwareInstallerType = "pkg" | "msi" | "deb" | "exe"; +export type ISoftwareInstallerType = "pkg" | "msi" | "deb" | "rpm" | "exe"; export interface ISoftwareLastInstall { install_uuid: string; diff --git a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx index ad06e516ec0a..4ab30c32c859 100644 --- a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx +++ b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx @@ -23,6 +23,7 @@ const getSupportedScriptTypeText = (pkgType: PackageType) => { const PKG_TYPE_TO_ID_TEXT = { pkg: "package IDs", deb: "package name", + rpm: "package name", msi: "product code", exe: "software name", } as const; diff --git a/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx b/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx index 343423d2d829..4e9d7aa74de0 100644 --- a/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx +++ b/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx @@ -58,7 +58,7 @@ interface IPackageFormProps { defaultSelfService?: boolean; } -const ACCEPTED_EXTENSIONS = ".pkg,.msi,.exe,.deb"; +const ACCEPTED_EXTENSIONS = ".pkg,.msi,.exe,.deb,.rpm"; const PackageForm = ({ isUploading, @@ -173,7 +173,7 @@ const PackageForm = ({ canEdit={isEditingSoftware} graphicName={"file-pkg"} accept={ACCEPTED_EXTENSIONS} - message=".pkg, .msi, .exe, or .deb" + message=".pkg, .msi, .exe, .deb, or .rpm" onFileUpload={onFileSelect} buttonMessage="Choose file" buttonType="link" diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx index 84b8c01ffa42..483a786e62eb 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx @@ -28,6 +28,7 @@ const getPlatformDisplayFromPackageSuffix = (packageName: string) => { case "pkg": return "macOS"; case "deb": + case "rpm": return "Linux"; case "exe": return "Windows"; diff --git a/frontend/utilities/file/fileUtils.tests.ts b/frontend/utilities/file/fileUtils.tests.ts index 8d650638920a..f21f84001959 100644 --- a/frontend/utilities/file/fileUtils.tests.ts +++ b/frontend/utilities/file/fileUtils.tests.ts @@ -2,15 +2,22 @@ import { getPlatformDisplayName } from "./fileUtils"; describe("fileUtils", () => { describe("getPlatformDisplayName", () => { - it("should return the correct platform display name depending on the file extension", () => { - const file = new File([""], "test.pkg"); - expect(getPlatformDisplayName(file)).toEqual("macOS"); + const testCases = [ + { extension: "pkg", platform: "macOS" }, + { extension: "json", platform: "macOS" }, + { extension: "mobileconfig", platform: "macOS" }, + { extension: "exe", platform: "Windows" }, + { extension: "msi", platform: "Windows" }, + { extension: "xml", platform: "Windows" }, + { extension: "deb", platform: "Linux" }, + { extension: "rpm", platform: "Linux" }, + ]; - const file2 = new File([""], "test.exe"); - expect(getPlatformDisplayName(file2)).toEqual("Windows"); - - const file3 = new File([""], "test.deb"); - expect(getPlatformDisplayName(file3)).toEqual("linux"); + testCases.forEach(({ extension, platform }) => { + it(`should return ${platform} for .${extension} files`, () => { + const file = new File([""], `test.${extension}`); + expect(getPlatformDisplayName(file)).toEqual(platform); + }); }); }); }); diff --git a/frontend/utilities/file/fileUtils.ts b/frontend/utilities/file/fileUtils.ts index ca7b74fdd12b..6853a68a69c3 100644 --- a/frontend/utilities/file/fileUtils.ts +++ b/frontend/utilities/file/fileUtils.ts @@ -1,4 +1,4 @@ -type IPlatformDisplayName = "macOS" | "Windows" | "linux"; +type IPlatformDisplayName = "macOS" | "Windows" | "Linux"; const getFileExtension = (file: File) => { const nameParts = file.name.split("."); @@ -15,7 +15,8 @@ export const FILE_EXTENSIONS_TO_PLATFORM_DISPLAY_NAME: Record< exe: "Windows", msi: "Windows", xml: "Windows", - deb: "linux", + deb: "Linux", + rpm: "Linux", }; /** diff --git a/frontend/utilities/software_install_scripts.ts b/frontend/utilities/software_install_scripts.ts index 5c21e8a1f465..be0a9f30d3ce 100644 --- a/frontend/utilities/software_install_scripts.ts +++ b/frontend/utilities/software_install_scripts.ts @@ -6,6 +6,8 @@ import installMsi from "../../pkg/file/scripts/install_msi.ps1"; import installExe from "../../pkg/file/scripts/install_exe.ps1"; // @ts-ignore import installDeb from "../../pkg/file/scripts/install_deb.sh"; +// @ts-ignore +import installRPM from "../../pkg/file/scripts/install_rpm.sh"; /* * getInstallScript returns a string with a script to install the @@ -20,6 +22,8 @@ const getDefaultInstallScript = (fileName: string): string => { return installMsi; case "deb": return installDeb; + case "rpm": + return installRPM; case "exe": return installExe; default: diff --git a/frontend/utilities/software_uninstall_scripts.ts b/frontend/utilities/software_uninstall_scripts.ts index 041d2519c4c1..172c53c14f68 100644 --- a/frontend/utilities/software_uninstall_scripts.ts +++ b/frontend/utilities/software_uninstall_scripts.ts @@ -6,6 +6,8 @@ import uninstallMsi from "../../pkg/file/scripts/uninstall_msi.ps1"; import uninstallExe from "../../pkg/file/scripts/uninstall_exe.ps1"; // @ts-ignore import uninstallDeb from "../../pkg/file/scripts/uninstall_deb.sh"; +// @ts-ignore +import uninstallRPM from "../../pkg/file/scripts/uninstall_rpm.sh"; /* * getUninstallScript returns a string with a script to uninstall the @@ -20,6 +22,8 @@ const getDefaultUninstallScript = (fileName: string): string => { return uninstallMsi; case "deb": return uninstallDeb; + case "rpm": + return uninstallRPM; case "exe": return uninstallExe; default: diff --git a/go.mod b/go.mod index 056fe1b18413..83670b64fdd1 100644 --- a/go.mod +++ b/go.mod @@ -199,6 +199,8 @@ require ( github.com/caarlos0/env/v6 v6.7.0 // indirect github.com/caarlos0/go-shellwords v1.0.12 // indirect github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect + github.com/cavaliergopher/cpio v1.0.1 // indirect + github.com/cavaliergopher/rpm v1.2.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudflare/circl v1.3.8 // indirect diff --git a/go.sum b/go.sum index beec8ba3e049..8bf177e83697 100644 --- a/go.sum +++ b/go.sum @@ -320,6 +320,10 @@ github.com/caarlos0/testfs v0.4.3 h1:q1zEM5hgsssqWanAfevJYYa0So60DdK6wlJeTc/yfUE github.com/caarlos0/testfs v0.4.3/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= +github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= +github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= +github.com/cavaliergopher/rpm v1.2.0 h1:s0h+QeVK252QFTolkhGiMeQ1f+tMeIMhGl8B1HUmGUc= +github.com/cavaliergopher/rpm v1.2.0/go.mod h1:R0q3vTqa7RUvPofAZYrnjJ63hh2vngjFfphuXiExVos= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= diff --git a/orbit/pkg/installer/installer.go b/orbit/pkg/installer/installer.go index c8fac3fcca4a..4059416f31ca 100644 --- a/orbit/pkg/installer/installer.go +++ b/orbit/pkg/installer/installer.go @@ -22,8 +22,10 @@ import ( "github.com/rs/zerolog/log" ) -type QueryResponse = osquery_gen.ExtensionResponse -type QueryResponseStatus = osquery_gen.ExtensionStatus +type ( + QueryResponse = osquery_gen.ExtensionResponse + QueryResponseStatus = osquery_gen.ExtensionStatus +) // Client defines the methods required for the API requests to the server. The // fleet.OrbitClient type satisfies this interface. @@ -202,7 +204,7 @@ func (r *Runner) installSoftware(ctx context.Context, installID string) (*fleet. return payload, fmt.Errorf("creating temporary directory: %w", err) } - log.Debug().Msgf("about to download software installer") + log.Debug().Str("install_id", installID).Msgf("about to download software installer") installerPath, err := r.OrbitClient.DownloadSoftwareInstaller(installer.InstallerID, tmpDir) if err != nil { return payload, err @@ -233,7 +235,7 @@ func (r *Runner) installSoftware(ctx context.Context, installID string) (*fleet. } if installer.PostInstallScript != "" { - log.Debug().Msgf("about to run post-install script") + log.Debug().Msgf("about to run post-install script for %s", installerPath) postOutput, postExitCode, postErr := r.runInstallerScript(ctx, installer.PostInstallScript, installerPath, "post-install-script"+scriptExtension) payload.PostInstallScriptOutput = &postOutput payload.PostInstallScriptExitCode = &postExitCode diff --git a/pkg/file/file.go b/pkg/file/file.go index 7abd79169f1d..45774a3e9d8c 100644 --- a/pkg/file/file.go +++ b/pkg/file/file.go @@ -42,6 +42,8 @@ func ExtractInstallerMetadata(r io.Reader) (*InstallerMetadata, error) { switch extension { case "deb": meta, err = ExtractDebMetadata(br) + case "rpm": + meta, err = ExtractRPMMetadata(br) case "exe": meta, err = ExtractPEMetadata(br) case "pkg": @@ -59,12 +61,16 @@ func ExtractInstallerMetadata(r io.Reader) (*InstallerMetadata, error) { return meta, err } +// typeFromBytes deduces the type from the magic bytes. +// See https://en.wikipedia.org/wiki/List_of_file_signatures. func typeFromBytes(br *bufio.Reader) (string, error) { switch { case hasPrefix(br, []byte{0x78, 0x61, 0x72, 0x21}): return "pkg", nil case hasPrefix(br, []byte("!\ndebian")): return "deb", nil + case hasPrefix(br, []byte{0xed, 0xab, 0xee, 0xdb}): + return "rpm", nil case hasPrefix(br, []byte{0xd0, 0xcf}): return "msi", nil case hasPrefix(br, []byte("MZ")): diff --git a/pkg/file/management.go b/pkg/file/management.go index 26d1294952a9..3f83255142d2 100644 --- a/pkg/file/management.go +++ b/pkg/file/management.go @@ -16,6 +16,9 @@ var installExeScript string //go:embed scripts/install_deb.sh var installDebScript string +//go:embed scripts/install_rpm.sh +var installRPMScript string + // GetInstallScript returns a script that can be used to install the given extension func GetInstallScript(extension string) string { switch extension { @@ -23,6 +26,8 @@ func GetInstallScript(extension string) string { return installMsiScript case "deb": return installDebScript + case "rpm": + return installRPMScript case "pkg": return installPkgScript case "exe": @@ -44,6 +49,9 @@ var removeMsiScript string //go:embed scripts/remove_deb.sh var removeDebScript string +//go:embed scripts/remove_rpm.sh +var removeRPMScript string + // GetRemoveScript returns a script that can be used to remove an // installer with the given extension. func GetRemoveScript(extension string) string { @@ -52,6 +60,8 @@ func GetRemoveScript(extension string) string { return removeMsiScript case "deb": return removeDebScript + case "rpm": + return removeRPMScript case "pkg": return removePkgScript case "exe": @@ -73,6 +83,9 @@ var uninstallMsiScript string //go:embed scripts/uninstall_deb.sh var uninstallDebScript string +//go:embed scripts/uninstall_rpm.sh +var uninstallRPMScript string + // GetUninstallScript returns a script that can be used to uninstall a // software item with the given extension. func GetUninstallScript(extension string) string { @@ -81,6 +94,8 @@ func GetUninstallScript(extension string) string { return uninstallMsiScript case "deb": return uninstallDebScript + case "rpm": + return uninstallRPMScript case "pkg": return uninstallPkgScript case "exe": diff --git a/pkg/file/management_test.go b/pkg/file/management_test.go index 8b5d8608c589..6f94fb293458 100644 --- a/pkg/file/management_test.go +++ b/pkg/file/management_test.go @@ -11,9 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -var ( - update = flag.Bool("update", false, "update the golden files of this test") -) +var update = flag.Bool("update", false, "update the golden files of this test") func TestMain(m *testing.M) { flag.Parse() @@ -41,6 +39,11 @@ func TestGetInstallAndRemoveScript(t *testing.T) { "remove": "./scripts/remove_deb.sh", "uninstall": "./scripts/uninstall_deb.sh", }, + "rpm": { + "install": "./scripts/install_rpm.sh", + "remove": "./scripts/remove_rpm.sh", + "uninstall": "./scripts/uninstall_rpm.sh", + }, "exe": { "install": "./scripts/install_exe.ps1", "remove": "./scripts/remove_exe.ps1", @@ -64,7 +67,7 @@ func assertGoldenMatches(t *testing.T, goldenFile string, actual string, update t.Helper() goldenPath := filepath.Join("testdata", goldenFile+".golden") - f, err := os.OpenFile(goldenPath, os.O_RDWR|os.O_CREATE, 0644) + f, err := os.OpenFile(goldenPath, os.O_RDWR|os.O_CREATE, 0o644) require.NoError(t, err) defer f.Close() diff --git a/pkg/file/rpm.go b/pkg/file/rpm.go new file mode 100644 index 000000000000..b82221a46b70 --- /dev/null +++ b/pkg/file/rpm.go @@ -0,0 +1,33 @@ +package file + +import ( + "crypto/sha256" + "fmt" + "io" + + "github.com/cavaliergopher/rpm" +) + +func ExtractRPMMetadata(r io.Reader) (*InstallerMetadata, error) { + h := sha256.New() + r = io.TeeReader(r, h) + + // Read the package headers + pkg, err := rpm.Read(r) + if err != nil { + return nil, fmt.Errorf("read headers: %w", err) + } + // r is now positioned at the RPM payload. + + // Ensure the whole file is read to get the correct hash + if _, err := io.Copy(io.Discard, r); err != nil { + return nil, fmt.Errorf("read all RPM content: %w", err) + } + + return &InstallerMetadata{ + Name: pkg.Name(), + Version: pkg.Version(), + SHASum: h.Sum(nil), + PackageIDs: []string{pkg.Name()}, + }, nil +} diff --git a/pkg/file/rpm_test.go b/pkg/file/rpm_test.go new file mode 100644 index 000000000000..d9d3127250e2 --- /dev/null +++ b/pkg/file/rpm_test.go @@ -0,0 +1,94 @@ +package file + +import ( + "crypto/sha256" + "io" + "os" + "path/filepath" + "testing" + + "github.com/fleetdm/fleet/v4/orbit/pkg/constant" + "github.com/goreleaser/nfpm/v2" + "github.com/goreleaser/nfpm/v2/files" + "github.com/goreleaser/nfpm/v2/rpm" + "github.com/stretchr/testify/require" +) + +func TestExtractRPMMetadata(t *testing.T) { + // + // Build an RPM package on the fly with nfpm. + // + tmpDir := t.TempDir() + err := os.WriteFile(filepath.Join(tmpDir, "foo.sh"), []byte("#!/bin/sh\n\necho \"Foo!\"\n"), constant.DefaultFileMode) + require.NoError(t, err) + contents := files.Contents{ + &files.Content{ + Source: filepath.Join(tmpDir, "**"), + Destination: "/", + }, + } + postInstallPath := filepath.Join(t.TempDir(), "postinstall.sh") + err = os.WriteFile(postInstallPath, []byte("#!/bin/sh\n\necho \"Hello world!\"\n"), constant.DefaultFileMode) + require.NoError(t, err) + info := &nfpm.Info{ + Name: "foobar", + Version: "1.2.3", + Description: "Foo bar", + Arch: "x86_64", + Maintainer: "Fleet Device Management", + Vendor: "Fleet Device Management", + License: "LICENSE", + Homepage: "https://example.com", + Overridables: nfpm.Overridables{ + Contents: contents, + Scripts: nfpm.Scripts{ + PostInstall: postInstallPath, + }, + }, + } + rpmPath := filepath.Join(t.TempDir(), "foobar.rpm") + out, err := os.OpenFile(rpmPath, os.O_CREATE|os.O_RDWR, constant.DefaultFileMode) + require.NoError(t, err) + t.Cleanup(func() { + out.Close() + }) + err = rpm.Default.Package(info, out) + require.NoError(t, err) + err = out.Close() + require.NoError(t, err) + + // + // Test ExtractRPMMetadata with the generated package. + // Using ExtractInstallerMetadata for broader testing (for a file + // with rpm extension it will call ExtractRPMMetadata). + // + f, err := os.Open(rpmPath) + require.NoError(t, err) + t.Cleanup(func() { + f.Close() + }) + m, err := ExtractInstallerMetadata(f) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + require.Empty(t, m.BundleIdentifier) + require.Equal(t, "rpm", m.Extension) + require.Equal(t, "foobar", m.Name) + require.Equal(t, []string{"foobar"}, m.PackageIDs) + require.Equal(t, sha256FilePath(t, rpmPath), m.SHASum) + require.Equal(t, "1.2.3", m.Version) +} + +func sha256FilePath(t *testing.T, path string) []byte { + f, err := os.Open(path) + require.NoError(t, err) + t.Cleanup(func() { + f.Close() + }) + h := sha256.New() + _, err = io.Copy(h, f) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + return h.Sum(nil) +} diff --git a/pkg/file/scripts/README.md b/pkg/file/scripts/README.md index 606801ce682c..c2e0bff612bb 100644 --- a/pkg/file/scripts/README.md +++ b/pkg/file/scripts/README.md @@ -3,10 +3,15 @@ This folder contains scripts to install/remove software for different types of installers. Scripts are stored on their own files for two reasons: - 1. Some of them are read and displayed in the UI. 2. It's helpful to have good syntax highlighting and easy ways to run them. +#### Scripts + +- `install_*.*`: Default installer scripts for each platform. +- `uninstall_*.*`: Default uinstaller scripts for each platform. +- `remove_*.*`: Uninstaller scripts used when the uninstall script is not set (for packages added before the uninstall feature was released) or empty uninstaller scripts. + #### Variables The scripts in this folder accept variables like `$VAR_NAME` that will be replaced/populated by `fleetd` when they run. @@ -14,4 +19,3 @@ The scripts in this folder accept variables like `$VAR_NAME` that will be replac Supported variables are: - `$INSTALLER_PATH` path to the installer file. - diff --git a/pkg/file/scripts/install_rpm.sh b/pkg/file/scripts/install_rpm.sh new file mode 100644 index 000000000000..fb01bc2acda5 --- /dev/null +++ b/pkg/file/scripts/install_rpm.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +dnf install --assumeyes "$INSTALLER_PATH" diff --git a/pkg/file/scripts/remove_rpm.sh b/pkg/file/scripts/remove_rpm.sh new file mode 100644 index 000000000000..efd8e0bb0c89 --- /dev/null +++ b/pkg/file/scripts/remove_rpm.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +package_name=$PACKAGE_ID + +# Fleet uninstalls app using product name that's extracted on upload +dnf remove --assumeyes "$package_name" diff --git a/pkg/file/scripts/uninstall_rpm.sh b/pkg/file/scripts/uninstall_rpm.sh new file mode 100644 index 000000000000..0d664e32f439 --- /dev/null +++ b/pkg/file/scripts/uninstall_rpm.sh @@ -0,0 +1,4 @@ +package_name=$PACKAGE_ID + +# Fleet uninstalls app using product name that's extracted on upload +dnf remove --assumeyes "$package_name" diff --git a/pkg/file/testdata/scripts/install_rpm.sh.golden b/pkg/file/testdata/scripts/install_rpm.sh.golden new file mode 100644 index 000000000000..fb01bc2acda5 --- /dev/null +++ b/pkg/file/testdata/scripts/install_rpm.sh.golden @@ -0,0 +1,3 @@ +#!/bin/sh + +dnf install --assumeyes "$INSTALLER_PATH" diff --git a/pkg/file/testdata/scripts/remove_rpm.sh.golden b/pkg/file/testdata/scripts/remove_rpm.sh.golden new file mode 100644 index 000000000000..efd8e0bb0c89 --- /dev/null +++ b/pkg/file/testdata/scripts/remove_rpm.sh.golden @@ -0,0 +1,6 @@ +#!/bin/sh + +package_name=$PACKAGE_ID + +# Fleet uninstalls app using product name that's extracted on upload +dnf remove --assumeyes "$package_name" diff --git a/pkg/file/testdata/scripts/uninstall_rpm.sh.golden b/pkg/file/testdata/scripts/uninstall_rpm.sh.golden new file mode 100644 index 000000000000..0d664e32f439 --- /dev/null +++ b/pkg/file/testdata/scripts/uninstall_rpm.sh.golden @@ -0,0 +1,4 @@ +package_name=$PACKAGE_ID + +# Fleet uninstalls app using product name that's extracted on upload +dnf remove --assumeyes "$package_name" diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index da9f5f7ff395..c14942189a71 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -557,6 +557,7 @@ SELECT hsi.self_service, hsi.host_deleted_at, hsi.created_at as created_at, + hsi.updated_at as updated_at, si.user_id AS software_installer_user_id, si.user_name AS software_installer_user_name, si.user_email AS software_installer_user_email diff --git a/server/datastore/mysql/software_installers_test.go b/server/datastore/mysql/software_installers_test.go index 49be5c29c220..487f2ddaadba 100644 --- a/server/datastore/mysql/software_installers_test.go +++ b/server/datastore/mysql/software_installers_test.go @@ -511,8 +511,18 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) { }) require.NoError(t, err) + beforeInstallRequest := time.Now() installUUID, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerID, false) require.NoError(t, err) + + res, err := ds.GetSoftwareInstallResults(ctx, installUUID) + require.NoError(t, err) + require.NotNil(t, res.UpdatedAt) + require.Less(t, beforeInstallRequest, res.CreatedAt) + createdAt := res.CreatedAt + require.Less(t, beforeInstallRequest, *res.UpdatedAt) + + beforeInstallResult := time.Now() err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host.ID, InstallUUID: installUUID, @@ -524,7 +534,7 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - res, err := ds.GetSoftwareInstallResults(ctx, installUUID) + res, err = ds.GetSoftwareInstallResults(ctx, installUUID) require.NoError(t, err) require.Equal(t, installUUID, res.InstallUUID) @@ -534,6 +544,10 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) { require.Equal(t, tc.preInstallQueryOutput, res.PreInstallQueryOutput) require.Equal(t, tc.postInstallScriptOutput, res.PostInstallScriptOutput) require.Equal(t, tc.installScriptOutput, res.Output) + require.NotNil(t, res.CreatedAt) + require.Equal(t, createdAt, res.CreatedAt) + require.NotNil(t, res.UpdatedAt) + require.Less(t, beforeInstallResult, *res.UpdatedAt) }) } } diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go index 09debe069579..84bcefa02ff6 100644 --- a/server/fleet/software_installer.go +++ b/server/fleet/software_installer.go @@ -363,6 +363,8 @@ func SofwareInstallerSourceFromExtensionAndName(ext, name string) (string, error switch ext { case "deb": return "deb_packages", nil + case "rpm": + return "rpm_packages", nil case "exe", "msi": return "programs", nil case "pkg": @@ -378,7 +380,7 @@ func SofwareInstallerSourceFromExtensionAndName(ext, name string) (string, error func SofwareInstallerPlatformFromExtension(ext string) (string, error) { ext = strings.TrimPrefix(ext, ".") switch ext { - case "deb": + case "deb", "rpm": return "linux", nil case "exe", "msi": return "windows", nil diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 3d6673afd3b6..79f11cdbeb9e 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -14404,3 +14404,74 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallersWithoutBundleIden } s.uploadSoftwareInstaller(payload, http.StatusOK, "") } + +func (s *integrationEnterpriseTestSuite) TestSoftwareUploadRPM() { + ctx := context.Background() + t := s.T() + + // Fedora and RHEL have hosts.platform = 'rhel'. + host := createOrbitEnrolledHost(t, "rhel", "", s.ds) + + // Upload an RPM package. + payload := &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "install script", + PreInstallQuery: "pre install query", + PostInstallScript: "post install script", + Filename: "ruby.rpm", + Title: "ruby", + } + s.uploadSoftwareInstaller(payload, http.StatusOK, "") + titleID := getSoftwareTitleID(t, s.ds, payload.Title, "rpm_packages") + + latestInstallUUID := func() string { + var id string + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + return sqlx.GetContext(ctx, q, &id, `SELECT execution_id FROM host_software_installs ORDER BY id DESC LIMIT 1`) + }) + return id + } + + // Send a request to the host to install the RPM package. + var installSoftwareResp installSoftwareResponse + beforeInstallRequest := time.Now() + s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/%d/install", host.ID, titleID), nil, http.StatusAccepted, &installSoftwareResp) + installUUID := latestInstallUUID() + + // Simulate host installing the RPM package. + beforeInstallResult := time.Now() + s.Do("POST", "/api/fleet/orbit/software_install/result", + json.RawMessage(fmt.Sprintf(`{ + "orbit_node_key": %q, + "install_uuid": %q, + "pre_install_condition_output": "1", + "install_script_exit_code": 1, + "install_script_output": "failed" + }`, *host.OrbitNodeKey, installUUID)), + http.StatusNoContent, + ) + + var resp getSoftwareInstallResultsResponse + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/install/%s/results", installUUID), nil, http.StatusOK, &resp) + assert.Equal(t, host.ID, resp.Results.HostID) + assert.Equal(t, installUUID, resp.Results.InstallUUID) + assert.Equal(t, fleet.SoftwareInstallFailed, resp.Results.Status) + assert.NotNil(t, resp.Results.PreInstallQueryOutput) + assert.Equal(t, fleet.SoftwareInstallerQuerySuccessCopy, *resp.Results.PreInstallQueryOutput) + assert.NotNil(t, resp.Results.Output) + assert.Equal(t, fmt.Sprintf(fleet.SoftwareInstallerInstallFailCopy, "failed"), *resp.Results.Output) + assert.Empty(t, resp.Results.PostInstallScriptOutput) + assert.Less(t, beforeInstallRequest, resp.Results.CreatedAt) + assert.Greater(t, time.Now(), resp.Results.CreatedAt) + assert.NotNil(t, resp.Results.UpdatedAt) + assert.Less(t, beforeInstallResult, *resp.Results.UpdatedAt) + + wantAct := fleet.ActivityTypeInstalledSoftware{ + HostID: host.ID, + HostDisplayName: host.DisplayName(), + SoftwareTitle: payload.Title, + SoftwarePackage: payload.Filename, + InstallUUID: installUUID, + Status: string(fleet.SoftwareInstallFailed), + } + s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0) +} diff --git a/server/service/testdata/software-installers/README.md b/server/service/testdata/software-installers/README.md index b5a59d9daf64..7baa9592be02 100644 --- a/server/service/testdata/software-installers/README.md +++ b/server/service/testdata/software-installers/README.md @@ -1,3 +1,4 @@ # testdata -- `fleet-osquery.msi` is a dummy MSI installer created by `packaging.BuildMSI` with a fake `orbit.exe` that just has `hello world` in it. Its software title is `Fleet osquery` and its version is `1.0.0`. \ No newline at end of file +- `fleet-osquery.msi` is a dummy MSI installer created by `packaging.BuildMSI` with a fake `orbit.exe` that just has `hello world` in it. Its software title is `Fleet osquery` and its version is `1.0.0`. +- `ruby.rpm` was downloaded from https://rpmfind.net/linux/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/r/ruby-3.3.5-15.fc42.x86_64.rpm. \ No newline at end of file diff --git a/server/service/testdata/software-installers/ruby.rpm b/server/service/testdata/software-installers/ruby.rpm new file mode 100644 index 0000000000000000000000000000000000000000..e7020796dab0014408676cc5777297253bf075b0 GIT binary patch literal 40422 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 literal 0 HcmV?d00001 From 24c84edd7b8737d8de7dd0bb5a907a8d6b0272dc Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Tue, 1 Oct 2024 12:32:41 -0400 Subject: [PATCH 09/19] fix: attempt to prevent race in profile adding and deleting (#22338) > Follow up on: #21891 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- server/datastore/mysql/apple_mdm.go | 29 +++++++++-- server/service/apple_mdm.go | 3 ++ .../service/integration_mdm_profiles_test.go | 51 +++++++++++++++++-- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 610a25db52fa..ad18242eb754 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -291,7 +291,9 @@ WHERE } func (ds *Datastore) DeleteMDMAppleConfigProfileByDeprecatedID(ctx context.Context, profileID uint) error { - return ds.deleteMDMAppleConfigProfileByIDOrUUID(ctx, profileID, "") + return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + return deleteMDMAppleConfigProfileByIDOrUUID(ctx, tx, profileID, "") + }) } func (ds *Datastore) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID string) error { @@ -299,10 +301,20 @@ func (ds *Datastore) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUI if strings.HasPrefix(profileUUID, fleet.MDMAppleDeclarationUUIDPrefix) { return ds.deleteMDMAppleDeclaration(ctx, profileUUID) } - return ds.deleteMDMAppleConfigProfileByIDOrUUID(ctx, 0, profileUUID) + return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + if err := deleteMDMAppleConfigProfileByIDOrUUID(ctx, tx, 0, profileUUID); err != nil { + return err + } + + if err := deleteUnsentAppleHostMDMProfile(ctx, tx, profileUUID); err != nil { + return err + } + + return nil + }) } -func (ds *Datastore) deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context, id uint, uuid string) error { +func deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context, tx sqlx.ExtContext, id uint, uuid string) error { var arg any stmt := `DELETE FROM mdm_apple_configuration_profiles WHERE ` if uuid != "" { @@ -312,7 +324,7 @@ func (ds *Datastore) deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context, arg = id stmt += `profile_id = ?` } - res, err := ds.writer(ctx).ExecContext(ctx, stmt, arg) + res, err := tx.ExecContext(ctx, stmt, arg) if err != nil { return ctxerr.Wrap(ctx, err) } @@ -328,6 +340,15 @@ func (ds *Datastore) deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context, return nil } +func deleteUnsentAppleHostMDMProfile(ctx context.Context, tx sqlx.ExtContext, uuid string) error { + const stmt = `DELETE FROM host_mdm_apple_profiles WHERE profile_uuid = ? AND status IS NULL AND operation_type = ? AND command_uuid = ''` + if _, err := tx.ExecContext(ctx, stmt, uuid, fleet.MDMOperationTypeInstall); err != nil { + return ctxerr.Wrap(ctx, err, "deleting host profile that has not been sent to host") + } + + return nil +} + func (ds *Datastore) DeleteMDMAppleDeclarationByName(ctx context.Context, teamID *uint, name string) error { const stmt = `DELETE FROM mdm_apple_declarations WHERE team_id = ? AND name = ?` diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 4387367ef017..c96777ea580e 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -770,9 +770,12 @@ func (svc *Service) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID } } + // This call will also delete host_mdm_apple_profiles references IFF the profile has not been sent to + // the host yet. if err := svc.ds.DeleteMDMAppleConfigProfile(ctx, profileUUID); err != nil { return ctxerr.Wrap(ctx, err) } + // cannot use the profile ID as it is now deleted if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil { return ctxerr.Wrap(ctx, err, "bulk set pending host profiles") diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go index 49a1f6eab0cf..1566fae3b114 100644 --- a/server/service/integration_mdm_profiles_test.go +++ b/server/service/integration_mdm_profiles_test.go @@ -4359,6 +4359,9 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() { testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"}) require.NoError(t, err) + teamDelete, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TeamDelete"}) + require.NoError(t, err) + testProfiles := make(map[string]fleet.MDMAppleConfigProfile) generateTestProfile := func(name string, identifier string) { i := identifier @@ -4402,6 +4405,12 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() { require.Equal(t, expected.Identifier, actual.Identifier) } + host, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t) + s.Do("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{ + TeamID: &teamDelete.ID, + HostIDs: []uint{host.ID}, + }, http.StatusOK) + // create new profile (no team) generateTestProfile("TestNoTeam", "") body, headers := generateNewReq("TestNoTeam", nil) @@ -4421,9 +4430,42 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() { require.NotEmpty(t, newCP.ProfileID) setTestProfileID("TestWithTeamID", newCP.ProfileID) + // Create a profile that we're going to remove immediately + generateTestProfile("TestImmediateDelete", "") + body, headers = generateNewReq("TestImmediateDelete", &teamDelete.ID) + newResp = s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusOK, headers) + newCP = fleet.MDMAppleConfigProfile{} + err = json.NewDecoder(newResp.Body).Decode(&newCP) + require.NoError(t, err) + require.NotEmpty(t, newCP.ProfileID) + setTestProfileID("TestImmediateDelete", newCP.ProfileID) + + // check that host_mdm_apple_profiles entry was created + var hostResp getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp) + require.NotNil(t, hostResp.Host.MDM.Profiles) + require.Len(t, *hostResp.Host.MDM.Profiles, 1) + require.Equal(t, (*hostResp.Host.MDM.Profiles)[0].Name, "TestImmediateDelete") + + // now delete the profile before it's sent, we should see the host_mdm_apple_profiles entry go + // away + deletedCP := testProfiles["TestImmediateDelete"] + deletePath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID) + var deleteResp deleteMDMAppleConfigProfileResponse + s.DoJSON("DELETE", deletePath, nil, http.StatusOK, &deleteResp) + // confirm deleted + var listResp listMDMAppleConfigProfilesResponse + s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesRequest{TeamID: teamDelete.ID}, http.StatusOK, &listResp) + require.Len(t, listResp.ConfigProfiles, 0) + getPath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID) + _ = s.DoRawWithHeaders("GET", getPath, nil, http.StatusNotFound, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)}) + // confirm no host profiles + hostResp = getHostResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp) + require.Nil(t, hostResp.Host.MDM.Profiles) + // list profiles (no team) expectedCP := testProfiles["TestNoTeam"] - var listResp listMDMAppleConfigProfilesResponse s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", nil, http.StatusOK, &listResp) require.Len(t, listResp.ConfigProfiles, 1) respCP := listResp.ConfigProfiles[0] @@ -4445,7 +4487,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() { // get profile (no team) expectedCP = testProfiles["TestNoTeam"] - getPath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", expectedCP.ProfileID) + getPath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", expectedCP.ProfileID) getResp := s.DoRawWithHeaders("GET", getPath, nil, http.StatusOK, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)}) checkGetResponse(getResp, expectedCP) @@ -4456,9 +4498,8 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() { checkGetResponse(getResp, expectedCP) // delete profile (no team) - deletedCP := testProfiles["TestNoTeam"] - deletePath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID) - var deleteResp deleteMDMAppleConfigProfileResponse + deletedCP = testProfiles["TestNoTeam"] + deletePath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID) s.DoJSON("DELETE", deletePath, nil, http.StatusOK, &deleteResp) // confirm deleted listResp = listMDMAppleConfigProfilesResponse{} From 8977594a809026ba387918a05ae2c3ab4de1e15c Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:53:09 -0400 Subject: [PATCH 10/19] Product design handbook: missing reference docs are bugs (#22541) This + announcement addresses of the following Fleet objective ([OKRs](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit?gid=1846478041#gid=1846478041)): Handbook and train team that anytime there is a missing or incorrect config setting or REST API in the docs, it is a released bug to be filed and fixed ASAP. --- handbook/product-design/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/handbook/product-design/README.md b/handbook/product-design/README.md index a81f4376f9e7..6b9a74ce64fe 100644 --- a/handbook/product-design/README.md +++ b/handbook/product-design/README.md @@ -137,6 +137,8 @@ Next, the API design DRI reviews all user stories and bugs with the release mile To signal that the reference docs branch is ready for release, the API design DRI opens a PR to `main`, adds the DRI for what goes in a release as the reviewer, and adds the release milestone. +> Anytime there is a missing or incorrect configuration option or REST API endpoint in the docs, it is treated as a released bug to be filed and fixed ASAP. + ### Interview a Product Designer candidate Ensure the interview process follows these steps in order. This process must follow [creating a new position](https://fleetdm.com/handbook/company/leadership#creating-a-new-position) through [receiving job applications](https://fleetdm.com/handbook/company/leadership#receiving-job-applications). From 514ca727ec75c5ba81be17155229d3362bd73e1a Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Tue, 1 Oct 2024 15:38:00 -0500 Subject: [PATCH 11/19] Update why-fleet.md (#22499) --- docs/Get started/why-fleet.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Get started/why-fleet.md b/docs/Get started/why-fleet.md index e0db75d68759..6ee517a43da3 100644 --- a/docs/Get started/why-fleet.md +++ b/docs/Get started/why-fleet.md @@ -4,9 +4,9 @@ Fleet is an open-source device management platform for Linux, macOS, Windows, Ch ## What's it for? -Managing computers today is getting harder. You have to juggle a mix of operating systems and devices, with a whole bunch of middleman vendors in between. +Managing computers today is getting harder. You have to juggle a mix of operating systems and devices, with a bunch of middleman vendors in between. -Fleet makes things easier by giving you a single system to manage and secure all your computing devices. You can do MDM, patch stuff, and verify anythingβ€”all from one dashboard. It's like having a universal remote control for all your organization's computers. +Fleet makes things easier by giving you a single system to secure and maintain all your computing devices over the air. You can do MDM, patch stuff, and verify anythingβ€”all from one system. It's like having a universal remote control for all your organization's computers. Fleet is open source, and free features will always be free. @@ -15,7 +15,7 @@ Fleet is open source, and free features will always be free. Fleet is used in production by IT and security teams with thousands of laptops and servers. Many deployments support tens of thousands of hosts, and a few large organizations manage deployments as large as 400,000+ hosts. - **Get what you need:** Fleet lets you work directly with [data](https://fleetdm.com/integrations) and events from the native operating system. It lets you go all the way down to the bare metal. It’s also modular. (You can turn off features you are not using.) -- **Out of the box:** Ready-to-use integrations exist for the [most common tools](https://fleetdm.com/integrations). You can also build custom workflows with the REST API, webhook events, and the fleetctl command line tool. Or go all in and manage computers [with GitOps](https://fleetdm.com/handbook/company#history). +- **Out of the box:** Ready-to-use integrations exist for the [most common tools](https://fleetdm.com/integrations). You can also build custom workflows with the REST API, webhook events, and the fleetctl command line tool. Or go all in and govern computers [with GitOps](https://github.com/fleetdm/fleet-gitops). - **Good neighbors:** We think tools should be as easy as possible for everyone to understand. We helped [create osquery](https://fleetdm.com/handbook/company#history), and we are committed to improving it. - **Free as in free:** The free version of Fleet willΒ [always be free](https://fleetdm.com/pricing). Fleet isΒ independently backedΒ and actively maintained with the help of many amazingΒ contributors. From a9a9e92f3f1fc3616d9d81e046e38c2a5d2d5df0 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Tue, 1 Oct 2024 17:38:22 -0300 Subject: [PATCH 12/19] Use node version defined in package.json (#22504) We did the same thing for Go. (This allows us to not require admin permissions to update the used Node version in CI.) --- .github/workflows/build-binaries.yaml | 7 +++---- .github/workflows/fleet-and-orbit.yml | 5 ++--- .github/workflows/goreleaser-fleet.yaml | 5 ++--- .github/workflows/goreleaser-snapshot-fleet.yaml | 4 ++-- .github/workflows/test-js.yml | 14 +++++++------- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build-binaries.yaml b/.github/workflows/build-binaries.yaml index 278f958b2842..8499171c1583 100644 --- a/.github/workflows/build-binaries.yaml +++ b/.github/workflows/build-binaries.yaml @@ -37,11 +37,10 @@ jobs: with: go-version-file: 'go.mod' - # Set the Node.js version - - name: Set up Node.js ${{ vars.NODE_VERSION }} + - name: Set up Node.js uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: ${{ vars.NODE_VERSION }} + node-version-file: package.json - name: JS Dependency Cache id: js-cache @@ -51,7 +50,7 @@ jobs: **/node_modules # Use a separate cache for this from other JS jobs since we run the # webpack steps and will have more to cache. - key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}-node_version-${{ vars.NODE_VERSION }} + key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-node_modules- diff --git a/.github/workflows/fleet-and-orbit.yml b/.github/workflows/fleet-and-orbit.yml index f4dfb2780eb7..22be676c7868 100644 --- a/.github/workflows/fleet-and-orbit.yml +++ b/.github/workflows/fleet-and-orbit.yml @@ -79,11 +79,10 @@ jobs: with: go-version-file: 'go.mod' - # Set the Node.js version - - name: Set up Node.js ${{ vars.NODE_VERSION }} + - name: Set up Node.js uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: ${{ vars.NODE_VERSION }} + node-version-file: package.json - name: Start tunnel env: diff --git a/.github/workflows/goreleaser-fleet.yaml b/.github/workflows/goreleaser-fleet.yaml index 6ba9aff8f08f..7a23296ef2fe 100644 --- a/.github/workflows/goreleaser-fleet.yaml +++ b/.github/workflows/goreleaser-fleet.yaml @@ -46,11 +46,10 @@ jobs: with: go-version-file: 'go.mod' - # Set the Node.js version - - name: Set up Node.js ${{ vars.NODE_VERSION }} + - name: Set up Node.js uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: ${{ vars.NODE_VERSION }} + node-version-file: package.json - name: Install JS Dependencies run: make deps-js diff --git a/.github/workflows/goreleaser-snapshot-fleet.yaml b/.github/workflows/goreleaser-snapshot-fleet.yaml index 927cf31be1da..f3c6a9340c96 100644 --- a/.github/workflows/goreleaser-snapshot-fleet.yaml +++ b/.github/workflows/goreleaser-snapshot-fleet.yaml @@ -60,10 +60,10 @@ jobs: go-version-file: 'go.mod' # Set the Node.js version - - name: Set up Node.js ${{ vars.NODE_VERSION }} + - name: Set up Node.js uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: ${{ vars.NODE_VERSION }} + node-version-file: package.json - name: Install Dependencies run: make deps diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 15b4fd05cee5..2b547e1ad5c7 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -42,14 +42,14 @@ jobs: with: egress-policy: audit - - name: Set up Node.js ${{ vars.NODE_VERSION }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 - with: - node-version: ${{ vars.NODE_VERSION }} - - name: Checkout Code uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Set up Node.js + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + with: + node-version-file: package.json + - name: JS Dependency Cache id: js-cache uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v2 @@ -87,10 +87,10 @@ jobs: with: egress-policy: audit - - name: Set up Node.js ${{ vars.NODE_VERSION }} + - name: Set up Node.js uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: ${{ vars.NODE_VERSION }} + node-version-file: package.json - name: Checkout Code uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 From 00d31e84505907824e6a4bfd1183bf6d026e7806 Mon Sep 17 00:00:00 2001 From: Rachael Shaw Date: Tue, 1 Oct 2024 15:39:00 -0500 Subject: [PATCH 13/19] Update linux-device-health.policies.yml (#22516) See https://github.com/fleetdm/fleet/pull/22498 --- it-and-security/lib/linux-device-health.policies.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/it-and-security/lib/linux-device-health.policies.yml b/it-and-security/lib/linux-device-health.policies.yml index 0d9e2f8aa2fb..b7093c02acdc 100644 --- a/it-and-security/lib/linux-device-health.policies.yml +++ b/it-and-security/lib/linux-device-health.policies.yml @@ -1,6 +1,6 @@ - name: Linux - Enable disk encryption - query: SELECT 1 FROM disk_encryption WHERE encrypted=1 AND name LIKE '/dev/dm-1'; + query: SELECT 1 FROM mounts m, disk_encryption d WHERE m.device_alias = d.name AND d.encrypted = 1 AND m.path = '/'; critical: false description: This policy checks if disk encryption is enabled. resolution: As an IT admin, deploy an image that includes disk encryption. - platform: linux \ No newline at end of file + platform: linux From beec753a3f055f63f3eacfb6ce1c84e6fcb9a6d2 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:07:30 -0400 Subject: [PATCH 14/19] API docs: OTA enrollment profile (#22457) - Bring OTA enrollment profile endpoint into REST API docs --- .../config-less-fleetd-agent-deployment.md | 43 ++++++++++- docs/Contributing/API-for-contributors.md | 1 - docs/REST API/rest-api.md | 75 +++++++++++++++++++ 3 files changed, 116 insertions(+), 3 deletions(-) diff --git a/articles/config-less-fleetd-agent-deployment.md b/articles/config-less-fleetd-agent-deployment.md index eb37159fdcf5..2ca3b5cd7c93 100644 --- a/articles/config-less-fleetd-agent-deployment.md +++ b/articles/config-less-fleetd-agent-deployment.md @@ -4,7 +4,7 @@ Deploying Fleet's agent across a diverse range of devices often involves the crucial step of enrolling each device. Traditionally, this involves [packaging](https://fleetdm.com/docs/using-fleet/fleetd#packaging) `fleetd` with configuration including the enroll secret and server URL. While effective, an alternative offers more flexibility in your deployment process. This guide introduces a different approach for deploying Fleet's agent without embedding configuration settings directly into `fleetd`. Ideal for IT administrators who prefer to generate a single package and maintain greater control over the distribution of enrollment secrets and server URLs, this method simplifies the enrollment process across macOS and Windows hosts. -Emphasizing adaptability and convenience, this approach allows for a more efficient way to manage device enrollments. Let’s dive into how to deploy Fleet's agent using this alternative method, ensuring a more open and flexible deployment process. +This approach emphasizes adaptability and convenience and allows for a more efficient way to manage device enrollments. Let’s explore how to deploy Fleet's agent using this alternative method, ensuring a more open and flexible deployment process. ## For macOS: @@ -44,6 +44,18 @@ fleetctl package --type=pkg --use-system-configuration --fleet-desktop PayloadVersion 1 + + EndUserEmail + END_USER_EMAIL_HERE + PayloadIdentifier + com.fleetdm.fleet.mdm.apple.mdm + PayloadType + com.apple.mdm + PayloadUUID + 29713130-1602-4D27-90C9-B822A295E44E + PayloadVersion + 1 + PayloadDisplayName Fleetd configuration @@ -56,11 +68,38 @@ fleetctl package --type=pkg --use-system-configuration --fleet-desktop PayloadVersion 1 PayloadDescription - Default configuration for the fleetd agent. + Configuration for the fleetd agent. ``` +You can optionally specify the `END_USER_EMAIL` that will be added to the host's [human-device mapping](https://fleetdm.com/docs/rest-api/rest-api#get-human-device-mapping): + +```xml + + + + + PayloadContent + + ... + + EndUserEmail + END_USER_EMAIL + PayloadIdentifier + com.fleetdm.fleet.mdm.apple.mdm + PayloadType + com.apple.mdm + PayloadUUID + 29713130-1602-4D27-90C9-B822A295E44E + PayloadVersion + 1 + + + ... + + +``` ## For Windows: diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index 074c3338b4c2..64caabc4cbf6 100644 --- a/docs/Contributing/API-for-contributors.md +++ b/docs/Contributing/API-for-contributors.md @@ -549,7 +549,6 @@ The MDM endpoints exist to support the related command-line interface sub-comman - [Get FileVault statistics](#get-filevault-statistics) - [Upload VPP content token](#upload-vpp-content-token) - [Disable VPP](#disable-vpp) -- [Get an over the air (OTA) enrollment profile](#get-an-over-the-air-ota-enrollment-profile) ### Generate Apple Business Manager public key (ADE) diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 0a760c32d5fc..a42ec1865781 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -5573,6 +5573,7 @@ solely on the response status code returned by this endpoint. ``` ###### Example response body + ```xml @@ -5720,6 +5721,7 @@ Get aggregate status counts of profiles for to macOS and Windows hosts that are - [Set custom MDM setup enrollment profile](#set-custom-mdm-setup-enrollment-profile) - [Get custom MDM setup enrollment profile](#get-custom-mdm-setup-enrollment-profile) - [Delete custom MDM setup enrollment profile](#delete-custom-mdm-setup-enrollment-profile) +- [Get Over-the-Air (OTA) enrollment profile](#get-over-the-air-ota-enrollment-profile) - [Get manual enrollment profile](#get-manual-enrollment-profile) - [Upload a bootstrap package](#upload-a-bootstrap-package) - [Get metadata about a bootstrap package](#get-metadata-about-a-bootstrap-package) @@ -5827,10 +5829,83 @@ Deletes the custom MDM setup enrollment profile assigned to a team or no team. `Status: 204` +### Get Over-the-Air (OTA) enrollment profile + +`GET /api/v1/fleet/enrollment_profiles/ota` + +The returned value is a signed `.mobileconfig` OTA enrollment profile. Install this profile on macOS, iOS, or iPadOS hosts to enroll them to a specific team in Fleet and turn on MDM features. + +To enroll macOS hosts, turn on MDM features, and add [human-device mapping](#get-human-device-mapping), install the [manual enrollment profile](#get-manual-enrollment-profile) instead. + +Learn more about OTA profiles [here](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/iPhoneOTAConfiguration/OTASecurity/OTASecurity.html). + +#### Parameters + +| Name | Type | In | Description | +|-------------------|---------|-------|----------------------------------------------------------------------------------| +| enroll_secret | string | query | **Required**. The enroll secret of the team this host will be assigned to. | + +#### Example + +`GET /api/v1/fleet/enrollment_profiles/ota?enroll_secret=foobar` + +##### Default response + +`Status: 200` + +> **Note:** To confirm success, it is important for clients to match content length with the response header (this is done automatically by most clients, including the browser) rather than relying solely on the response status code returned by this endpoint. + +##### Example response headers + +```http + Content-Length: 542 + Content-Type: application/x-apple-aspen-config; charset=urf-8 + Content-Disposition: attachment;filename="fleet-mdm-enrollment-profile.mobileconfig" + X-Content-Type-Options: nosniff +``` + +###### Example response body + +```xml + + + + + PayloadContent + + URL + https://foo.example.com/api/fleet/ota_enrollment?enroll_secret=foobar + DeviceAttributes + + UDID + VERSION + PRODUCT + SERIAL + + + PayloadOrganization + Acme Inc. + PayloadDisplayName + Acme Inc. enrollment + PayloadVersion + 1 + PayloadUUID + fdb376e5-b5bb-4d8c-829e-e90865f990c9 + PayloadIdentifier + com.fleetdm.fleet.mdm.apple.ota + PayloadType + Profile Service + + +``` + + ### Get manual enrollment profile Retrieves an unsigned manual enrollment profile for macOS hosts. Install this profile on macOS hosts to turn on MDM features manually. +To add [human-device mapping](#get-human-device-mapping), add the end user's email to the enrollment profle. Learn how [here](https://fleetdm.com/guides/config-less-fleetd-agent-deployment#basic-article). + `GET /api/v1/fleet/enrollment_profiles/manual` ##### Example From c545495f607cd4819288b52307cf5ba9b66a3573 Mon Sep 17 00:00:00 2001 From: Marko Lisica <83164494+marko-lisica@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:09:33 +0200 Subject: [PATCH 15/19] API design: Self-service: Install Apple App Store apps on macOS (#22102) API design for: - #19620 --- docs/Configuration/yaml-files.md | 4 +- docs/Contributing/API-for-contributors.md | 191 +++++++++++++++++++++- docs/REST API/rest-api.md | 178 ++++---------------- 3 files changed, 228 insertions(+), 145 deletions(-) diff --git a/docs/Configuration/yaml-files.md b/docs/Configuration/yaml-files.md index 7599fd259f9a..fae1c212c192 100644 --- a/docs/Configuration/yaml-files.md +++ b/docs/Configuration/yaml-files.md @@ -354,7 +354,9 @@ software: - `app_store_id` is the ID of the Apple App Store app. You can find this at the end of the app's App Store URL. For example, "Bear - Markdown Notes" URL is "https://apps.apple.com/us/app/bear-markdown-notes/id1016366447" and the `app_store_id` is `1016366447`. -> Make sure to include only the ID itself, and not the `id` prefix shown in the URL. The ID must be wrapped in quotes as shown in the example so that it is processed as a string. +> Make sure to include only the ID itself, and not the `id` prefix shown in the URL. The ID must be wrapped in quotes as shown in the example so that it is processed as a string. + +`self_service` only applies to macOS, and is ignored for other platforms. For example, if the app is supported on macOS, iOS, and iPadOS, and `self_service` is set to `true`, it will be self-service on macOS workstations but not iPhones or iPads. ##### Separate file diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index 64caabc4cbf6..27f819f72467 100644 --- a/docs/Contributing/API-for-contributors.md +++ b/docs/Contributing/API-for-contributors.md @@ -541,6 +541,10 @@ The MDM endpoints exist to support the related command-line interface sub-comman - [Renew VPP token](#renew-vpp-token) - [Delete VPP token](#delete-vpp-token) - [Batch-apply MDM custom settings](#batch-apply-mdm-custom-settings) +- [Batch-apply packages](#batch-apply-packages) +- [Batch-apply App Store apps](#batch-apply-app-store-apps) +- [Get token to download package](#get-token-to-download-package) +- [Download package using a token](#download-package-using-a-token) - [Initiate SSO during DEP enrollment](#initiate-sso-during-dep-enrollment) - [Complete SSO during DEP enrollment](#complete-sso-during-dep-enrollment) - [Over the air enrollment](#over-the-air-enrollment) @@ -1743,7 +1747,7 @@ If the `name` is not already associated with an existing team, this API route cr | scripts | list | body | A list of script files to add to this team so they can be executed at a later time. | | software | object | body | The team's software that will be available for install. | | software.packages | list | body | An array of objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, `post_install_script` - script that runs after software install, and `self_service` boolean. | -| software.app_store_apps | list | body | An array objects. Each object consists of `app_store_id` - ID of the App Store app formatted as a string (in quotes) rather than a number. | +| software.app_store_apps | list | body | An array of objects. Each object consists of `app_store_id` - ID of the App Store app and `self_service` boolean. | | mdm.macos_settings.enable_disk_encryption | bool | body | Whether disk encryption should be enabled for hosts that belong to this team. | | force | bool | query | Force apply the spec even if there are (ignorable) validation errors. Those are unknown keys and agent options-related validations. | | dry_run | bool | query | Validate the provided JSON for unknown keys and invalid value types and return any validation errors, but do not apply the changes. | @@ -1836,6 +1840,7 @@ If the `name` is not already associated with an existing team, this API route cr "app_store_apps": [ { "app_store_id": "12464567", + "self_service": true } ] } @@ -3376,3 +3381,187 @@ Run a live script and get results back (5 minute timeout). Live scripts only run "exit_code": 0 } ``` +## Software + +### Batch-apply software + +_Available in Fleet Premium._ + +`POST /api/v1/fleet/software/batch` + +This endpoint is asynchronous, meaning it will start a background process to download and apply the software and return a `request_uuid` in the JSON response that can be used to query the status of the batch-apply (using the `GET /api/v1/fleet/software/batch/:request_uuid` endpoint defined below). + +#### Parameters + +| Name | Type | In | Description | +| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| team_name | string | query | The name of the team to add the software package to. Ommitting these parameters will add software to 'No Team'. | +| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. | +| software | object | body | The team's software that will be available for install. | +| software.packages | list | body | An array of objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, `post_install_script` - script that runs after software install, and `uninstall_script` - command that Fleet runs to uninstall software. | + +#### Example + +`POST /api/v1/fleet/software/batch` + +##### Default response + +`Status: 200` +```json +{ + "request_uuid": "ec23c7b6-c336-4109-b89d-6afd859659b4", +} +``` + +### Get status of software batch-apply request + +_Available in Fleet Premium._ + +`GET /api/v1/fleet/software/batch/:request_uuid` + +This endpoint allows querying the status of a batch-apply software request (`POST /api/v1/fleet/software/batch`). +Returns `"status"` field that can be one of `"processing"`, `"complete"` or `"failed"`. +If `"status"` is `"completed"` then the `"packages"` field contains the applied packages. +If `"status"` is `"processing"` then the operation is ongoing and the request should be retried. +If `"status"` is `"failed"` then the `"message"` field contains the error message. + +#### Parameters + +| Name | Type | In | Description | +| ------------ | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| request_uuid | string | query | The request_uuid returned by the `POST /api/v1/fleet/software/batch` endpoint. | +| team_name | string | query | The name of the team to add the software package to. Ommitting these parameters will add software to 'No Team'. | +| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. | + +##### Default responses + +`Status: 200` +```json +{ + "status": "processing", + "message": "", + "packages": null +} +``` + +`Status: 200` +```json +{ + "status": "completed", + "message": "", + "packages": [ + { + "team_id": 1, + "title_id": 2751, + "url": "https://ftp.mozilla.org/pub/firefox/releases/129.0.2/win64/en-US/Firefox%20Setup%20129.0.2.msi" + } + ] +} +``` + +`Status: 200` +```json +{ + "status": "failed", + "message": "validation failed: software.url Couldn't edit software. URL (\"https://foobar.does.not.exist.com\") returned \"Not Found\". Please make sure that URLs are reachable from your Fleet server.", + "packages": null +} +``` + +### Batch-apply App Store apps + +_Available in Fleet Premium._ + +`POST /api/latest/fleet/software/app_store_apps/batch` + +#### Parameters + +| Name | Type | In | Description | +| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| team_name | string | query | The name of the team to add the software package to. Ommitting this parameter will add software to "No team". | +| dry_run | bool | query | If `true`, will validate the provided VPP apps and return any validation errors, but will not apply the changes. | +| app_store_apps | list | body | An array of objects. Each object contains `app_store_id` and `self_service`. | +| app_store_apps.app_store_id | string | body | ID of the App Store app. | +| app_store_apps.self_service | boolean | body | Whether the VPP app is "Self-service" or not. | + +#### Example + +`POST /api/latest/fleet/software/app_store_apps/batch` +```json +{ + "team_name": "Foobar", + "app_store_apps": { + { + "app_store_id": "597799333", + "self_service": false, + }, + { + "app_store_id": "497799835", + "self_service": true, + } + } +} +``` + +##### Default response + +`Status: 204` + +### Get token to download package + +_Available in Fleet Premium._ + +`POST /api/v1/fleet/software/titles/:software_title_id/package/token?alt=media` + +The returned token is a one-time use token that expires after 10 minutes. + +#### Parameters + +| Name | Type | In | Description | +|-------------------|---------|-------|------------------------------------------------------------------| +| software_title_id | integer | path | **Required**. The ID of the software title for software package. | +| team_id | integer | query | **Required**. The team ID containing the software package. | +| alt | integer | query | **Required**. Must be specified and set to "media". | + +#### Example + +`POST /api/v1/fleet/software/titles/123/package/token?alt=media&team_id=2` + +##### Default response + +`Status: 200` + +```json +{ + "token": "e905e33e-07fe-4f82-889c-4848ed7eecb7" +} +``` + +### Download package using a token + +_Available in Fleet Premium._ + +`GET /api/v1/fleet/software/titles/:software_title_id/package/token/:token?alt=media` + +#### Parameters + +| Name | Type | In | Description | +|-------------------|---------|------|--------------------------------------------------------------------------| +| software_title_id | integer | path | **Required**. The ID of the software title to download software package. | +| token | string | path | **Required**. The token to download the software package. | + +#### Example + +`GET /api/v1/fleet/software/titles/123/package/token/e905e33e-07fe-4f82-889c-4848ed7eecb7` + +##### Default response + +`Status: 200` + +```http +Status: 200 +Content-Type: application/octet-stream +Content-Disposition: attachment +Content-Length: +Body: +``` diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index a42ec1865781..cb23ae838055 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -4295,8 +4295,10 @@ Resends a configuration profile for the specified host. "name": "Logic Pro", "software_package": null "app_store_app": { - "app_store_id": "1091189122" + "app_store_id": "1091189122", + "icon_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f4/25/1f/f4251f60-e27a-6f05-daa7-9f3a63aac929/AppIcon-0-0-85-220-0-0-4-0-0-2x-0-0-0-0-0.png/512x512bb.png" "version": "2.04", + "self_service": false, "last_install": { "command_uuid": "0aa14ae5-58fe-491a-ac9a-e4ee2b3aac40", "installed_at": "2024-05-15T15:23:57Z" @@ -6517,7 +6519,8 @@ None. ] ``` -Get Volume Purchasing Program (VPP) +### Get Volume Purchasing Program (VPP) + > **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. @@ -8727,10 +8730,6 @@ Deletes the session specified by ID. When the user associated with the session n - [Get package install result](#get-package-install-result) - [Download package](#download-package) - [Delete package or App Store app](#delete-package-or-app-store-app) -- [Batch-apply software](#batch-apply-software) -- [Batch-apply app store apps](#batch-apply-app-store-apps) -- [Get token to download package](#get-token-to-download-package) -- [Download package using a token](#download-package-using-a-token) ### List software @@ -9101,9 +9100,10 @@ Returns information about the specified software. By default, `versions` are sor "software_package": null, "app_store_app": { "name": "Logic Pro", - "app_store_id": "1091189122", + "app_store_id": 1091189122, "latest_version": "2.04", "icon_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f1/65/1e/a4844ccd-486d-455f-bb31-67336fe46b14/AppIcon-1x_U007emarketing-0-7-0-85-220-0.png/512x512bb.jpg", + "self_service": true, "status": { "installed": 3, "pending": 1, @@ -9182,6 +9182,7 @@ Returns information about the specified software version. } ``` + ### Get operating system version Retrieves information about the specified operating system (OS) version. @@ -9454,6 +9455,7 @@ Add App Store (VPP) app purchased in Apple Business Manager. | app_store_id | string | body | **Required.** The ID of App Store app. | | team_id | integer | body | **Required**. The team ID. Adds VPP software to the specified team. | | platform | string | body | The platform of the app (`darwin`, `ios`, or `ipados`). Default is `darwin`. | +| self_service | boolean | body | Self-service software is optional and can be installed by the end user. | #### Example @@ -9466,6 +9468,7 @@ Add App Store (VPP) app purchased in Apple Business Manager. "app_store_id": "497799835", "team_id": 2, "platform": "ipados" + "self_service": true } ``` @@ -9473,38 +9476,6 @@ Add App Store (VPP) app purchased in Apple Business Manager. `Status: 200` -### Download package - -> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. - -_Available in Fleet Premium._ - -`GET /api/v1/fleet/software/titles/:software_title_id/package?alt=media` - -#### Parameters - -| Name | Type | In | Description | -| ---- | ------- | ---- | -------------------------------------------- | -| software_title_id | integer | path | **Required**. The ID of the software title to download software package.| -| team_id | integer | query | **Required**. The team ID. Downloads a software package added to the specified team. | -| alt | integer | query | **Required**. If specified and set to "media", downloads the specified software package. | - -#### Example - -`GET /api/v1/fleet/software/titles/123/package?alt=media?team_id=2` - -##### Default response - -`Status: 200` - -```http -Status: 200 -Content-Type: application/octet-stream -Content-Disposition: attachment -Content-Length: -Body: -``` - ### Install package or App Store app > **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. @@ -9562,7 +9533,7 @@ _Available in Fleet Premium._ `GET /api/v1/fleet/software/install/:install_uuid/results` -Get the results of a software package install. +Get the results of a software package install. To get the results of an App Store app install, use the [List MDM commands](#list-mdm-commands) and [Get MDM command results](#get-mdm-command-results) API enpoints. Fleet uses an MDM command to install App Store apps. @@ -9593,141 +9564,62 @@ To get the results of an App Store app install, use the [List MDM commands](#lis } ``` -### Delete package or App Store app +### Download package > **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. _Available in Fleet Premium._ -Deletes software that's available for install (package or App Store app). - -`DELETE /api/v1/fleet/software/titles/:software_title_id/available_for_install` +`GET /api/v1/fleet/software/titles/:software_title_id/package?alt=media` #### Parameters | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | -| software_title_id | integer | path | **Required**. The ID of the software title to delete software available for install. | -| team_id | integer | query | **Required**. The team ID. Deletes a software package added to the specified team. | - -#### Example - -`DELETE /api/v1/fleet/software/titles/24/available_for_install?team_id=2` - -##### Default response - -`Status: 204` - -### Batch-apply software - -_Available in Fleet Premium._ - -`POST /api/v1/fleet/software/batch` - -#### Parameters - -| Name | Type | In | Description | -| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request; omit this parameter if using `team_name`. Omitting these parameters will add software to "No Team". | -| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request; omit this parameter if using `team_id`. Omitting these parameters will add software to "No Team". | -| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. | -| software | object | body | The team's software that will be available for install. | -| software.packages | list | body | An array of objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, `post_install_script` - script that runs after software install, and `uninstall_script` - command that Fleet runs to uninstall software. | -| software.app_store_apps | list | body | An array objects. Each object consists of `app_store_id` - ID of the App Store app. | - -If both `team_id` and `team_name` parameters are included, this endpoint will respond with an error. If no `team_name` or `team_id` is provided, the scripts will be applied for **all hosts**. - -#### Example - -`POST /api/v1/fleet/software/batch` - -##### Default response - -`Status: 204` - -### Batch-apply app store apps - -_Available in Fleet Premium._ - -`POST /api/v1/fleet/software/app_store_apps/batch` - -#### Parameters - -| Name | Type | In | Description | -|-----------------|---------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| team_name | integer | query | **Required**. The name of the team to add the app to. | -| dry_run | bool | query | If `true`, will validate the provided apps and return any validation errors, but will not apply the changes. | -| apps_store_apps | list | body | The list of objects containing `app_store_id`: a string representation of the app's App ID, `self_service`: a bool indicating if the app's installation can be initiated by end users. | - -> Note that this endpoint replaces all apps associated with a team. - -#### Example - -`POST /api/v1/fleet/software/app_store_apps/batch` - -#### Default response - -`Status: 204` - -### Get token to download package - -_Available in Fleet Premium._ - -`POST /api/v1/fleet/software/titles/:software_title_id/package/token?alt=media` - -The returned token is a one-time use token that expires after 10 minutes. - -#### Parameters - -| Name | Type | In | Description | -|-------------------|---------|-------|------------------------------------------------------------------| -| software_title_id | integer | path | **Required**. The ID of the software title for software package. | -| team_id | integer | query | **Required**. The team ID containing the software package. | -| alt | integer | query | **Required**. Must be specified and set to "media". | +| software_title_id | integer | path | **Required**. The ID of the software title to download software package.| +| team_id | integer | query | **Required**. The team ID. Downloads a software package added to the specified team. | +| alt | integer | query | **Required**. If specified and set to "media", downloads the specified software package. | #### Example -`POST /api/v1/fleet/software/titles/123/package/token?alt=media&team_id=2` +`GET /api/v1/fleet/software/titles/123/package?alt=media?team_id=2` ##### Default response `Status: 200` -```json -{ - "token": "e905e33e-07fe-4f82-889c-4848ed7eecb7" -} +```http +Status: 200 +Content-Type: application/octet-stream +Content-Disposition: attachment +Content-Length: +Body: ``` -### Download package using a token +### Delete package or App Store app + +> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. _Available in Fleet Premium._ -`GET /api/v1/fleet/software/titles/:software_title_id/package/token/:token?alt=media` +Deletes software that's available for install (package or App Store app). + +`DELETE /api/v1/fleet/software/titles/:software_title_id/available_for_install` #### Parameters -| Name | Type | In | Description | -|-------------------|---------|------|--------------------------------------------------------------------------| -| software_title_id | integer | path | **Required**. The ID of the software title to download software package. | -| token | string | path | **Required**. The token to download the software package. | +| Name | Type | In | Description | +| ---- | ------- | ---- | -------------------------------------------- | +| software_title_id | integer | path | **Required**. The ID of the software title to delete software available for install. | +| team_id | integer | query | **Required**. The team ID. Deletes a software package added to the specified team. | #### Example -`GET /api/v1/fleet/software/titles/123/package/token/e905e33e-07fe-4f82-889c-4848ed7eecb7` +`DELETE /api/v1/fleet/software/titles/24/available_for_install?team_id=2` ##### Default response -`Status: 200` - -```http -Status: 200 -Content-Type: application/octet-stream -Content-Disposition: attachment -Content-Length: -Body: -``` - +`Status: 204` ## Vulnerabilities From f8fff1685dd51e51e3f6133555ce121d15f52be1 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Tue, 1 Oct 2024 18:25:17 -0300 Subject: [PATCH 16/19] Fix lint-js (#22557) I missed this change in https://github.com/fleetdm/fleet/pull/22504 --- .github/workflows/test-js.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 2b547e1ad5c7..ab60d81939a2 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -87,14 +87,14 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Set up Node.js uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version-file: package.json - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: JS Dependency Cache id: js-cache uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v2 From fd928e78b9742a2af715ecd490ad4f69fd58e1be Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 1 Oct 2024 16:34:01 -0500 Subject: [PATCH 17/19] Website: add JNUC banner to device-management page (#22560) Closes: https://github.com/fleetdm/fleet/issues/22559 Changes: -Added a banner to the top of the device management page for JNUC atendees to book a meeting with the CEO. --- .../assets/images/icon-hand-wave-20x20@2x.png | Bin 0 -> 3655 bytes .../styles/pages/device-management.less | 21 ++++++++++++++++++ website/views/pages/device-management.ejs | 1 + 3 files changed, 22 insertions(+) create mode 100644 website/assets/images/icon-hand-wave-20x20@2x.png diff --git a/website/assets/images/icon-hand-wave-20x20@2x.png b/website/assets/images/icon-hand-wave-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ab60da6214029ac30c0a3aed79d6d87e70278414 GIT binary patch literal 3655 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 Z@Edh7l=U&8RX_j$002ovPDHLkV1l!>@mc@? literal 0 HcmV?d00001 diff --git a/website/assets/styles/pages/device-management.less b/website/assets/styles/pages/device-management.less index 10557dc6196e..551df9ac5800 100644 --- a/website/assets/styles/pages/device-management.less +++ b/website/assets/styles/pages/device-management.less @@ -39,6 +39,27 @@ strong { color: @core-fleet-black; } + [purpose='jnuc-banner'] { + height: 44px; + background-color: #0587FF; + p { + color: #FFF; + font-size: 14px; + font-weight: 700; + margin-bottom: 0px; + } + a { + color: #FFF; + text-decoration-line: underline; + text-underline-offset: 2px; + + } + img { + display: inline; + height: 20px; + margin-right: 10px; + } + } [purpose='page-container'] { padding-left: 64px; padding-right: 64px; diff --git a/website/views/pages/device-management.ejs b/website/views/pages/device-management.ejs index 0c077596e59a..409a23381318 100644 --- a/website/views/pages/device-management.ejs +++ b/website/views/pages/device-management.ejs @@ -1,4 +1,5 @@
+
πŸ‘‹

Attending JNUC? Chat with our CEO.

From 570d1847f2a01de8df32dc65f6aa66441d75c2a4 Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:08:48 -0500 Subject: [PATCH 18/19] Update change a fleetie's job title (#22561) --- handbook/company/leadership.md | 6 +++++- handbook/digital-experience/README.md | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/handbook/company/leadership.md b/handbook/company/leadership.md index 1cad2e84de28..90c9d0b2a70c 100644 --- a/handbook/company/leadership.md +++ b/handbook/company/leadership.md @@ -415,7 +415,11 @@ Although it's sad to see someone go, Fleet understands that not everything is me ## Changing someone's position -From time to time, someone's job title changes. To do this, reach out to [Digital Experience](https://fleetdm.com/handbook/digital-experience). + +From time to time, someone's job title changes. Use the following steps to change someone's position: +1. Create Slack channel: Create a private "#YYYY-change-title-for-xxxxxx" Slack channel (where "xxxxxx" is the Fleetie's name and YYYY is the current year) for discussion and invite the CEO and Head of Digital Experience. +2. At-mention the Head of Digital Experience in the new channel with any context regarding the title change. Share any related documents with the Head of Digital Experience and the CEO. +3. After getting approval from the [Head of People](https://fleetdm.com/handbook/digital-experience#team), the Digital Experience team will take the necessary steps to [change the fleetie's job title](https://fleetdm.com/handbook/digital-experience#change-a-fleeties-job-title). image diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md index b634a973ec1f..ab17f0cf5c89 100644 --- a/handbook/digital-experience/README.md +++ b/handbook/digital-experience/README.md @@ -91,7 +91,8 @@ When a Fleetie, consultant or advisor requests an update to their personnel deta ### Change a Fleetie's job title When Digital Experience receives notification of a Fleetie's job title changing, follow these steps to ensure accurate recording of the change across our systems. -1. Update ["πŸ§‘β€πŸš€ Fleeties"](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0): +1. The Head of Digital Experience will bring the proposed title change to the next Roundup meeting with the CEO for approval. If the proposed change is rejected, the Head of Digital Experience will inform the requesting manager as to why. +1. If approved, update ["πŸ§‘β€πŸš€ Fleeties"](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0): - Search the spreadsheet for the Fleetie in need of a job title change. - Input the new job title in the Fleetie's row in the "Job title" cell. - Navigate to the "Org chart" tab of the spreadsheet, and verify that the Fleetie's title appears correctly in the org chart. From 8cfa35c1c55c7e830c7375910959770324579d0f Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:55:54 -0400 Subject: [PATCH 19/19] fleetdm.com/pricing: Disk encryption for Linux coming soon (#22543) - Mention [LUKS](https://access.redhat.com/solutions/100463) --- handbook/company/pricing-features-table.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handbook/company/pricing-features-table.yml b/handbook/company/pricing-features-table.yml index 3070d8d19c7a..e36234d9e617 100644 --- a/handbook/company/pricing-features-table.yml +++ b/handbook/company/pricing-features-table.yml @@ -378,7 +378,7 @@ # β•‘β•£ β•‘β•‘β•‘β• β•£ β•‘ ║╠╦╝║ β•‘β•£ β•‘β•‘β•‘β•šβ•β•—β• β•©β•— β•‘β•£ β•‘β•‘β•‘β•‘ β• β•¦β•β•šβ•¦β•β• β•β• β•‘ β•‘β•‘ β•‘β•‘β•‘β•‘ # β•šβ•β•β•β•šβ•β•š β•šβ•β•β•©β•šβ•β•šβ•β•β•šβ•β• β•β•©β•β•©β•šβ•β•β•© β•© β•šβ•β•β•β•šβ•β•šβ•β•β•©β•šβ• β•© β•© β•© β•©β•šβ•β•β•β•šβ• - industryName: Enforce disk encryption - description: Encrypt system drives on macOS and Windows computers, manage escrowed encryption keys, and report on disk encryption status (FileVault, BitLocker). + description: Encrypt system drives on macOS, Windows, and Linux (coming soon) computers, manage escrowed encryption keys, and report on disk encryption status (FileVault, BitLocker, LUKS). documentationUrl: https://fleetdm.com/docs/using-fleet/mdm-disk-encryption friendlyName: Ensure hard disks are encrypted productCategories: [Device management]