From 4b82ff64e00cc47a5fb05f9241f13c98faaf4430 Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:32:41 -0500 Subject: [PATCH 001/119] Update README.md (#19570) --- handbook/digital-experience/README.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md index 91d51db08d..4084572a0c 100644 --- a/handbook/digital-experience/README.md +++ b/handbook/digital-experience/README.md @@ -165,28 +165,23 @@ If the action fails, please complete the following steps: ### Communicate Fleet's potential energy to stakeholders -On the first business day of every month, the Apprentice will send an update to the stakeholders of Fleet using the following steps: +On the first business day of every month, the Head of Digital Experience will send an update to the stakeholders of Fleet using the following steps: 1. Copy the following template into an outgoing email with the subject line: "[Investor update] Fleet, YYYY-MM". ``` Hi investors and friends, -Here’s a quick update on the numbers from last month: -• Gross new ∆ARR (QTD): + TODO -• Social media mentions (LinkedIn): 3.8 per day (Goal: 5) (Want to help?) -• Current version: 4.48.0 (See what's new) -• Next in-person event: Kansas City, (April 20) BSides KC -• Next press release: 2024-04-30: "Stop nudging" -"Stop installing updates and forcing restarts when your users are busy using their computers. Fleet finds time in the calendar for a reboot and uses AI to explain why." +FYI we just updated the self-service investor update portal with the numbers from last month: https://docs.google.com/spreadsheets/d/10T7Q9iuHA4vpfV7qZCm6oMd5U1bLftBSobYD0RR8RkM/edit#gid=0 Thanks for your support, Mike and the Fleet team + ``` 2. Address the email to the executive team's Gmail. -3. Using the [🌧️🦉 Investors + advisors](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1068113636) spreadsheet, collect all of the investor emails from previous funding rounds and add them to bcc of the email and send. +3. Using the [🌧️🦉 Investors + advisors](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1068113636) spreadsheet, bcc the correct individuals and send the email. ### Refresh event calendar From f0ec662996e0ea210d40575368d6e8e035bf37d3 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:36:10 -0400 Subject: [PATCH 002/119] [unreleased bug] Fleet UI: Only global admins see ABM and APNs banners (#19571) --- frontend/components/App/App.tsx | 49 +++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/frontend/components/App/App.tsx b/frontend/components/App/App.tsx index 75b4528c24..d2f4cd5595 100644 --- a/frontend/components/App/App.tsx +++ b/frontend/components/App/App.tsx @@ -56,6 +56,7 @@ const App = ({ children, location }: IAppProps): JSX.Element => { const { config, currentUser, + isGlobalAdmin, isGlobalObserver, isOnlyObserver, isAnyTeamMaintainerOrTeamAdmin, @@ -82,29 +83,37 @@ const App = ({ children, location }: IAppProps): JSX.Element => { setNoSandboxHosts(noSandboxHosts); } - if (configResponse.mdm.apple_bm_enabled_and_configured) { - // FIXME: we need to catch and check for a 400 status code because the - // API behaves this way when the token is already expired or invalid. - // - // This is a quick fix to not completely break the UI, but it doesn't - // allow us to show ABM information when the token is expired so it - // should be fixed upstream. - try { - const abmInfo = await mdmAppleBMAPI.getAppleBMInfo(); - setABMExpiry(abmInfo.renew_date); - } catch (error) { - console.error(error); - const abmError = error as AxiosResponse; - if (abmError.status === 400) { - const pastDate = "2024-06-03T17:28:44Z"; - setABMExpiry(pastDate); + // These endpoints 403 for non-global admins + if (isGlobalAdmin) { + if (configResponse.mdm.apple_bm_enabled_and_configured) { + // FIXME: we need to catch and check for a 400 status code because the + // API behaves this way when the token is already expired or invalid. + // + // This is a quick fix to not completely break the UI, but it doesn't + // allow us to show ABM information when the token is expired so it + // should be fixed upstream. + try { + const abmInfo = await mdmAppleBMAPI.getAppleBMInfo(); + setABMExpiry(abmInfo.renew_date); + } catch (error) { + console.error(error); + const abmError = error as AxiosResponse; + if (abmError.status === 400) { + const pastDate = "2024-06-03T17:28:44Z"; + setABMExpiry(pastDate); + } + } + } + if (configResponse.mdm.enabled_and_configured) { + try { + const apnsInfo = await mdmAppleAPI.getAppleAPNInfo(); + setAPNsExpiry(apnsInfo.renew_date); + } catch (error) { + console.error(error); } } } - if (configResponse.mdm.enabled_and_configured) { - const apnsInfo = await mdmAppleAPI.getAppleAPNInfo(); - setAPNsExpiry(apnsInfo.renew_date); - } + setConfig(configResponse); } catch (error) { console.error(error); From 0ea339c7e6544cb1dc42909389df20222460e9b4 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:52:06 -0700 Subject: [PATCH 003/119] Add macOS `tcc_access` table to `fleetd` (#19355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #18222 Table results: Screenshot 2024-05-29 at 6 15 21 PM Optimized querying of host `TCC.db`s as constrained by query `WHERE` clauses on `uid`: Screenshot 2024-06-03 at 6 20 50 PM Screenshot 2024-06-03 at 6 19 31 PM Screenshot 2024-06-03 at 6 15 01 PM Screenshot 2024-06-03 at 6 17 54 PM - [x] Changes file added for user-visible changes in `orbit/changes/`. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality on macOS (only supported OS) --------- Co-authored-by: Jacob Shandling Co-authored-by: Lucas Manuel Rodriguez --- orbit/changes/18222-add-tcc-table | 1 + orbit/pkg/table/extension_darwin.go | 2 + orbit/pkg/table/tcc_access/tcc_access.go | 209 ++++++++++++++++++ orbit/pkg/table/tcc_access/tcc_access_test.go | 77 +++++++ .../testdata/Users/testUser1/test-TCC.db | Bin 0 -> 65536 bytes .../testdata/Users/testUser2/test-TCC.db | Bin 0 -> 65536 bytes .../pkg/table/tcc_access/testdata/test-TCC.db | Bin 0 -> 65536 bytes tools/tuf/test/create_repository.sh | 4 +- 8 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 orbit/changes/18222-add-tcc-table create mode 100644 orbit/pkg/table/tcc_access/tcc_access.go create mode 100644 orbit/pkg/table/tcc_access/tcc_access_test.go create mode 100644 orbit/pkg/table/tcc_access/testdata/Users/testUser1/test-TCC.db create mode 100644 orbit/pkg/table/tcc_access/testdata/Users/testUser2/test-TCC.db create mode 100644 orbit/pkg/table/tcc_access/testdata/test-TCC.db diff --git a/orbit/changes/18222-add-tcc-table b/orbit/changes/18222-add-tcc-table new file mode 100644 index 0000000000..1178f6824a --- /dev/null +++ b/orbit/changes/18222-add-tcc-table @@ -0,0 +1 @@ +- Add `tcc_access` table to `fleetd` for macOS. diff --git a/orbit/pkg/table/extension_darwin.go b/orbit/pkg/table/extension_darwin.go index 7cdbd9250f..45a4f56253 100644 --- a/orbit/pkg/table/extension_darwin.go +++ b/orbit/pkg/table/extension_darwin.go @@ -23,6 +23,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/table/pwd_policy" "github.com/fleetdm/fleet/v4/orbit/pkg/table/software_update" "github.com/fleetdm/fleet/v4/orbit/pkg/table/sudo_info" + "github.com/fleetdm/fleet/v4/orbit/pkg/table/tcc_access" "github.com/fleetdm/fleet/v4/orbit/pkg/table/user_login_settings" "github.com/macadmins/osquery-extension/tables/filevaultusers" @@ -45,6 +46,7 @@ func PlatformTables(opts PluginOpts) []osquery.OsqueryPlugin { table.NewPlugin("pwd_policy", pwd_policy.Columns(), pwd_policy.Generate), table.NewPlugin("csrutil_info", csrutil_info.Columns(), csrutil_info.Generate), table.NewPlugin("nvram_info", nvram_info.Columns(), nvram_info.Generate), + table.NewPlugin("tcc_access", tcc_access.Columns(), tcc_access.Generate), table.NewPlugin("authdb", authdb.Columns(), authdb.Generate), table.NewPlugin("pmset", pmset.Columns(), pmset.Generate), table.NewPlugin("sudo_info", sudo_info.Columns(), sudo_info.Generate), diff --git a/orbit/pkg/table/tcc_access/tcc_access.go b/orbit/pkg/table/tcc_access/tcc_access.go new file mode 100644 index 0000000000..de0929aadd --- /dev/null +++ b/orbit/pkg/table/tcc_access/tcc_access.go @@ -0,0 +1,209 @@ +//go:build darwin +// +build darwin + +package tcc_access + +import ( + "bytes" + "context" + "errors" + "fmt" + "os/exec" + "strings" + + "github.com/osquery/osquery-go/plugin/table" +) + +var ( + tccPathPrefix = "" + tccPathSuffix = "/Library/Application Support/com.apple.TCC/TCC.db" + dbQuery = "SELECT service, client, client_type, auth_value, auth_reason, last_modified, policy_id, indirect_object_identifier, indirect_object_identifier_type FROM access;" + sqlite3Path = "/usr/bin/sqlite3" + dbColNames = []string{"service", "client", "client_type", "auth_value", "auth_reason", "last_modified", "policy_id", "indirect_object_identifier", "indirect_object_identifier_type"} +) + +// Columns is the schema of the table. +func Columns() []table.ColumnDefinition { + return []table.ColumnDefinition{ + // added here + table.TextColumn("source"), + table.IntegerColumn("uid"), + // derived from a TCC.db + table.TextColumn("service"), + table.TextColumn("client"), + table.IntegerColumn("client_type"), + table.IntegerColumn("auth_value"), + table.IntegerColumn("auth_reason"), + table.BigIntColumn("last_modified"), + table.IntegerColumn("policy_id"), + table.TextColumn("indirect_object_identifier"), + table.IntegerColumn("indirect_object_identifier_type"), + } +} + +// Generate is called to return the results for the table at query time. +// Constraints for generating can be retrieved from the queryContext. + +func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { + // get all usernames and uids on the mac + usersInfo, err := getUsersInfo() + if err != nil { + return nil, err + } + + var rows []map[string]string + uidConstraintList, ok := queryContext.Constraints["uid"] + + // build rows for every user-level TCC.db + for _, userInfo := range usersInfo { + username, uid := userInfo[0], userInfo[1] + satisfiesUidConstraints := true + + if ok { + // there are uid constraints + satisfiesUidConstraints, err = satisfiesConstraints(uid, uidConstraintList.Constraints) + if err != nil { + return nil, err + } + } + + if satisfiesUidConstraints { + tccPath := tccPathPrefix + "/Users/" + username + tccPathSuffix + uRs, err := getTCCAccessRows(uid, tccPath) + if err != nil { + return nil, err + } + rows = append(rows, uRs...) + } + } + + // and for the system-level TCC.db + sysSatisfiesUidConstraints := true + if ok { + // if there are uid constraints + sysSatisfiesUidConstraints, err = satisfiesConstraints("0", uidConstraintList.Constraints) + if err != nil { + return nil, err + } + } + if sysSatisfiesUidConstraints { + sRs, err := getTCCAccessRows("0", tccPathPrefix+tccPathSuffix) + if err != nil { + return nil, err + } + rows = append(rows, sRs...) + } + + return rows, nil +} + +func getTCCAccessRows(uid, tccPath string) ([]map[string]string, error) { + // querying direclty with sqlite3 avoids additional C compilation requirements that would be introduced by using + // https://github.com/mattn/go-sqlite3 + cmd := exec.Command(sqlite3Path, tccPath, dbQuery) + var dbOut bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &dbOut + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("Generate failed at `cmd.Run()`%s: %w", stderr.String(), err) + } + + parsedRows := parseTCCDbReadOutput(dbOut.Bytes()) + rows, err := buildTableRows(uid, parsedRows) + if err != nil { + return nil, err + } + return rows, nil +} + +func parseTCCDbReadOutput(dbOut []byte) [][]string { + // split by newLine for rows, then by "|" for columns + rawRows := strings.Split(string(dbOut[:]), "\n") + n := len(rawRows) + if n == 0 { + return nil + } + // the end of the db response is "\n", making the final row "", which we want to omit + rawRows = rawRows[:n-1] + + parsedRows := make([][]string, 0, len(rawRows)) + for _, rawRow := range rawRows { + parsedRows = append(parsedRows, strings.Split(rawRow, "|")) + } + return parsedRows +} + +func buildTableRows(uid string, parsedRows [][]string) ([]map[string]string, error) { + source := "system" + if uid != "0" { + source = "user" + } + + var rows []map[string]string + for _, parsedRow := range parsedRows { + row := make(map[string]string) + row["source"] = source + row["uid"] = uid + for i, rowColVal := range parsedRow { + row[dbColNames[i]] = rowColVal + } + rows = append(rows, row) + } + return rows, nil +} + +func satisfiesConstraints(uid string, constraints []table.Constraint) (bool, error) { + for _, constraint := range constraints { + // for each constraint on the column + switch constraint.Operator { + case table.OperatorEquals: + if constraint.Expression != uid { + return false, nil + } + case table.OperatorGreaterThan: + if constraint.Expression >= uid { + return false, nil + } + case table.OperatorLessThan: + if constraint.Expression <= uid { + return false, nil + } + case table.OperatorGreaterThanOrEquals: + if constraint.Expression > uid { + return false, nil + } + case table.OperatorLessThanOrEquals: + if constraint.Expression < uid { + return false, nil + } + default: + return false, errors.New("invalid comparison for column 'uid': supported comparisons are `=`, `<`, `>`, `<=`, and `>=`") + } + } + return true, nil +} + +func getUsersInfo() ([][]string, error) { + var parsedFilteredUsersInfo [][]string + + cmd := exec.Command("dscl", ".", "list", "/Users", "UniqueID") + out, err := cmd.Output() + if err != nil { + return nil, err + } + usersInfo := strings.Split(string(out[:]), "\n") + for _, userInfo := range usersInfo { + if len(userInfo) > 0 { + split := strings.Fields(userInfo) + uN := split[0] + // filter for relevant users + if !strings.HasPrefix(uN, "_") && uN != "nobody" && uN != "root" && uN != "daemon" && len(uN) > 0 { + parsedFilteredUsersInfo = append(parsedFilteredUsersInfo, split) + } + } + } + + return parsedFilteredUsersInfo, nil +} diff --git a/orbit/pkg/table/tcc_access/tcc_access_test.go b/orbit/pkg/table/tcc_access/tcc_access_test.go new file mode 100644 index 0000000000..adc2b98025 --- /dev/null +++ b/orbit/pkg/table/tcc_access/tcc_access_test.go @@ -0,0 +1,77 @@ +//go:build darwin +// +build darwin + +package tcc_access + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/osquery/osquery-go/plugin/table" + "github.com/stretchr/testify/require" +) + +// TestGenerate tests the tcc_access table generation. +func TestGenerate(t *testing.T) { + tccPathPrefix = "./testdata" + tccPathSuffix = "/test-TCC.db" + + overrideCommand(t, "dscl", "testUser1 1 \ntestUser2 2\n") + + rows, err := Generate(context.Background(), table.QueryContext{}) + require.NoError(t, err) + + require.Len(t, rows, 93) + + // Check "uid" of the returned rows match the entries in the TCC files. + for _, row := range rows { + if strings.HasPrefix(row["service"], "test-sys-service-") { + require.Equal(t, "0", row["uid"]) + } else if strings.HasPrefix(row["service"], "test-u1-service-") { + require.Equal(t, "1", row["uid"]) + } else if strings.HasPrefix(row["service"], "test-u2-service-") { + require.Equal(t, "2", row["uid"]) + } + } + + rows, err = Generate(context.Background(), table.QueryContext{ + Constraints: map[string]table.ConstraintList{ + "uid": { + Affinity: table.ColumnTypeText, + Constraints: []table.Constraint{ + { + Operator: table.OperatorEquals, + Expression: "1", + }, + }, + }, + }, + }) + require.NoError(t, err) + require.Len(t, rows, 31) + for _, row := range rows { + serviceName := row["service"] + require.Contains(t, serviceName, "u1-service") + require.NotContains(t, serviceName, "u2-service") + require.NotContains(t, serviceName, "sys-service") + } +} + +// overrideCommand allows us to override a system command (just during the execution +// of the test) by a script that prints the given output. +func overrideCommand(t *testing.T, cmdName string, output string) { + tmpDir := t.TempDir() + pathValue := os.Getenv("PATH") + os.Setenv("PATH", tmpDir+":"+os.ExpandEnv("$PATH")) + t.Cleanup(func() { + os.Setenv("PATH", pathValue) + }) + cmdPath := filepath.Join(tmpDir, cmdName) + scriptContent := []byte(fmt.Sprintf("#!/bin/sh\nprintf '%%s' \"%s\"", output)) + err := os.WriteFile(cmdPath, scriptContent, 0o744) //nolint:gosec + require.NoError(t, err) +} diff --git a/orbit/pkg/table/tcc_access/testdata/Users/testUser1/test-TCC.db b/orbit/pkg/table/tcc_access/testdata/Users/testUser1/test-TCC.db new file mode 100644 index 0000000000000000000000000000000000000000..3b86ec417a8fb87846e1577acc78914660c0ace4 GIT binary patch literal 65536 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 G$njsjdcJi4 literal 0 HcmV?d00001 diff --git a/orbit/pkg/table/tcc_access/testdata/test-TCC.db b/orbit/pkg/table/tcc_access/testdata/test-TCC.db new file mode 100644 index 0000000000000000000000000000000000000000..9683d5aa6ad02b189fc398fa8f6a276d4eebed7d GIT binary patch literal 65536 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 uWOW!O7 literal 0 HcmV?d00001 diff --git a/tools/tuf/test/create_repository.sh b/tools/tuf/test/create_repository.sh index 203eb791bc..2fabf7aa10 100755 --- a/tools/tuf/test/create_repository.sh +++ b/tools/tuf/test/create_repository.sh @@ -106,7 +106,9 @@ for system in $SYSTEMS; do --platform macos \ --name desktop \ --version 42.0.0 -t 42.0 -t 42 -t stable - rm desktop.app.tar.gz + if [[ -z "$MACOS_USE_PREBUILT_DESKTOP_APP_TAR_GZ" ]]; then + rm desktop.app.tar.gz + fi fi # Add Nudge application on macos (if enabled). From 9b263de850c854d908dd18f33bd06ec22600d3ca Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:02:25 -0400 Subject: [PATCH 004/119] Update etc_hosts.yml (#19532) made file paths bullets instead of 1 line. --------- Co-authored-by: Eric --- schema/tables/etc_hosts.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/schema/tables/etc_hosts.yml b/schema/tables/etc_hosts.yml index d04c712705..0808897836 100644 --- a/schema/tables/etc_hosts.yml +++ b/schema/tables/etc_hosts.yml @@ -9,11 +9,12 @@ examples: |- notes: |- The `hosts` file is customized by many organizations. As part of a defense-in-depth security posture it's important to track `hosts` modifications. Endpoints with a modified `hosts` configuration connected to enterprise networks can potentially bypass network rules, proxies and firewalls or be routed to malicious sites. - File paths: - Linux: /etc/hosts - macOS: /private/etc/hosts - Windows: C:\Windows\system32\drivers\etc + File paths to `hosts`: + - Linux: /etc/hosts + - macOS: /private/etc/hosts + - Windows: C:\Windows\system32\drivers\etc + **More info**: - [DNS](https://en.wikipedia.org/wiki/Domain_Name_System) - The `/etc/hosts` [Guide For Linux](https://thelinuxcode.com/etc-hosts-file-complete-guide-for-linux/) - [How to edit the hosts file on Windows](https://www.howtogeek.com/784196/how-to-edit-the-hosts-file-on-windows-10-or-11) @@ -21,3 +22,4 @@ columns: - name: pid_with_namespace platforms: - linux + From 2ab64f4e9a4b1acc5a1dcd7fe58ae5f3ceb39356 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Thu, 6 Jun 2024 16:23:57 -0700 Subject: [PATCH 005/119] Add demo desktop background (#19579) --- .../images/demo/fleet-desktop-migration.png | Bin 0 -> 224388 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/assets/images/demo/fleet-desktop-migration.png diff --git a/website/assets/images/demo/fleet-desktop-migration.png b/website/assets/images/demo/fleet-desktop-migration.png new file mode 100644 index 0000000000000000000000000000000000000000..0601570d27e73093ee664e69fe34b0a36010fc5e GIT binary patch literal 224388 zcmeFad03Oz);1o8iY+Sbse&kiQ<((?lpze&77>9^8AT9KA(6o_&ydhk^b`ctFvt*M zWr&PX0){y#1!PDNVwl4q1Pnu%!w{1Ep7cFEB!2Jd@2~H=-s}DHT$g0?MD||$-fORQ zueH~c{Pq*XWdB!3z5;Xaxct1)ARk?%e~tOkJKj3%q>kd&MCD1d{xY|8Iw> z)#(M`=8ga>lS`mdvJ33RL9f$wJp8>ab z@8r+_KU{lvyZ(oFfZJd1ZTQb`9{)Oe$A5hH`{SaCf_E#v`6_Uh>Q^* z1(DU|qaboGd=&m~_)$K|c&Q+A!6FyT*D&A`xnMpDA{Wd@LF9t@D2QAz9|e&M=A-a` z!v*vE6k^M?qwD`6!62E*}Mv)#an`zt8Gc&eF~SInjNM&o=B0#7PpD`P2Mw zuK#^A|8FMAza^R|9|ch|oR5Mi8O}#R#0mdILBt99Fo-xI9|aL7Dl5nz==>=-u~s6}^Z5uA=ws z-&ORw{#`|H!{1i)Ui`b#<(R)KT^3E0zv`kD<)a`{T|Nq;J<3NxWQ_PIh^#Ijg=mp` z;lm*EqkI%Zgn^HOh%oR`5D^AG3L?V5M?pjw_$Y`710MwuVc?@6A`E|`AR-KW7(|4D zkAjFW@KF#E20jY^(+R_I7V{|}=KWjmw~Df3C&fzq|tC&nf=b4-p0A zBMEEqoM2gn^HOh%oR`5D^AG3L?V5M?pjw z_$Y`7!=EUK2m>Dm5n{A>y6w@b}79f!~$dCbidk_YP9*<_XC9gk81hoL_bE4zI|gVfRkfr9O8*$k=soH% zFmxqkGf0oMV_R^3OU_9!B>Ub=42524i&=?_@lJwcU1Z&1kqVrTzOta1Lu{qYlkPY3 zvt>spbE(OMPu49mLdBM+RzI3`tet_ChGZ_ALxna_;L3-$p1H&$O-(JXl}S@7l;_-LJ%v_FK_9=|bY7o%pc(?ZSV`7j zs;D1IhiHrCcp~bxu9L}=OZs22e#fg&dWo7C2O00Y!B(qY*>%$b1r#H>;6dYu@sm{7 zN4H_>bl0b?=((`H-WS}7n0!?%&oJZWkNG1yG_fGg8t)E%ZMnN!pJ`jF)_La^Eulj7 zUNMaRNzrVEvYgjg?5vMr4tQ3>$mX3YjMCD4O-?q1aC93kerw&7p=CkYy?t}^-b#Cp zj0MYo^;2e557Mw^931v)&0IDclB#DG7CG1I=9Xd^R366Ybj-jP~wyNp}7ex88>-q%5JHPvDN+UQC)m>a)6iF zn8KaNkO)Igk$T>cQOBp%qXjJ}E#veD?(~lvsu)_wBo?XWWrQV}C80L2-lfss}DfM&s(~N5JIup_sle5n` z@_f_S*h;W^Q&*S%rtRvcN1( z z5H!xSdwpL7t#nY$;WW}4`})Ua$SkN~h=8&}LXpM=rgh z>X*Gv4wzvza9${&Y+7Q>Uk7VMZrlL$*!ylXfX_yo$SEaP$K#rNfZ^)wb+eH#(0Y418pWPl&Y~6J50k>E4_`!++9ogvGJdBzh|e zL!LKS#4SH&gxz77-^Ci0mKi86yVTsZ zQ*v&rv&ZLbEK=lyBwKHU7y!%ebUi|!7`#YWUT-#8ZkMY6dKirxIWo~acG^CF%3-|- zo?%|tw0V9R4jV?aJ?Jc*_yLx-%rHB;@jz~{+(BX&VCa`u$&T2*I$O|2H~G;JF&eybs7g3_^l%l9kV;o6Lu7IpeYqtDdp zxRh0ou_U!pmV~vJ<-5)m?36+;rma84Bp`UBYawSKuX+4fVm?MsegLPAeRKz?o#~ zb*kjdy@}T`{mT+PCOM%pOgVMS9=$l}~E!iD|nAxm%qn+mc z$^A!YStGJGoUP5gkwYN%j`1>bfMZTA;%qDPS%>L))oJrWd3*T&rFWj%nA*xzYF<`f z)JV@d*{)fe=@!U=LJIRsDGwE-og+>)ac_~4W4#my=eF+8z8`bcU`ESYU@0NRVe3V_ zOWb7@9^X{U*wR}2)kZ^DFaeOvnvER z_~?)988FWva|FDo<4yw*zj2%OThXM49!Vx49>KKxrb!k3fp{c zGR_E)wQt4L1tz;}e1aPK`JKUbEt=unSEg52Fj0mUQ-L@4Pk!^Vu*0`{56a7DqN5Y3 zOsl8}tQMRzAV~v9@c1tJ+v&+sYGlBCdl+}&>V4{B!9yg;0<%8sreYslpS)a@wzQ~e zl;(n%)eg83xjK1V{a(^io)_mKE#(!!yQ9HZ9s&Mj{>E=x+-nuFGI1nAl5AJWNnvk8QNI}4uo z-HqHeTRrS97++pJ8?wh|Vs$)CqrjvfLqoygrh+kTqJrZGU&-$z>NG9lv{NXnpDd

q|wt*B#q8M%E+k)Nk(H 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

R=u$1y(>xIK_Yee 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 Date: Fri, 7 Jun 2024 01:15:49 -0500 Subject: [PATCH 006/119] Update sales.rituals.yml (#19380) Added daily standup and weekly Opportunity pipeline review # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [ ] Added/updated tests - [ ] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [ ] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [ ] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [ ] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [ ] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). --------- Co-authored-by: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> --- handbook/sales/sales.rituals.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/handbook/sales/sales.rituals.yml b/handbook/sales/sales.rituals.yml index aa01598dfb..639c896715 100644 --- a/handbook/sales/sales.rituals.yml +++ b/handbook/sales/sales.rituals.yml @@ -2,7 +2,7 @@ -- + - task: "Prioritize for next sprint" # Title that will actually show in rituals table startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday frequency: "Triweekly" # must be supported by @@ -12,7 +12,27 @@ autoIssue: # Enables automation of GitHub issues labels: [ "#g-sales" ] # label to be applied to issue repo: "confidential" -- + - + task: "g-sales standup" # Title that will actually show in rituals table + startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday + frequency: "Daily" # must be supported by + description: "Review progress on priorities for Sprint. Discuss previous day accomplishments, goals for today and any blockers." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" + moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table + dri: "alexmitchelliii" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) + autoIssue: # Enables automation of GitHub issues + labels: [ "#g-sales" ] # label to be applied to issue + repo: "confidential" + - + task: "Opportunity pipeline review" # Title that will actually show in rituals table + startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday + frequency: "Weekly" # must be supported by + description: "Review status of sales opportunities and discuss next steps." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" + moreInfoUrl: "https://fleetdm.com/handbook/customers#review-rep-activity" #URL used to highlight "description:" test in table + dri: "alexmitchelliii" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) + autoIssue: # Enables automation of GitHub issues + labels: [ "#g-sales" ] # label to be applied to issue + repo: "confidential" + - task: "Review rep activity" startedOn: "2023-09-18" frequency: "Monthly" From 4b3818468f5967733fa48d4df6e26e25ab260cbc Mon Sep 17 00:00:00 2001 From: Erik Gomez Date: Fri, 7 Jun 2024 11:09:57 -0500 Subject: [PATCH 007/119] add optional cookie for the API interactions fleetApiOptionalCookie (#19573) --- ee/vulnerability-dashboard/docker-compose.yml | 1 + .../scripts/replace-placeholder-host-values.js | 5 ++++- .../scripts/update-critical-software.js | 4 +++- ee/vulnerability-dashboard/scripts/update-reports.js | 3 +++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ee/vulnerability-dashboard/docker-compose.yml b/ee/vulnerability-dashboard/docker-compose.yml index a1c92cdaed..054743ea65 100644 --- a/ee/vulnerability-dashboard/docker-compose.yml +++ b/ee/vulnerability-dashboard/docker-compose.yml @@ -14,6 +14,7 @@ services: sails_session__url: redis://redis:6379 sails_custom__fleetBaseUrl: '' #Add the base url of your Fleet instance: ex: https://fleet.example.com sails_custom__fleetApiToken: '' # Add the API token of an API-only user [?] Here's how you get one: https://fleetdm.com/docs/using-fleet/fleetctl-cli#get-the-api-token-of-an-api-only-user + sails_custom__fleetApiOptionalCookie: '' # If your fleet instance requires optional cookies, use this to interact with the APIs redis: image: "redis:alpine" diff --git a/ee/vulnerability-dashboard/scripts/replace-placeholder-host-values.js b/ee/vulnerability-dashboard/scripts/replace-placeholder-host-values.js index 20ecc42368..89a2ff093b 100644 --- a/ee/vulnerability-dashboard/scripts/replace-placeholder-host-values.js +++ b/ee/vulnerability-dashboard/scripts/replace-placeholder-host-values.js @@ -27,6 +27,10 @@ module.exports = { Authorization: `Bearer ${sails.config.custom.fleetApiToken}` }; + if (sails.config.custom.fleetApiOptionalCookie) { + headers['Cookie'] = sails.config.custom.fleetApiOptionalCookie; + } + let page = 0; let HOSTS_PAGE_SIZE = 100; @@ -85,4 +89,3 @@ module.exports = { }; - diff --git a/ee/vulnerability-dashboard/scripts/update-critical-software.js b/ee/vulnerability-dashboard/scripts/update-critical-software.js index efda7e2ee4..29c0cc462b 100644 --- a/ee/vulnerability-dashboard/scripts/update-critical-software.js +++ b/ee/vulnerability-dashboard/scripts/update-critical-software.js @@ -28,6 +28,9 @@ module.exports = { let headers = { Authorization: `Bearer ${sails.config.custom.fleetApiToken}` }; + if (sails.config.custom.fleetApiOptionalCookie) { + headers['Cookie'] = sails.config.custom.fleetApiOptionalCookie; + } sails.log('Running custom shell script... (`sails run update-critical-software`)'); @@ -354,4 +357,3 @@ module.exports = { }; - diff --git a/ee/vulnerability-dashboard/scripts/update-reports.js b/ee/vulnerability-dashboard/scripts/update-reports.js index 6dc74297f2..8190194d44 100644 --- a/ee/vulnerability-dashboard/scripts/update-reports.js +++ b/ee/vulnerability-dashboard/scripts/update-reports.js @@ -28,6 +28,9 @@ module.exports = { let headers = { Authorization: `Bearer ${sails.config.custom.fleetApiToken}` }; + if (sails.config.custom.fleetApiOptionalCookie) { + headers['Cookie'] = sails.config.custom.fleetApiOptionalCookie; + } // Keep track of the latest vulnerabilities, hosts, and software seen in the Fleet scan. // We'll use these later to check if any records have gone missing. From bdfcf646b75dffb72ccc985231adfe5e4e837598 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 7 Jun 2024 11:18:34 -0500 Subject: [PATCH 008/119] Vulnerability dashboard: batch `Host` record creation (#19595) Changes: - Updated the `update-reports` script to create new host records in batches. --- ee/vulnerability-dashboard/scripts/update-reports.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ee/vulnerability-dashboard/scripts/update-reports.js b/ee/vulnerability-dashboard/scripts/update-reports.js index 8190194d44..9212890537 100644 --- a/ee/vulnerability-dashboard/scripts/update-reports.js +++ b/ee/vulnerability-dashboard/scripts/update-reports.js @@ -386,7 +386,10 @@ module.exports = { sails.log.warn(`Dry run: ${hostRecordsToUpdate.length} hosts will be updated with new information. (Fleet returned them in the API.)`); } else { sails.log(`Creating ${newRecordsForUnrecognizedHosts.length} host records… `); - await Host.createEach(newRecordsForUnrecognizedHosts); + let batchedNewRecordsForUnrecognizedHosts = _.chunk(newRecordsForUnrecognizedHosts, 500); + for(let batch of batchedNewRecordsForUnrecognizedHosts){ + await Host.createEach(batch); + } for(let hostUpdate of hostRecordsToUpdate){ await Host.updateOne({id: hostUpdate.id}).set(_.omit(hostUpdate, 'id')); } @@ -696,7 +699,10 @@ module.exports = { totalNumberOfHostRecordsCreated += newRecordsForUnrecognizedHosts.length; totalNumberOfHostRecordsUpdated += hostRecordsToUpdate.length; sails.log.verbose(`Creating ${newRecordsForUnrecognizedHosts.length} new host records…`); - await Host.createEach(newRecordsForUnrecognizedHosts); + let batchedNewRecordsForUnrecognizedHosts = _.chunk(newRecordsForUnrecognizedHosts, 500); + for(let batch of batchedNewRecordsForUnrecognizedHosts){ + await Host.createEach(batch); + } sails.log.verbose(`Updating ${hostRecordsToUpdate.length} host records…`); for(let hostUpdate of hostRecordsToUpdate){ await Host.updateOne({id: hostUpdate.id}).set(_.omit(hostUpdate, 'id')); From 225fe666d246e1e2a0ab938c21564d7364258c56 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Fri, 7 Jun 2024 11:04:45 -0700 Subject: [PATCH 009/119] Updating release minor version steps (#19248) --- tools/release/README.md | 24 +++++++++++++++++++++--- tools/release/publish_release.sh | 24 ++++++++++++++++-------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/tools/release/README.md b/tools/release/README.md index 0fe38b990a..f753d8a6f9 100644 --- a/tools/release/README.md +++ b/tools/release/README.md @@ -34,17 +34,35 @@ example ./tools/release/publish_release.sh -a # Do QA until ready to release -# QA is passed on all teams and ready for release +# - QA is passed on all teams and ready for release +# - Merge changelog and versions update PR into main +# - git pull main locally with the changelog as the latest commit # Tag main ./tools/release/publish_release.sh -ag + +# - Wait for build to run + # Publish main ./tools/release/publish_release.sh -auq -# Go update osquery-slack version + +# - Wait for publish process to complete. +# - Merge release article and wait for website to build. +# - When the release article is published, create a LinkedIn post on Fleet's company page. +# - Copy te LinkedIn post URL as the value for the linkedin_post_url variable in the general_announce_info() function. +# - Go update osquery-slack version + +# Announce release +# Change $current_version to the current version that was just released +# For example, ./tools/release/publish_release.sh -anu -v 4.50.0 +./tools/release/publish_release.sh -anu -v {current_version} ``` ... -TODO example output +:cloud: :rocket: The latest version of Fleet is 4.50.0. +More info: https://github.com/fleetdm/fleet/releases/tag/fleet-v4.50.0 +Release article: https://fleetdm.com/releases/fleet-4.50.0 +LinkedIn post: https://www.linkedin.com/feed/update/urn:li:activity:7199509896705232898/ ... diff --git a/tools/release/publish_release.sh b/tools/release/publish_release.sh index 1461ed4cc4..330f3ed8f9 100755 --- a/tools/release/publish_release.sh +++ b/tools/release/publish_release.sh @@ -674,24 +674,32 @@ fi next_tag="fleet-$next_ver" if [[ "$target_milestone_number" == "" ]]; then - echo "Missing milestone $target_milestone, Please create one and tie tickets to the milestone to continue" - exit 1 + if [ "$announce_only" = "false" ]; then + echo "Missing milestone $target_milestone, Please create one and tie tickets to the milestone to continue" + exit 1 + fi fi echo "Found milestone $target_milestone with number $target_milestone_number" if [ "$print_info" = "true" ]; then - print_announce_info - exit 0 + if [ "$announce_only" = "false" ]; then + print_announce_info + exit 0 + fi fi if [ "$do_tag" = "true" ]; then - tag - exit 0 + if [ "$announce_only" = "false" ]; then + tag + exit 0 + fi fi if [ "$release_notes" = "true" ]; then - update_release_notes - exit 0 + if [ "$announce_only" = "false" ]; then + update_release_notes + exit 0 + fi fi if [ "$publish_release" = "true" ]; then From 2edc88596f733bd42b6f47735f048f9716741b0d Mon Sep 17 00:00:00 2001 From: Nathanael Holliday <100959072+hollidayn@users.noreply.github.com> Date: Fri, 7 Jun 2024 14:41:35 -0500 Subject: [PATCH 010/119] Update README.md (#19503) Changed to reflect company policy of screening all employees. --- handbook/business-operations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handbook/business-operations/README.md b/handbook/business-operations/README.md index 597c10f149..0590539e1d 100644 --- a/handbook/business-operations/README.md +++ b/handbook/business-operations/README.md @@ -37,7 +37,7 @@ Recurring monthly or annual expenses, such as the tools we use throughout Fleet, ### Access a background check -Fleet team members with access to Fleet's infrastructure undergo a background check provided through [Vetty](https://vetty.co/). Only the most recent background checks appear on the home page of Vetty's dashboard. To access a complete list of background checks run in Vetty, scroll down to the bottom of the candidates page and click "View Historical". +All Fleet team members undergo a background check provided through [Vetty](https://vetty.co/). Only the most recent background checks appear on the home page of Vetty's dashboard. To access a complete list of background checks run in Vetty, scroll down to the bottom of the candidates page and click "View Historical". ### Process an email from a state agency From 3a4a2904d21e5cf00f14f5418dca5d4c9318afd9 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Fri, 7 Jun 2024 13:10:24 -0700 Subject: [PATCH 011/119] Update canary profiles and policies (#19598) --- ...-disable-update-notifications.mobileconfig | 84 +++++++++++++ .../macos-device-health-canary.policies.yml | 80 +++++++++++++ it-and-security/lib/macos-mdm-migration.sh | 113 ++++++++++++++++++ it-and-security/teams/workstations-canary.yml | 4 +- 4 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 it-and-security/lib/configuration-profiles/macos-disable-update-notifications.mobileconfig create mode 100644 it-and-security/lib/macos-device-health-canary.policies.yml create mode 100755 it-and-security/lib/macos-mdm-migration.sh diff --git a/it-and-security/lib/configuration-profiles/macos-disable-update-notifications.mobileconfig b/it-and-security/lib/configuration-profiles/macos-disable-update-notifications.mobileconfig new file mode 100644 index 0000000000..cfda6f3d03 --- /dev/null +++ b/it-and-security/lib/configuration-profiles/macos-disable-update-notifications.mobileconfig @@ -0,0 +1,84 @@ + + + + + PayloadUUID + CDB0BC64-F3EB-4B1A-AA5E-9A5D994CA592 + PayloadType + Configuration + PayloadOrganization + Macjutsu + PayloadIdentifier + CDB0BC64-F3EB-4B1A-AA5E-9A5D994CA592 + PayloadDisplayName + Apple Software Update Disable Notifications + PayloadDescription + These settings disable the Apple software update notifications and banners. + PayloadVersion + 1 + PayloadEnabled + + PayloadRemovalDisallowed + + PayloadScope + System + PayloadContent + + + PayloadDisplayName + Notifications Payload + PayloadIdentifier + 84DB38D0-8A4B-4382-B1D2-11235122FF6D + PayloadOrganization + Macjutsu + PayloadType + com.apple.notificationsettings + PayloadUUID + 84DB38D0-8A4B-4382-B1D2-11235122FF6D + PayloadVersion + 1 + NotificationSettings + + + BundleIdentifier + _system_center_:com.apple.softwareupdatenotification + CriticalAlertEnabled + + NotificationsEnabled + + + + + + PayloadDisplayName + Custom Settings + PayloadIdentifier + 87E2F5E4-1C8A-4D43-AA52-676364A3326E + PayloadOrganization + Macjutsu + PayloadType + com.apple.ManagedClient.preferences + PayloadUUID + 87E2F5E4-1C8A-4D43-AA52-676364A3326E + PayloadVersion + 1 + PayloadContent + + com.apple.systempreferences + + Forced + + + mcx_preference_settings + + AttentionPrefBundleIDs + 0 + + + + + + + + + diff --git a/it-and-security/lib/macos-device-health-canary.policies.yml b/it-and-security/lib/macos-device-health-canary.policies.yml new file mode 100644 index 0000000000..134680169b --- /dev/null +++ b/it-and-security/lib/macos-device-health-canary.policies.yml @@ -0,0 +1,80 @@ +- name: macOS - Enable FileVault + query: SELECT 1 FROM filevault_status WHERE status = 'FileVault is On.'; + critical: false + description: This policy checks if FileVault (disk encryption) is enabled. + resolution: As an IT admin, turn on disk encryption in Fleet. + platform: darwin +- name: macOS - Enable Firewall + query: SELECT 1 FROM managed_policies WHERE domain='com.apple.security.firewall' AND username = '' AND name='EnableFirewall' AND CAST(value AS INT) = 1; + critical: false + description: This policy checks if Firewall is enabled. + resolution: An an IT admin, deploy a macOS, Firewall profile with the EnableFirewall option set to true. + platform: darwin +- name: macOS - Disable guest account + query: SELECT 1 FROM plist WHERE path='/Library/Preferences/com.apple.loginwindow.plist' AND key='GuestEnabled' AND value = 0; + critical: false + description: This policy checks if the guest account is disabled. + resolution: An an IT admin, deploy a macOS, login window profile with the DisableGuestAccount option set to true. + platform: darwin +- name: macOS - Require 10 character password + query: SELECT 1 WHERE + EXISTS ( + SELECT 1 FROM managed_policies WHERE + domain='com.apple.screensaver' AND + name='askForPassword' AND + CAST(value AS INT) + ) + AND EXISTS ( + SELECT 1 FROM managed_policies WHERE + domain='com.apple.screensaver' AND + name='minLength' AND + CAST(value AS INT) <= 10 + ); + critical: false + description: This policy checks if the end user is required to enter a password, with at least 10 characters, to unlock the host. + resolution: An an IT admin, deploy a macOS, screensaver profile with the askForPassword option set to true and minLength option set to 10. + platform: darwin +- name: macOS - Enable screen saver after 20 minutes + query: SELECT 1 WHERE + EXISTS ( + SELECT 1 FROM managed_policies WHERE + domain='com.apple.screensaver' AND + name='idleTime' AND + CAST(value AS INT) <= 1200 AND + username = '' + ) + AND NOT EXISTS ( + SELECT 1 FROM managed_policies WHERE + domain='com.apple.screensaver' AND + name='idleTime' AND + CAST(value AS INT) > 1200 + ); + critical: false + description: This policy checks if maximum amount of time (in minutes) the device is allowed to sit idle before the screen is locked. End users can select any value less than the specified maximum. + resolution: An an IT admin, deploy a macOS, screen saver profile with the maxInactivity option set to 20 minutes. + platform: darwin +- name: macOS - No 1Password emergency kit stored in desktop, documents, or downloads folders + query: SELECT 1 WHERE + NOT EXISTS ( + SELECT 1 FROM file WHERE + filename LIKE '%Emergency Kit%.pdf' AND + (path LIKE '/Users/%/Desktop/%' OR path LIKE '/Users/%/Documents/%' OR path LIKE '/Users/%/Downloads/%' OR path LIKE '/Users/Shared/%') + ); + critical: false + description: Looks for PDF files with file names typically used by 1Password for emergency recovery kits. To protect the performance of your devices, the search is one level deep and limited to the Desktop, Documents, Downloads, and Shared folders. + resolution: Delete 1Password emergency kits from your computer, and empty the trash. 1Password emergency kits should only be printed and stored in a physically secure location. + platform: darwin +- name: macOS - Check if latest version + query: SELECT 1 FROM os_version WHERE major = "14" AND minor = "5" AND patch >= "1"; + critical: false + description: Using an outdated macOS version risks exposure to security vulnerabilities and potential system instability. + resolution: We will update your macOS to the latest version. + platform: darwin + calendar_events_enabled: false +- name: macOS - MDM migration complete + query: SELECT 1 AS result FROM system_info WHERE local_hostname != 'Titanosauria'; + critical: false + description: Determines if the device has completed MDM migration to Fleet. + resolution: We will migrate your macOS MDM to Fleet. + platform: darwin + calendar_events_enabled: true diff --git a/it-and-security/lib/macos-mdm-migration.sh b/it-and-security/lib/macos-mdm-migration.sh new file mode 100755 index 0000000000..0954f562c9 --- /dev/null +++ b/it-and-security/lib/macos-mdm-migration.sh @@ -0,0 +1,113 @@ +#!/bin/zsh + +# Function to start System Events if it isn't running +start_system_events() { + osascript -e ' + tell application "System Events" + if not running then + launch + delay 2 + end if + end tell' +} + +# Define the URL of the new wallpaper +new_wallpaper_url="https://fleetdm.com/images/demo/fleet-desktop-migration.png" + +# Define the path where the new wallpaper will be saved +new_wallpaper_path="/tmp/fleet-desktop-migration.png" + +# Download the new wallpaper +curl -o "$new_wallpaper_path" "$new_wallpaper_url" + +# Check if the download was successful +if [[ ! -f "$new_wallpaper_path" ]] || [[ ! -s "$new_wallpaper_path" ]]; then + echo "Failed to download the new wallpaper." + exit 1 +fi + +# Start System Events if it isn't running +start_system_events + +# Get the current wallpaper +current_wallpaper=$(osascript -e ' +tell application "System Events" + set currentDesktop to a reference to current desktop + set desktopPicture to picture of currentDesktop + try + return POSIX path of desktopPicture + on error + return desktopPicture + end try +end tell +') + +# Check if the current wallpaper path is valid +if [[ -z "$current_wallpaper" ]]; then + echo "Failed to get the current wallpaper path." + exit 1 +fi + +echo "Current wallpaper: $current_wallpaper" +echo "Fleet wallpaper: $new_wallpaper_path" + +# Function to change wallpaper using Finder +change_wallpaper() { + local wallpaper_path=$1 + osascript -e " + tell application \"Finder\" + set desktop picture to POSIX file \"$wallpaper_path\" + end tell" +} + +# Function to check the result of the previous command +check_result() { + if [[ $? -ne 0 ]]; then + echo "Failed to change to the new wallpaper." + exit 1 + fi +} + +# Set the new wallpaper +change_wallpaper "$new_wallpaper_path" +check_result + +# Wait for 5 seconds +sleep 5 + +# Open Chrome with a specific URL and maximize the window and set full screen and unmute +chrome_url="https://www.loom.com/share/e5f733b92773476690b8d4f38592b35d?t=254&sid=f993c904-bf49-40e4-b55c-26c81a91c60&autoplay=1" +osascript -e " +tell application \"Google Chrome\" + activate + open location \"$chrome_url\" + delay 2 + tell application \"System Events\" + tell process \"Google Chrome\" + set frontmost to true + perform action \"AXRaise\" of window 1 + end tell + end tell +end tell" + +# Wait for 1 second and press the "F" key +sleep 1 +osascript -e ' +tell application "System Events" + keystroke "F" +end tell' + +# Wait for 1 more second and press the "M" key +sleep 1 +osascript -e ' +tell application "System Events" + keystroke "M" +end tell' +# Wait for 30 seconds +sleep 30 + +# Revert to the original wallpaper +change_wallpaper "$current_wallpaper" +check_result + +echo "Wallpaper changed to $new_wallpaper_path for 30 seconds, then reverted back to $current_wallpaper" diff --git a/it-and-security/teams/workstations-canary.yml b/it-and-security/teams/workstations-canary.yml index cdff5d7225..5d1c8b7985 100644 --- a/it-and-security/teams/workstations-canary.yml +++ b/it-and-security/teams/workstations-canary.yml @@ -86,6 +86,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-disable-update-notifications.mobileconfig - path: ../lib/configuration-profiles/passcode-settings-ddm.json macos_setup: bootstrap_package: "" @@ -103,12 +104,13 @@ controls: - path: ../lib/collect-fleetd-logs.sh - path: ../lib/macos-see-automatic-enrollment-profile.sh - path: ../lib/macos-remove-old-nudge.sh + - path: ../lib/macos-mdm-migration.sh - path: ../lib/windows-remove-fleetd.ps1 - path: ../lib/windows-turn-off-mdm.ps1 - path: ../lib/windows-install-bitdefender.ps1 - path: ../lib/windows-enable-ms-defender.ps1 policies: - - path: ../lib/macos-device-health.policies.yml + - path: ../lib/macos-device-health-canary.policies.yml - path: ../lib/windows-device-health.policies.yml - path: ../lib/linux-device-health.policies.yml queries: From 8334a94d109614c879511f8f605a330bdf749cdc Mon Sep 17 00:00:00 2001 From: Isabell Reedy <113355639+ireedy@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:29:52 -0400 Subject: [PATCH 012/119] Process for recognizing teammates on their workiversary (#19567) Got approval from @JoStableford live during our biz ops github time. .. --------- Co-authored-by: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> --- handbook/business-operations/README.md | 31 +++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/handbook/business-operations/README.md b/handbook/business-operations/README.md index 0590539e1d..029bf9090f 100644 --- a/handbook/business-operations/README.md +++ b/handbook/business-operations/README.md @@ -252,7 +252,36 @@ When BizOps receives notification of a Fleetie's manager changing, follow these - For a team member moving from "classified" to "confidential" access, check Gusto, Plane, and other systems to remove their access. > **Note:** The Fleeties spreadsheet is the source of truth for who everyone's manager is and their job titles. - + +### Recognize employee workiversaries + +At Fleet, everyone is recognized on their [workiversary](https://fleetdm.com/handbook/company/communications#workiversaries). To ensure this happens, take the following steps: + +1. Bimonthly, use [Fleeties (private google doc)](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0) to determine who is celebrating their workiversary in the following two months. +2. Post in the #help-classifed Slack channel and cc the Head of Business Operations. Use the following template: + + + ``` + [Month] + [workiversary date (DD-MMM)] - [teammate name] - [number of years at Fleet] + ``` + + + The Apprentice to the CEO will also use this post to update the [All hands](https://fleetdm.com/handbook/company/communications#all-hands) deck. +3. On the day prior to a workiversary, send the teammate’s manager a DM on Slack: + + + ``` + Hey! Just a heads up, tomorrow is [teammate’s name] [number of years at Fleet] workiversary at Fleet. + BizOps were planning on posting something in the #random channel to recognize them, but I was wondering if you would like to instead? + ``` + + + > If a manager elects to post and hasn't done so by 2pm ET on the day of the workiversary, send them a friendly reminder and offer to post instead. + +4. If the manager has deferred to BizOps, schedule a Slack post for the following day to recognize the teammate's contributions at Fleet. If you’re unsure about what to post, take a look at what’s been [posted previously](https://docs.google.com/document/d/1Va4TYAs9Tb0soDQPeoeMr-qHxk0Xrlf-DUlBe4jn29Q/edit). + + ### Prepare salary benchmarking information 1. Use the relevant template text in the README section of the [¶¶ 💌 Compensation decisions document](https://docs.google.com/document/d/1NQ-IjcOTbyFluCWqsFLMfP4SvnopoXDcX0civ-STS5c/edit?usp=sharing) for a current Fleetie, a new role, a prospective hire, or other benchmarking use case. From d2ed0319a3f8f862f9b29d4dc074b2478a8da909 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Fri, 7 Jun 2024 14:31:25 -0700 Subject: [PATCH 013/119] Add lukeheath as fallback for time-sensitive docs PRs (#19605) --- CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 11a23080aa..398d08892c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -64,9 +64,9 @@ go.mod @fleetdm/go # # (see website/config/custom.js for DRIs of other paths not listed here) ############################################################################################## -/docs @rachaelshaw -/docs/Using-Fleet/REST-API.md @rachaelshaw # « REST API reference documentation -/docs/Contributing/API-for-contributors.md @rachaelshaw # « Advanced / contributors-only API reference documentation +/docs @rachaelshaw @lukeheath +/docs/Using-Fleet/REST-API.md @rachaelshaw @lukeheath # « REST API reference documentation +/docs/Contributing/API-for-contributors.md @rachaelshaw @lukeheath # « Advanced / contributors-only API reference documentation /schema @eashaw # « Data tables (osquery/fleetd schema) documentation /docs/Deploy/_kubernetes/ @dherder # « Kubernetes best practice ############################################################################################## From 6aef9520e9877b0e16f52b5cfff45903f3052c79 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Fri, 7 Jun 2024 17:33:00 -0400 Subject: [PATCH 014/119] MDM setup docs: APNs and ABM in the UI (#19463) --- ...points-to-expose-to-the-public-internet.md | 2 +- articles/windows-mdm-setup.md | 9 +- charts/fleet/values.yaml | 12 +- .../fleet-server-configuration.md | 2 +- .../Configuration-for-contributors.md | 2 +- docs/Using Fleet/MDM-setup.md | 252 +----------------- handbook/engineering/README.md | 2 +- website/config/routes.js | 4 +- 8 files changed, 18 insertions(+), 267 deletions(-) diff --git a/articles/what-api-endpoints-to-expose-to-the-public-internet.md b/articles/what-api-endpoints-to-expose-to-the-public-internet.md index d022f0f1ff..0c24d923ae 100644 --- a/articles/what-api-endpoints-to-expose-to-the-public-internet.md +++ b/articles/what-api-endpoints-to-expose-to-the-public-internet.md @@ -61,7 +61,7 @@ If you would like to use Fleet's Windows MDM features, the following endpoints n The `/api/*/fleet/*` endpoints accessed by the fleetd agent can use mTLS with the certificate provided via the `--fleet-tls-client-certificate` flag in the `fleetctl package` command. -The `/mdm/apple/mdm` and `/api/mdm/apple/enroll` endpoints can use mTLS with the [SCEP certificate issued by the Fleet server](https://fleetdm.com/docs/configuration/fleet-server-configuration#mdm-apple-scep-cert-bytes). +The `/mdm/apple/mdm` and `/api/mdm/apple/enroll` endpoints can use mTLS with the SCEP certificate issued by the Fleet server. These endpoints don't use mTLS: - `/mdm/apple/scep` diff --git a/articles/windows-mdm-setup.md b/articles/windows-mdm-setup.md index 89398b5df1..a8baceb6df 100644 --- a/articles/windows-mdm-setup.md +++ b/articles/windows-mdm-setup.md @@ -12,15 +12,12 @@ To use automatic enrollment (aka zero-touch) features on Windows, follow instruc Fleet uses a certificate and key pair to authenticate and manage interactions between Fleet and Windows host. -> If you're already using Fleet's macOS MDM features, you already have a SCEP certificate and key. Skip to step 2 and reuse the SCEP certificate and key as your WSTEP certificate and key. +How to generate a certificate and key: -If you're not using macOS MDM features, run the following command to download three files and send an email to you with an attached CSR file. +1. With [OpenSSL](https://www.openssl.org/) installed, open your Terminal (macOS) or PowerShell (Windows) and run the following command to create a key: `openssl genrsa --traditional -out fleet-mdm-win-wstep.key 4096`. -``` -fleetctl generate mdm-apple --email --org -``` +2. Create a certificate: `openssl req -x509 -new -nodes -key fleet-mdm-win-wstep.key -sha256 -days 3652 -out fleet-mdm-win-wstep.crt -subj '/CN=Fleet Root CA/C=US/O=Fleet.'`. -Save the SCEP certificate and SCEP key. These are your certificate and key. You can ignore the downloaded APNs key and the APNs CSR that was sent to your email. ### Step 2: Configure Fleet with your certificate and key diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 97e5b3d680..fa79563ac2 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -201,14 +201,10 @@ gke: # All of the environment variables that can be set for Fleet environments: # MDM Settings - # The following environment variables are used to configure Fleet to work with - # Apple's MDM service. These are optional and only required if you are using - # Fleet to manage Apple devices. - # To more information: https://fleetdm.com/docs/using-fleet/mdm-setup#step-3-configure-fleet-with-the-required-files - FLEET_MDM_APPLE_APNS_CERT_BYTES: "" - FLEET_MDM_APPLE_APNS_KEY_BYTES: "" - FLEET_MDM_APPLE_SCEP_CERT_BYTES: "" - FLEET_MDM_APPLE_SCEP_KEY_BYTES: "" + # The following environment variable is required if you are using + # Fleet's macOS MDM features. + # To more information: https://fleetdm.com/docs/using-fleet/fleet-server-configuration#server-private-key + FLEET_SERVER_PRIVATE_KEY: "" ## Section: Environment Variables from Secrets/CMs # envsFrom: diff --git a/docs/Configuration/fleet-server-configuration.md b/docs/Configuration/fleet-server-configuration.md index 90f87b5736..4319759347 100644 --- a/docs/Configuration/fleet-server-configuration.md +++ b/docs/Configuration/fleet-server-configuration.md @@ -2807,7 +2807,7 @@ packaging: > The [`server_private_key` configuration option](#server_private_key) is required for macOS MDM features. -> The Apple Push Notification service (APNs), SCEP, and Apple Business Manager (ABM) [configuration](https://github.com/fleetdm/fleet/fleet-v4.51.0/main/docs/Contributing/Configuration-for-contributors.md#mobile-device-management-mdm) are deprecated as of Fleet 4.51. They are maintained for backwards compatibility. Please upload your APNs certificate and ABM token in **Settings > Integrations MDM** and **Settings > Integrations > Automatic enrollment** respectively. +> The Apple Push Notification service (APNs), Simple Certificate Enrollment Protocol (SCEP), and Apple Business Manager (ABM) [certificate and key configuration](https://github.com/fleetdm/fleet/fleet-v4.51.0/main/docs/Contributing/Configuration-for-contributors.md#mobile-device-management-mdm) are deprecated as of Fleet 4.51. They are maintained for backwards compatibility. Please upload your APNs certificate and ABM token. Learn how [here](../Using%20Fleet/MDM-setup.md). ##### mdm.apple_scep_signer_validity_days diff --git a/docs/Contributing/Configuration-for-contributors.md b/docs/Contributing/Configuration-for-contributors.md index 08c2534366..4a409ed6b9 100644 --- a/docs/Contributing/Configuration-for-contributors.md +++ b/docs/Contributing/Configuration-for-contributors.md @@ -414,7 +414,7 @@ The content of the Simple Certificate Enrollment Protocol (SCEP) certificate. An -----END CERTIFICATE----- ``` -The SCEP certificate/key pair [generated by Fleet](https://fleetdm.com/docs/using-fleet/MDM-setup#step-1-generate-the-required-files) expires every 10 years. It's recommended to never change these unless they were compromised. +The SCEP certificate/key pair generated by Fleet expires every 10 years. It's recommended to never change these unless they were compromised. If your certificate/key pair was compromised and you change the pair, the disk encryption keys will no longer be viewable on all macOS hosts' **Host details** page until you turn disk encryption off and back on and the keys are [reset by the end user](https://fleetdm.com/docs/using-fleet/MDM-migration-guide#how-to-turn-on-disk-encryption). diff --git a/docs/Using Fleet/MDM-setup.md b/docs/Using Fleet/MDM-setup.md index d948d8bc4a..bfc175f23c 100644 --- a/docs/Using Fleet/MDM-setup.md +++ b/docs/Using Fleet/MDM-setup.md @@ -10,216 +10,19 @@ To turn on Windows MDM features, head to this [Windows MDM setup article](https: Apple uses APNs to authenticate and manage interactions between Fleet and the host. -This section will show you how to: -1. Generate the files to connect Fleet to APNs. -2. Generate an APNs certificate from Apple Push Certificates Portal. -3. Configure Fleet with the required files. +To connect Fleet to APNs or renew APNs, head to the **Settings > Integrations > Mobile device management (MDM)** page. -### Step 1: generate the required files - -For the MDM protocol to function, we need to generate the four following files: -- APNs certificate -- APNs private key -- Simple Certificate Enrollment Protocol (SCEP) certificate -- SCEP private key - -The APNs certificates serve as authentication between Fleet and Apple, while the SCEP certificates serve as authentication between Fleet and hosts. - -> To prevent abuse, please use your work email. If your email isn't accepted, please make sure it's not on this [list of blocked emails](https://github.com/fleetdm/fleet/blob/d5df23964b0b52f1d442b66ffe4451dc2a9ef969/website/api/controllers/deliver-apple-csr.js#L60). - -Use either of the following methods to generate the necessary files: - -#### Fleet UI - -1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. -2. Under **Apple Push Certificates Portal**, select **Request**, then fill out the form. This should generate three files and send an email to you with an attached CSR file. - -#### Fleetctl CLI - -Run the following command to download three files and send an email to you with an attached CSR file. - -```sh -fleetctl generate mdm-apple --email --org -``` - -### Step 2: generate an APNs certificate -1. Log in to or enroll in [Apple Push Certificates Portal](https://identity.apple.com). -2. Select **Create a Certificate**. -3. Upload your CSR and input a friendly name, such as "Fleet." -4. Download the APNs certificate. - -> **Important:** Take note of the Apple ID you use to sign into Apple Push Certificates Portal. You'll need to use the same Apple ID when renewing your APNs certificate. - -### Step 3: configure Fleet with the generated files - -Restart the Fleet server with the contents of the APNs certificate, APNs private key, SCEP certificate, and SCEP private key in the following environment variables: - -> Note: Any environment variable that ends in `_BYTES` expects the file's actual content to be passed in, not a path to the file. If you want to pass in a file path, remove the `_BYTES` suffix from the environment variable. - -* [FLEET_MDM_APPLE_APNS_CERT_BYTES](https://fleetdm.com/docs/deploying/configuration#mdm-apple-apns-cert-bytes) -* [FLEET_MDM_APPLE_APNS_KEY_BYTES](https://fleetdm.com/docs/deploying/configuration#mdm-apple-apns-key-bytes) -* [FLEET_MDM_APPLE_SCEP_CERT_BYTES](https://fleetdm.com/docs/deploying/configuration#mdm-apple-scep-cert-bytes) -* [FLEET_MDM_APPLE_SCEP_KEY_BYTES](https://fleetdm.com/docs/deploying/configuration#mdm-apple-scep-key-bytes) -* [FLEET_MDM_APPLE_SCEP_CHALLENGE](https://fleetdm.com/docs/deploying/configuration#mdm-apple-scep-challenge) - -> You do not need to provide the APNs CSR which was emailed to you. - -### Step 4: confirm that Fleet is set up correctly - -Use either of the following methods to confirm that Fleet is set up. You should see information about the APNs certificate such as serial number and renewal date. - -#### Fleet UI - -Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. - -#### Fleetctl CLI - -``` -fleetctl get mdm-apple -``` - -## Renewing APNs - -> **Important:** Apple requires that APNs certificates are renewed annually. +> Apple requires that APNs certificates are renewed annually. > - If your certificate expires, you will have to turn MDM off and back on for all macOS hosts. > - Be sure to use the same Apple ID from year-to-year. If you don't, you will have to turn MDM off and back on for all macOS hosts. -This section will guide you through how to: -1. Generate the files required to renew your APNs certificate. -2. Renew your APNs certificate in Apple Push Certificates Portal. -3. Configure Fleet with the required files. -4. Confirm that Fleet is set up correctly. - -Use either of the following methods to see your APNs certificate's renewal date and other important information: - -#### Fleet UI - -Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. - -#### Fleetctl CLI - -```sh -fleetctl get mdm-apple -``` - -### Step 1: generate the required files - -- A new APNs certificate. - -Run the following command in `fleetctl`. This will download three files and send an email to you with an attached CSR file. You may ignore the APNs key, SCEP certificate, and SCEP key as you do not need these to renew APNs. - -```sh -fleetctl generate mdm-apple --email --org -``` - -### Step 2: renew APNs certificate - -1. Log in to or enroll in [Apple Push Certificates Portal](https://identity.apple.com) using the same Apple ID you used to get your original APNs certificate. -2. Click **Renew** next to your certificate (make sure that the certificate's **Common Name (CN)** matches the one presented in Fleet). -3. Upload your CSR. -4. Download the new APNs certificate. - -### Step 3: configure Fleet with the generated files - -Restart the Fleet server with the contents of the APNs certificate in the following environment variable: -* [FLEET_MDM_APPLE_APNS_CERT_BYTES](https://fleetdm.com/docs/deploying/configuration#mdm-apple-apns-cert-bytes) - -### Step 4: confirm that Fleet is set up correctly - -Use either of the following methods to confirm that Fleet is set up: - -#### Fleet UI: - -1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. - -2. Follow the on-screen instructions in the **Apple Push Certificates Portal** section. - -#### Fleetctl CLI: - -Run the following command. You should see information about the new APNs certificate such as serial number and renewal date. - -```sh -fleetctl get mdm-apple -``` - -## Renewing SCEP -The SCEP certificates generated by Fleet and uploaded to the environment variables expire every 10 years. To renew them, regenerate the keys and update the relevant environment variables. - ## Apple Business Manager (ABM) > Available in Fleet Premium -By connecting Fleet to ABM, Macs purchased through Apple or an authorized reseller can automatically enroll to Fleet when they’re first unboxed and set up by your end user. +To connect Fleet to ABM or renew ABM, head to the **Settings > Integrations > Automatic enrollment > Apple Business Manager** page. -New or wiped macOS hosts that are in ABM, before they've been set up, appear in Fleet with **MDM status** set to "Pending". - -This section will guide you through how to: - -1. Generate certificate and private key for ABM -2. Create a new MDM server record for Fleet in ABM -3. Download the MDM server token from ABM -4. Upload the server token, certificate, and private key to the Fleet server -5. Set the new MDM server as the auto-enrollment server for Macs in ABM - -### Step 1: generate the required certificate and private key - -User either of the following methods to generate a certificate and private key pair. This pair is how Fleet authenticates itself to ABM: - -#### Fleet UI: - -1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. -2. Under **Apple Business Manager**, click the "Download" button - -#### Fleetctl CLI: - -```sh -fleetctl generate mdm-apple-bm -``` - -### Step 2: create a new MDM server in ABM - -Create an MDM server record in ABM which represents Fleet: - -1. Log in to or enroll in [ABM](https://business.apple.com) -2. Click your name at the bottom left of the screen -3. Click **Preferences** -4. Click **MDM Server Assignment** -5. Click the **Add** button at the top -6. Enter a name for the server such as "Fleet" -7. Upload the certificate generated in Step 1 - -### Step 3: download the server token - -In the details page of the newly created server, click **Download Token** at the top. You should receive a `.p7m` file. - -### Step 4: upload server token, certificate, and private key to Fleet - -With the three generated files, we now give them to the Fleet server so that it can authenticate itself to ABM. - -Restart the Fleet server with the contents of the server token, certificate, and private key in following environment variables: -* [FLEET_MDM_APPLE_BM_SERVER_TOKEN_BYTES](https://fleetdm.com/docs/deploying/configuration#mdm-apple-bm-server-token-bytes) -* [FLEET_MDM_APPLE_BM_CERT_BYTES](https://fleetdm.com/docs/deploying/configuration#mdm-apple-bm-cert-bytes) -* [FLEET_MDM_APPLE_BM_KEY_BYTES](https://fleetdm.com/docs/deploying/configuration#mdm-apple-bm-key-bytes) - -### Step 3: confirm that Fleet is set up correctly - -Use either of the following methods to confirm that Fleet is set up correctly. You should see information about the ABM server token such as organization name and renewal date. - -#### Fleet UI: - -1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. - -2. Navigate to the **Apple Business Manager** section. - -#### Fleetctl CLI: - -```sh -fleetctl get mdm-apple -``` - -### Step 5: set Fleet to be the MDM server for Macs in ABM - -Set Fleet to be the MDM for all future Macs purchased via Apple or an authorized reseller: +After connecting Fleet to ABM, set Fleet to be the MDM for all Macs: 1. Log in to [Apple Business Manager](https://business.apple.com) 2. Click your profile icon in the bottom left @@ -227,57 +30,12 @@ Set Fleet to be the MDM for all future Macs purchased via Apple or an authorized 4. Click **MDM Server Assignment** and click **Edit** next to **Default Server Assignment**. 5. Switch **Mac** to Fleet. -### Step 6: set the default team for hosts enrolled via ABM +New or wiped macOS hosts that are in ABM, before they've been set up, appear in Fleet with **MDM status** set to "Pending". All hosts that automatically enroll will be assigned to the default team. If no default team is set, then the host will be placed in "No team". > A host can be transferred to a new (not default) team before it enrolls. In the Fleet UI, you can do this under **Settings** > **Teams**. -Use either of the following methods to change the default team: - -#### Fleet UI - -1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. - -2. In the Apple Business Manager section, select the **Edit team** button next to **Default team**. - -3. Choose a team and select **Save**. - -#### Fleetctl CLI - -1. Create a `config` YAML document if you don't have one already. Learn how [here](./configuration-files/README.md#organization-settings). This document is used to change settings in Fleet. - -2. Set the `mdm.apple_bm_default_team` configuration option to the desired team's name. - -3. Run the `fleetctl apply -f ` command. - -## Renewing ABM - -> Apple expires ABM server tokens certificates once every year or whenever the account that downloaded the token has their password changed. - -Use either of the following methods to see your ABM renewal date and other important information: - -#### Fleet UI - -1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. - -2. Look at the **Apple Business Manager** section. - -#### Fleetctl CLI - -```sh -fleetctl get mdm-apple -``` - -If you have configured Fleet with an Apple Business Manager server token for mobile device management (a Fleet Premium feature), you will eventually need to renew that token. [As documented in the Apple Business Manager User Guide](https://support.apple.com/en-ca/guide/apple-business-manager/axme0f8659ec/web), the token expires after a year or whenever the account that downloaded the token has their password changed. - -To renew the token: -1. Log in to [business.apple.com](https://business.apple.com) -2. Select Fleet's MDM server record -3. Download a new token for that server record -4. In your Fleet server, update the environment variable [FLEET_MDM_APPLE_BM_SERVER_TOKEN_BYTES](https://fleetdm.com/docs/deploying/configuration#mdm-apple-bm-server-token-bytes) -5. Restart the Fleet server - diff --git a/handbook/engineering/README.md b/handbook/engineering/README.md index 494f9351d5..47fec5cb51 100644 --- a/handbook/engineering/README.md +++ b/handbook/engineering/README.md @@ -414,7 +414,7 @@ Steps to renew the certificate: * Update `sails_custom__mdmVendorKeyPassphrase` with the passphrase used in step 4 * Update `sails_custom__mdmVendorKeyPem` with `VendorPrivateKey.key` from step 4 9. Store updated values in [Confidential 1Password Vault](https://start.1password.com/open/i?a=N3F7LHAKQ5G3JPFPX234EC4ZDQ&v=lcvkjobeheaqdgnz33ontpuhxq&i=byyfn2knejwh42a2cbc5war5sa&h=fleetdevicemanagement.1password.com) -10. Verify by logging into a normal apple account (not billing@...) and Generate a new Push Certificate following our [setup MDM](https://fleetdm.com/docs/using-fleet/mdm-setup#step-2-generate-an-apns-certificate) steps and verify the Expiration date is 1 year from today. +10. Verify by logging into a normal apple account (not billing@...) and Generate a new Push Certificate following our [setup MDM](https://fleetdm.com/docs/using-fleet/mdm-setup) steps and verify the Expiration date is 1 year from today. 11. Adjust calendar event to be between 2-4 weeks before the next expiration. ### Perform an incident postmortem diff --git a/website/config/routes.js b/website/config/routes.js index bf6f330297..5423f243c5 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -522,8 +522,8 @@ module.exports.routes = { 'GET /learn-more-about/calendar-events': '/announcements/fleet-in-your-calendar-introducing-maintenance-windows', 'GET /learn-more-about/setup-windows-mdm': '/docs/using-fleet/mdm-setup', 'GET /learn-more-about/setup-abm': '/docs/using-fleet/mdm-setup#apple-business-manager-abm', - 'GET /learn-more-about/renew-apns': '/docs/using-fleet/mdm-setup#renewing-apns', - 'GET /learn-more-about/renew-abm': '/docs/using-fleet/mdm-macos-setup#renewing-abm', + 'GET /learn-more-about/renew-apns': '/docs/using-fleet/mdm-setup#apple-push-notification-service-apns', + 'GET /learn-more-about/renew-abm': '/docs/using-fleet/mdm-setup#apple-business-manager-abm', 'GET /learn-more-about/fleet-server-private-key': '/docs/using-fleet/fleet-server-configuration#server-private-key', // Sitemap From 2a9b10855dd28e4f7897fb1e004ca136ff499cd3 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Fri, 7 Jun 2024 14:50:57 -0700 Subject: [PATCH 015/119] Fix learn more about configuration redirect (#19607) --- website/config/routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/config/routes.js b/website/config/routes.js index 5423f243c5..7814ed055c 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -524,7 +524,7 @@ module.exports.routes = { 'GET /learn-more-about/setup-abm': '/docs/using-fleet/mdm-setup#apple-business-manager-abm', 'GET /learn-more-about/renew-apns': '/docs/using-fleet/mdm-setup#apple-push-notification-service-apns', 'GET /learn-more-about/renew-abm': '/docs/using-fleet/mdm-setup#apple-business-manager-abm', - 'GET /learn-more-about/fleet-server-private-key': '/docs/using-fleet/fleet-server-configuration#server-private-key', + 'GET /learn-more-about/fleet-server-private-key': '/docs/configuration/fleet-server-configuration#server-private-key', // Sitemap // ============================================================================================================= From da0268eec855afb4fad1dbb48b4e2d656c38d1dd Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 7 Jun 2024 17:48:44 -0500 Subject: [PATCH 016/119] Website: Update Markdown headings (#19608) Closes: #19606 Changes: - Updated the `to-html` helper to add optional linebreaks to all Markdown headings that contain an underscore. --- website/api/helpers/strings/to-html.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/api/helpers/strings/to-html.js b/website/api/helpers/strings/to-html.js index 91e29a5273..5182528b98 100644 --- a/website/api/helpers/strings/to-html.js +++ b/website/api/helpers/strings/to-html.js @@ -94,7 +94,11 @@ module.exports = { }; } else { customRenderer.heading = function (text, level) { - return ''+text+''; + var textWithLineBreaks; + if(text.match(/\S(\w+\_\S)+(\w\S)+/g) && !text.match(/\s/g)){ + textWithLineBreaks = text.replace(/(\_)/g, '​_'); + } + return ''+(textWithLineBreaks ? textWithLineBreaks : text)+''; }; } From 518e5f4087d7b705b8dac53de18dd54f1ffe2fda Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 7 Jun 2024 18:12:27 -0500 Subject: [PATCH 017/119] Website: Update links to queries in query library (#19604) Closes: #19228 Changes: - Removed the click event from the cards on the /queries page and updated them to be links. --- website/assets/styles/pages/query-library.less | 4 ++++ website/views/pages/query-library.ejs | 11 ++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/website/assets/styles/pages/query-library.less b/website/assets/styles/pages/query-library.less index a6d883789e..6584078dd2 100644 --- a/website/assets/styles/pages/query-library.less +++ b/website/assets/styles/pages/query-library.less @@ -67,6 +67,7 @@ padding: 2px 8px; border-radius: 20px; background-color: #E2E4EA; + color: #515774; } [purpose='selected-tag'] { @@ -199,6 +200,7 @@ .description { padding: 0px 30px 0px 30px; p { + color: #515774; font-size: 16px; line-height: 25px; } @@ -263,6 +265,7 @@ box-shadow: none; border: none; border-radius: 8px; + color: #515774; &:hover { .query-card { background-color: #F8F7FF; @@ -271,6 +274,7 @@ border-radius: 8px; cursor: pointer; } + text-decoration: none; } } diff --git a/website/views/pages/query-library.ejs b/website/views/pages/query-library.ejs index 59172d0798..14f54ec040 100644 --- a/website/views/pages/query-library.ejs +++ b/website/views/pages/query-library.ejs @@ -98,15 +98,15 @@

-
+
{{query.name}}
-
Critical - Requires MDM - {{tag}} + Critical + Requires MDM + {{tag}}
@@ -141,7 +141,8 @@
-
+ +

There are no results that match your filters. Everyone can contribute.

From 1fac823fa9496e7f8ad666582149048517342c0b Mon Sep 17 00:00:00 2001 From: Eric Date: Sun, 9 Jun 2024 15:00:55 -0500 Subject: [PATCH 018/119] Website: Update Salesforce helepr to set an an Owner ID on all new records. (#19609) Changes: - Updated the update-or-create-contact-and-account helper to always set the integrations admin user as the owner of new accounts and contact records created. --- .../helpers/salesforce/update-or-create-contact-and-account.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/api/helpers/salesforce/update-or-create-contact-and-account.js b/website/api/helpers/salesforce/update-or-create-contact-and-account.js index 3388a161fb..891d69154d 100644 --- a/website/api/helpers/salesforce/update-or-create-contact-and-account.js +++ b/website/api/helpers/salesforce/update-or-create-contact-and-account.js @@ -103,6 +103,7 @@ module.exports = { } else { // If no existing account record was found, create a new one. // Create a timestamp to use for the new account's assigned date. + salesforceAccountOwnerId = '0054x00000735wDAAQ';// « "Integrations admin" user. let today = new Date(); let nowOn = today.toISOString().replace('Z', '+0000'); @@ -117,6 +118,7 @@ module.exports = { Website: enrichmentData.employer.emailDomain, LinkedIn_company_URL__c: enrichmentData.employer.linkedinCompanyPageUrl,// eslint-disable-line camelcase NumberOfEmployees: enrichmentData.employer.numberOfEmployees, + OwnerId: salesforceAccountOwnerId }); salesforceAccountId = newAccountRecord.id; }//fi From 9a4b6a4abe8cde340a74a52b36f6e8e1d6f9bba1 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:00:06 -0400 Subject: [PATCH 019/119] Dogfood policies: deduplicate (#19613) - Add inline policies that are unique to "Workstations" or "Workstations (canary)" to remove duplicate policies in `macos-device-health-canary.yml` --- .../macos-device-health-canary.policies.yml | 80 ------------------- .../lib/macos-device-health.policies.yml | 9 +-- it-and-security/teams/workstations-canary.yml | 16 +++- it-and-security/teams/workstations.yml | 7 ++ 4 files changed, 23 insertions(+), 89 deletions(-) delete mode 100644 it-and-security/lib/macos-device-health-canary.policies.yml diff --git a/it-and-security/lib/macos-device-health-canary.policies.yml b/it-and-security/lib/macos-device-health-canary.policies.yml deleted file mode 100644 index 134680169b..0000000000 --- a/it-and-security/lib/macos-device-health-canary.policies.yml +++ /dev/null @@ -1,80 +0,0 @@ -- name: macOS - Enable FileVault - query: SELECT 1 FROM filevault_status WHERE status = 'FileVault is On.'; - critical: false - description: This policy checks if FileVault (disk encryption) is enabled. - resolution: As an IT admin, turn on disk encryption in Fleet. - platform: darwin -- name: macOS - Enable Firewall - query: SELECT 1 FROM managed_policies WHERE domain='com.apple.security.firewall' AND username = '' AND name='EnableFirewall' AND CAST(value AS INT) = 1; - critical: false - description: This policy checks if Firewall is enabled. - resolution: An an IT admin, deploy a macOS, Firewall profile with the EnableFirewall option set to true. - platform: darwin -- name: macOS - Disable guest account - query: SELECT 1 FROM plist WHERE path='/Library/Preferences/com.apple.loginwindow.plist' AND key='GuestEnabled' AND value = 0; - critical: false - description: This policy checks if the guest account is disabled. - resolution: An an IT admin, deploy a macOS, login window profile with the DisableGuestAccount option set to true. - platform: darwin -- name: macOS - Require 10 character password - query: SELECT 1 WHERE - EXISTS ( - SELECT 1 FROM managed_policies WHERE - domain='com.apple.screensaver' AND - name='askForPassword' AND - CAST(value AS INT) - ) - AND EXISTS ( - SELECT 1 FROM managed_policies WHERE - domain='com.apple.screensaver' AND - name='minLength' AND - CAST(value AS INT) <= 10 - ); - critical: false - description: This policy checks if the end user is required to enter a password, with at least 10 characters, to unlock the host. - resolution: An an IT admin, deploy a macOS, screensaver profile with the askForPassword option set to true and minLength option set to 10. - platform: darwin -- name: macOS - Enable screen saver after 20 minutes - query: SELECT 1 WHERE - EXISTS ( - SELECT 1 FROM managed_policies WHERE - domain='com.apple.screensaver' AND - name='idleTime' AND - CAST(value AS INT) <= 1200 AND - username = '' - ) - AND NOT EXISTS ( - SELECT 1 FROM managed_policies WHERE - domain='com.apple.screensaver' AND - name='idleTime' AND - CAST(value AS INT) > 1200 - ); - critical: false - description: This policy checks if maximum amount of time (in minutes) the device is allowed to sit idle before the screen is locked. End users can select any value less than the specified maximum. - resolution: An an IT admin, deploy a macOS, screen saver profile with the maxInactivity option set to 20 minutes. - platform: darwin -- name: macOS - No 1Password emergency kit stored in desktop, documents, or downloads folders - query: SELECT 1 WHERE - NOT EXISTS ( - SELECT 1 FROM file WHERE - filename LIKE '%Emergency Kit%.pdf' AND - (path LIKE '/Users/%/Desktop/%' OR path LIKE '/Users/%/Documents/%' OR path LIKE '/Users/%/Downloads/%' OR path LIKE '/Users/Shared/%') - ); - critical: false - description: Looks for PDF files with file names typically used by 1Password for emergency recovery kits. To protect the performance of your devices, the search is one level deep and limited to the Desktop, Documents, Downloads, and Shared folders. - resolution: Delete 1Password emergency kits from your computer, and empty the trash. 1Password emergency kits should only be printed and stored in a physically secure location. - platform: darwin -- name: macOS - Check if latest version - query: SELECT 1 FROM os_version WHERE major = "14" AND minor = "5" AND patch >= "1"; - critical: false - description: Using an outdated macOS version risks exposure to security vulnerabilities and potential system instability. - resolution: We will update your macOS to the latest version. - platform: darwin - calendar_events_enabled: false -- name: macOS - MDM migration complete - query: SELECT 1 AS result FROM system_info WHERE local_hostname != 'Titanosauria'; - critical: false - description: Determines if the device has completed MDM migration to Fleet. - resolution: We will migrate your macOS MDM to Fleet. - platform: darwin - calendar_events_enabled: true diff --git a/it-and-security/lib/macos-device-health.policies.yml b/it-and-security/lib/macos-device-health.policies.yml index c9b7879157..75bfaf4b0b 100644 --- a/it-and-security/lib/macos-device-health.policies.yml +++ b/it-and-security/lib/macos-device-health.policies.yml @@ -64,11 +64,4 @@ description: Looks for PDF files with file names typically used by 1Password for emergency recovery kits. To protect the performance of your devices, the search is one level deep and limited to the Desktop, Documents, Downloads, and Shared folders. resolution: Delete 1Password emergency kits from your computer, and empty the trash. 1Password emergency kits should only be printed and stored in a physically secure location. platform: darwin -- name: macOS - Check if latest version - query: SELECT 1 FROM os_version WHERE major = '14' AND minor = '5'; - # patch query: SELECT 1 FROM os_version WHERE major = "14" AND minor = "5" AND patch >= "1"; - critical: false - description: Using an outdated macOS version risks exposure to security vulnerabilities and potential system instability. - resolution: We will update your macOS to the latest version. - platform: darwin - calendar_events_enabled: true \ No newline at end of file + \ No newline at end of file diff --git a/it-and-security/teams/workstations-canary.yml b/it-and-security/teams/workstations-canary.yml index 5d1c8b7985..55304f6ebf 100644 --- a/it-and-security/teams/workstations-canary.yml +++ b/it-and-security/teams/workstations-canary.yml @@ -110,9 +110,23 @@ controls: - path: ../lib/windows-install-bitdefender.ps1 - path: ../lib/windows-enable-ms-defender.ps1 policies: - - path: ../lib/macos-device-health-canary.policies.yml + - path: ../lib/macos-device-health.policies.yml - path: ../lib/windows-device-health.policies.yml - path: ../lib/linux-device-health.policies.yml + - name: macOS - Check if latest version + query: SELECT 1 FROM os_version WHERE major = '14' AND minor = '5'; + critical: false + description: Using an outdated macOS version risks exposure to security vulnerabilities and potential system instability. + resolution: We will update your macOS to the latest version. + platform: darwin + calendar_events_enabled: false + - name: macOS - MDM migration complete + query: SELECT 1 AS result FROM system_info WHERE local_hostname != 'Titanosauria'; + critical: false + description: Determines if the device has completed MDM migration to Fleet. + resolution: We will migrate your macOS MDM to Fleet. + platform: darwin + calendar_events_enabled: true queries: - path: ../lib/collect-failed-login-attempts.queries.yml - path: ../lib/collect-fleetd-information.yml diff --git a/it-and-security/teams/workstations.yml b/it-and-security/teams/workstations.yml index 61d123a0d2..6e28a28f6f 100644 --- a/it-and-security/teams/workstations.yml +++ b/it-and-security/teams/workstations.yml @@ -61,6 +61,13 @@ policies: - path: ../lib/macos-device-health.policies.yml - path: ../lib/windows-device-health.policies.yml - path: ../lib/linux-device-health.policies.yml + - name: macOS - Check if latest version + query: SELECT 1 FROM os_version WHERE major = '14' AND minor = '5'; + critical: false + description: Using an outdated macOS version risks exposure to security vulnerabilities and potential system instability. + resolution: We will update your macOS to the latest version. + platform: darwin + calendar_events_enabled: true queries: - path: ../lib/collect-failed-login-attempts.queries.yml - path: ../lib/collect-usb-devices.queries.yml From be753af9d6cacd501f7ee57eaa8f81788776e947 Mon Sep 17 00:00:00 2001 From: Joanne Stableford <59930035+JoStableford@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:31:18 -0400 Subject: [PATCH 020/119] Add responsibility to BizOps handbook - low credit alert (#19445) --- handbook/business-operations/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/handbook/business-operations/README.md b/handbook/business-operations/README.md index 029bf9090f..5fb48ff42d 100644 --- a/handbook/business-operations/README.md +++ b/handbook/business-operations/README.md @@ -312,6 +312,15 @@ Create a [new montly accounting issue](https://github.com/fleetdm/confidential/i - **When is the issue created?** We create and close the monthly accounting issue for the previous month within the first 7 days of the following month. For example, the monthly accounting issue to close out the month of January is created promptly in February and closed before the end of the day, Feb 7th. A convenient trick is to create the issue on the first Friday of the month and close it ASAP. +### Respond to low credit alert +Fleet admins will receive an email alert when the usage of company cards for the month is aproaching the company credit limit. To avoid the limit being exceeded, a Brex admin will follow these steps: +1. Sign in to Fleet's Brex account. +2. On the landing page, use the "Move money" button to "Add funds to your Brex business accounts". +3. Select "Transfer from a connected account" and select the primary business account. +4. Choose the "One time" transfer option and process the transfer. + +No further action needs to be taken, the amount available for use will increase without disruption to regular processes. + ### Check franchise tax status No later than the second month of every quarter, we check [Delaware divison of corporations](https://icis.corp.delaware.gov) to ensure that Fleet has paid the quarterly franchise tax amounts to remain in good standing with the state of Delaware. - Go to the [DCIS - eCorp website](https://icis.corp.delaware.gov/ecorp/logintax.aspx?FilingType=FranchiseTax) and use the details in 1Password to look up Fleet's status. From fbe9c1b49824979eac4ce3f69b005c8a92f1ebac Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Mon, 10 Jun 2024 09:47:23 -0700 Subject: [PATCH 021/119] Adding changes for Fleet v4.51.0 (#19601) --- CHANGELOG.md | 43 +++++++++++++++++++ changes/10383-mdm-saved-certs-ui | 1 - changes/11942-duplicated-software | 1 - changes/14722-activity-feed-webhooks | 2 - changes/16795-update-go | 1 - changes/17309-support-env-vars-profiles | 1 - changes/17513-bulk-host-opts-filters | 1 - changes/17587-software-self-service-ui | 1 - .../17860-improve-license-expiration-banner | 1 - changes/18053-ubuntu-kernel-vuln-detection | 1 - changes/18119-iphone-ipad-support | 1 - changes/18447-firefox-esr | 1 - changes/18461-windows-lock | 1 - .../18515-remove-host-ids-from-list-labels | 1 - changes/18732-switch-teams-reset-page | 1 - changes/18741-form-field-tooltip-positions | 1 - changes/18833-filter-software-by-self-service | 1 - .../18834-add-self-service-install-endpoint | 1 - changes/18834-fleetctl-add-self-service-field | 1 - changes/18838-additional-db-optimizations | 5 --- .../18847-software-self-install-activities | 1 - changes/18862-upgradeCIS-win11 | 1 - changes/18881-queries-table-filter-bugs | 2 - ...18912-controls-language-and-cta-button-fix | 1 - ...9001-builtin-label-names-selecting-targets | 1 - changes/19014-certs-endpoints | 2 - changes/19052-activity-feed-webhooks | 1 - changes/19072-additional-stats | 1 - changes/19152-gitops-duplicate-enroll-secret | 1 - changes/19171-host-query-bug-fixes | 1 - changes/19179-bm | 1 - changes/19267-bugfix-ui-wipe-menu | 3 -- changes/19272-live-query-lag | 1 - changes/19311-scep-renew | 1 - changes/19464-private-key-errors | 1 - changes/add-tuxedo-os | 1 - ...e-18847-add-ui-activities-for-self-service | 1 - changes/jve-fix-lock-script-typo | 1 - changes/jve-pk-docs | 2 - changes/post-apns-cert | 2 - changes/save-certs-encrypted | 2 - charts/fleet/Chart.yaml | 2 +- charts/fleet/values.yaml | 2 +- .../dogfood/terraform/aws/variables.tf | 2 +- .../dogfood/terraform/gcp/variables.tf | 2 +- terraform/README.md | 2 +- terraform/addons/vuln-processing/variables.tf | 4 +- terraform/byo-vpc/README.md | 2 +- terraform/byo-vpc/byo-db/README.md | 2 +- terraform/byo-vpc/byo-db/byo-ecs/README.md | 2 +- 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 +- tools/release/publish_release.sh | 2 +- 58 files changed, 66 insertions(+), 75 deletions(-) delete mode 100644 changes/10383-mdm-saved-certs-ui delete mode 100644 changes/11942-duplicated-software delete mode 100644 changes/14722-activity-feed-webhooks delete mode 100644 changes/16795-update-go delete mode 100644 changes/17309-support-env-vars-profiles delete mode 100644 changes/17513-bulk-host-opts-filters delete mode 100644 changes/17587-software-self-service-ui delete mode 100644 changes/17860-improve-license-expiration-banner delete mode 100644 changes/18053-ubuntu-kernel-vuln-detection delete mode 100644 changes/18119-iphone-ipad-support delete mode 100644 changes/18447-firefox-esr delete mode 100644 changes/18461-windows-lock delete mode 100644 changes/18515-remove-host-ids-from-list-labels delete mode 100644 changes/18732-switch-teams-reset-page delete mode 100644 changes/18741-form-field-tooltip-positions delete mode 100644 changes/18833-filter-software-by-self-service delete mode 100644 changes/18834-add-self-service-install-endpoint delete mode 100644 changes/18834-fleetctl-add-self-service-field delete mode 100644 changes/18838-additional-db-optimizations delete mode 100644 changes/18847-software-self-install-activities delete mode 100644 changes/18862-upgradeCIS-win11 delete mode 100644 changes/18881-queries-table-filter-bugs delete mode 100644 changes/18912-controls-language-and-cta-button-fix delete mode 100644 changes/19001-builtin-label-names-selecting-targets delete mode 100644 changes/19014-certs-endpoints delete mode 100644 changes/19052-activity-feed-webhooks delete mode 100644 changes/19072-additional-stats delete mode 100644 changes/19152-gitops-duplicate-enroll-secret delete mode 100644 changes/19171-host-query-bug-fixes delete mode 100644 changes/19179-bm delete mode 100644 changes/19267-bugfix-ui-wipe-menu delete mode 100644 changes/19272-live-query-lag delete mode 100644 changes/19311-scep-renew delete mode 100644 changes/19464-private-key-errors delete mode 100644 changes/add-tuxedo-os delete mode 100644 changes/issue-18847-add-ui-activities-for-self-service delete mode 100644 changes/jve-fix-lock-script-typo delete mode 100644 changes/jve-pk-docs delete mode 100644 changes/post-apns-cert delete mode 100644 changes/save-certs-encrypted diff --git a/CHANGELOG.md b/CHANGELOG.md index 391060e529..b1cdef747e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +## Fleet 4.51.0 (Jun 10, 2024) + +### Endpoint Operations +- Added support for environment variables in configuration profiles for GitOps. +- `fleetctl gitops --dry-run` now errors on duplicate (or conflicting) global/team enroll secrets. +- Added `activities_webhook` configuration option to allow for a webhook to be called when an activity is recorded. This can be used to send activity data to external services. If the webhook response is a 429 error code, the webhook retries for up to 30 minutes. +- Added Tuxedo OS to the Linux distribution platform list. + +### Device Management (MDM) +- **NOTE:** Added new required Fleet server config environment variable when MDM is enabled, + `FLEET_SERVER_PRIVATE_KEY`. This variable contains the private key used to encrypt the MDM + certificates and keys stored in Fleet. Learm more at + https://fleetdm.com/learn-more-about/fleet-server-private-key. +- Added MDM support for iPhone/iPad. +- Added software self-service support. +- Added query parameter `self_service` to filter the list of software titles and the list of a host's software so that only those available to install via self-service are returned. +- Added the device-authenticated endpoint `POST /device/{token}/software/install/{software_title_id}` to self-install software. +- Added new endpoints to configure ABM keypairs and tokens. +- Added `GET /fleet/mdm/apple/request_csr` endpoint, which returns the signed APNS CSR needed to activate Apple MDM. +- Added the ability to automatically log off and lock out `Administrator` users on Windows hosts. +- Added clearer error messages when attempting to set up Apple MDM without a server private key configured. +- Added UI for the global and host activities for self-service software installation. +- Updated UI to support new workflows for macOS MDM setup and credentials. +- Updated UI to support software self-service features. +- Updated UI controls page language and hid CTA button for users without access to turn on MDM. + +### Vulnerability Management +- Updated the CIS policies for Windows 11 Enterprise from v2.0.0 (03-07-2023) to v3.0.0 (02-22-2024). +- Fleet now detects Ubuntu kernel vulnerabilities from the Canonical OVAL feed. +- Fleet now detects and reports vulnerabilities on Firefox ESR editions on macOS. + +### Bug fixes and improvements +- Fixed a bug that might prevent enqueuing commands to renew SCEP certificates if the host was enrolled more than once. +- Prevented the `host_id`s field from being returned from the list labels endpoint. +- Improved software ingestion performance by deduplicating incoming software. +- Placed all form field label tooltips on top. +- Fixed a number of related issues with the filtering and sorting of the queries table. +- Added various optimizations to the rendering of the queries table. +- Fixed host query page styling bugs. +- Fixed a UI bug where "Wipe" action was not being hidden from observers. +- Fixed UI bug for builtin label names for selecting targets. +- Removed references to Administrator accounts in the comments of the Windows lock script. + ## Fleet 4.50.2 (May 31, 2024) ### Bug fixes diff --git a/changes/10383-mdm-saved-certs-ui b/changes/10383-mdm-saved-certs-ui deleted file mode 100644 index 2b796dc690..0000000000 --- a/changes/10383-mdm-saved-certs-ui +++ /dev/null @@ -1 +0,0 @@ -- Updated UI to support new workflows for macOS MDM setup and credentials. diff --git a/changes/11942-duplicated-software b/changes/11942-duplicated-software deleted file mode 100644 index 065a101590..0000000000 --- a/changes/11942-duplicated-software +++ /dev/null @@ -1 +0,0 @@ -Improved software ingestion performance by deduplicating incoming software. diff --git a/changes/14722-activity-feed-webhooks b/changes/14722-activity-feed-webhooks deleted file mode 100644 index aa3f94a79a..0000000000 --- a/changes/14722-activity-feed-webhooks +++ /dev/null @@ -1,2 +0,0 @@ -Added `activities_webhook` configuration option to allow for a webhook to be called when an activity is recorded. This can be used to send activity data to external services. -If the webhook response is a 429 error code, the webhook retries for up to 30 minutes. diff --git a/changes/16795-update-go b/changes/16795-update-go deleted file mode 100644 index d4684530a3..0000000000 --- a/changes/16795-update-go +++ /dev/null @@ -1 +0,0 @@ -* Update Go version to go1.22.3 diff --git a/changes/17309-support-env-vars-profiles b/changes/17309-support-env-vars-profiles deleted file mode 100644 index 9e95efa76c..0000000000 --- a/changes/17309-support-env-vars-profiles +++ /dev/null @@ -1 +0,0 @@ -* Support environment variables in configuration profiles for GitOps. diff --git a/changes/17513-bulk-host-opts-filters b/changes/17513-bulk-host-opts-filters deleted file mode 100644 index 76328c6023..0000000000 --- a/changes/17513-bulk-host-opts-filters +++ /dev/null @@ -1 +0,0 @@ -- Bulk Host Delete and Transfer now support status and labelID filters together \ No newline at end of file diff --git a/changes/17587-software-self-service-ui b/changes/17587-software-self-service-ui deleted file mode 100644 index ba4297a3be..0000000000 --- a/changes/17587-software-self-service-ui +++ /dev/null @@ -1 +0,0 @@ -- Updated UI to support software self-service features. diff --git a/changes/17860-improve-license-expiration-banner b/changes/17860-improve-license-expiration-banner deleted file mode 100644 index 23eaa3a507..0000000000 --- a/changes/17860-improve-license-expiration-banner +++ /dev/null @@ -1 +0,0 @@ -- UI: Updated look to license expiration banner diff --git a/changes/18053-ubuntu-kernel-vuln-detection b/changes/18053-ubuntu-kernel-vuln-detection deleted file mode 100644 index 79d0bf7b5a..0000000000 --- a/changes/18053-ubuntu-kernel-vuln-detection +++ /dev/null @@ -1 +0,0 @@ -- fleet now detects Ubuntu kernel vulnerabilities from the Canonical OVAL feed \ No newline at end of file diff --git a/changes/18119-iphone-ipad-support b/changes/18119-iphone-ipad-support deleted file mode 100644 index 89d958a3af..0000000000 --- a/changes/18119-iphone-ipad-support +++ /dev/null @@ -1 +0,0 @@ -* Added MDM support for iPhone/iPad. diff --git a/changes/18447-firefox-esr b/changes/18447-firefox-esr deleted file mode 100644 index 15a57163c3..0000000000 --- a/changes/18447-firefox-esr +++ /dev/null @@ -1 +0,0 @@ -- detect and report vulnerabilities on Firefox ESR editions on macOS \ No newline at end of file diff --git a/changes/18461-windows-lock b/changes/18461-windows-lock deleted file mode 100644 index 68dd284c2e..0000000000 --- a/changes/18461-windows-lock +++ /dev/null @@ -1 +0,0 @@ -- Adds the ability to automatically log off and lock out `Administrator` users on Windows hosts. \ No newline at end of file diff --git a/changes/18515-remove-host-ids-from-list-labels b/changes/18515-remove-host-ids-from-list-labels deleted file mode 100644 index 20de8a9a61..0000000000 --- a/changes/18515-remove-host-ids-from-list-labels +++ /dev/null @@ -1 +0,0 @@ -- Prevent the `host_id`s field from being returned from the list labels endpoint. diff --git a/changes/18732-switch-teams-reset-page b/changes/18732-switch-teams-reset-page deleted file mode 100644 index 4c5bb6e851..0000000000 --- a/changes/18732-switch-teams-reset-page +++ /dev/null @@ -1 +0,0 @@ -- UI fix: Switching team resets to page 0 for all software and policy tables diff --git a/changes/18741-form-field-tooltip-positions b/changes/18741-form-field-tooltip-positions deleted file mode 100644 index 4131593b4a..0000000000 --- a/changes/18741-form-field-tooltip-positions +++ /dev/null @@ -1 +0,0 @@ -* Place all form field label tooltips on top diff --git a/changes/18833-filter-software-by-self-service b/changes/18833-filter-software-by-self-service deleted file mode 100644 index 20381213a0..0000000000 --- a/changes/18833-filter-software-by-self-service +++ /dev/null @@ -1 +0,0 @@ -* Added query parameter `self_service` to filter the list of software titles and the list of a host's software so that only those available to install via self-service are returned. diff --git a/changes/18834-add-self-service-install-endpoint b/changes/18834-add-self-service-install-endpoint deleted file mode 100644 index b69b8568f0..0000000000 --- a/changes/18834-add-self-service-install-endpoint +++ /dev/null @@ -1 +0,0 @@ -* Added the device-authenticated endpoint `POST /device/{token}/software/install/{software_title_id}` to self-install software. diff --git a/changes/18834-fleetctl-add-self-service-field b/changes/18834-fleetctl-add-self-service-field deleted file mode 100644 index 4a934e3493..0000000000 --- a/changes/18834-fleetctl-add-self-service-field +++ /dev/null @@ -1 +0,0 @@ -* Added the `self_service` field to `fleetctl apply` and `fleetctl gitops` YAML configuration files. diff --git a/changes/18838-additional-db-optimizations b/changes/18838-additional-db-optimizations deleted file mode 100644 index 97be894d07..0000000000 --- a/changes/18838-additional-db-optimizations +++ /dev/null @@ -1,5 +0,0 @@ -MySQL query optimizations: -- During software ingestion, switched to updating last_opened_at as a batch (for 1 host). -- Removed DELETE FROM software statement that ran for every host update (when software was deleted). The cleanup of unused software is now only done during the vulnerability job. -- `/api/v1/fleet/software/versions/:id` endpoint can return software even if it has been recently deleted from all hosts. During hourly cleanup, this software item will be removed from the database. -- Moved aggregated query stats calculations to read replica DB to reduce load on the master. diff --git a/changes/18847-software-self-install-activities b/changes/18847-software-self-install-activities deleted file mode 100644 index d7c1a8e2f6..0000000000 --- a/changes/18847-software-self-install-activities +++ /dev/null @@ -1 +0,0 @@ -* Added the `self_install` and `software_package` fields to the `installed_software` activity. diff --git a/changes/18862-upgradeCIS-win11 b/changes/18862-upgradeCIS-win11 deleted file mode 100644 index fd9b56f643..0000000000 --- a/changes/18862-upgradeCIS-win11 +++ /dev/null @@ -1 +0,0 @@ -* Updated the CIS policies for Windows 11 Enterprise fro v2.0.0 - 03-07-2023 to v3.0.0 - 02-22-2024 diff --git a/changes/18881-queries-table-filter-bugs b/changes/18881-queries-table-filter-bugs deleted file mode 100644 index a8c5470354..0000000000 --- a/changes/18881-queries-table-filter-bugs +++ /dev/null @@ -1,2 +0,0 @@ -- Fix a number of related issues with the filtering and sorting of the queries table. -- Add various optimizations to the rendering of the queries table. diff --git a/changes/18912-controls-language-and-cta-button-fix b/changes/18912-controls-language-and-cta-button-fix deleted file mode 100644 index 3297d715ec..0000000000 --- a/changes/18912-controls-language-and-cta-button-fix +++ /dev/null @@ -1 +0,0 @@ -- UI: Updates to controls page language and hide CTA button for users without access to turn on MDM diff --git a/changes/19001-builtin-label-names-selecting-targets b/changes/19001-builtin-label-names-selecting-targets deleted file mode 100644 index 323d69fe0b..0000000000 --- a/changes/19001-builtin-label-names-selecting-targets +++ /dev/null @@ -1 +0,0 @@ -- UI: Fix builtin label names for selecting targets diff --git a/changes/19014-certs-endpoints b/changes/19014-certs-endpoints deleted file mode 100644 index d2bc4f9cca..0000000000 --- a/changes/19014-certs-endpoints +++ /dev/null @@ -1,2 +0,0 @@ -- Adds a `GET /fleet/mdm/apple/request_csr` endpoint, which returns the signed APNS CSR needed to - activate Apple MDM. \ No newline at end of file diff --git a/changes/19052-activity-feed-webhooks b/changes/19052-activity-feed-webhooks deleted file mode 100644 index 8196bc1eac..0000000000 --- a/changes/19052-activity-feed-webhooks +++ /dev/null @@ -1 +0,0 @@ -* Added webhook for the activity feed. diff --git a/changes/19072-additional-stats b/changes/19072-additional-stats deleted file mode 100644 index 4fc4d27e24..0000000000 --- a/changes/19072-additional-stats +++ /dev/null @@ -1 +0,0 @@ -- Added additional statistics items as part of our quality telemetry. diff --git a/changes/19152-gitops-duplicate-enroll-secret b/changes/19152-gitops-duplicate-enroll-secret deleted file mode 100644 index c59a690f78..0000000000 --- a/changes/19152-gitops-duplicate-enroll-secret +++ /dev/null @@ -1 +0,0 @@ -`fleetctl gitops --dry-run` now errors on duplicate (or conflicting) global/team enroll secrets. diff --git a/changes/19171-host-query-bug-fixes b/changes/19171-host-query-bug-fixes deleted file mode 100644 index feb8546734..0000000000 --- a/changes/19171-host-query-bug-fixes +++ /dev/null @@ -1 +0,0 @@ -- Fix host query page styling bugs diff --git a/changes/19179-bm b/changes/19179-bm deleted file mode 100644 index 1871fa0e9c..0000000000 --- a/changes/19179-bm +++ /dev/null @@ -1 +0,0 @@ -* Added new endpoints to configure ABM keypairs and tokens diff --git a/changes/19267-bugfix-ui-wipe-menu b/changes/19267-bugfix-ui-wipe-menu deleted file mode 100644 index d59372b2c2..0000000000 --- a/changes/19267-bugfix-ui-wipe-menu +++ /dev/null @@ -1,3 +0,0 @@ -- Fixed UI bug where "Wipe" action was not being hidden from observers (note: this is only a - frontend bug and any observer that attempted to perform this action would be forbidden by the - backend). diff --git a/changes/19272-live-query-lag b/changes/19272-live-query-lag deleted file mode 100644 index 64c41cf945..0000000000 --- a/changes/19272-live-query-lag +++ /dev/null @@ -1 +0,0 @@ -Live queries now work via UI with large (~1 second) replication lag (for master-replica DB setup). diff --git a/changes/19311-scep-renew b/changes/19311-scep-renew deleted file mode 100644 index 7d0bf4ecb9..0000000000 --- a/changes/19311-scep-renew +++ /dev/null @@ -1 +0,0 @@ -* Fixed a bug that might prevent enqueing commands to renew SCEP certificates if the host was enrolled more than once. diff --git a/changes/19464-private-key-errors b/changes/19464-private-key-errors deleted file mode 100644 index ddd0fd2f65..0000000000 --- a/changes/19464-private-key-errors +++ /dev/null @@ -1 +0,0 @@ -- Adds clearer error messages when attempting to set up Apple MDM without a server private key configured. \ No newline at end of file diff --git a/changes/add-tuxedo-os b/changes/add-tuxedo-os deleted file mode 100644 index ca21a9cd63..0000000000 --- a/changes/add-tuxedo-os +++ /dev/null @@ -1 +0,0 @@ -* Added Tuxedo OS to the Linux distribution platform list. diff --git a/changes/issue-18847-add-ui-activities-for-self-service b/changes/issue-18847-add-ui-activities-for-self-service deleted file mode 100644 index d3c82f980f..0000000000 --- a/changes/issue-18847-add-ui-activities-for-self-service +++ /dev/null @@ -1 +0,0 @@ -- add UI for the global and host activities for self-service software installation diff --git a/changes/jve-fix-lock-script-typo b/changes/jve-fix-lock-script-typo deleted file mode 100644 index bc314a8baf..0000000000 --- a/changes/jve-fix-lock-script-typo +++ /dev/null @@ -1 +0,0 @@ -- Removes references to Administrator accounts in the comments of the Windows lock script. \ No newline at end of file diff --git a/changes/jve-pk-docs b/changes/jve-pk-docs deleted file mode 100644 index 5d404722ba..0000000000 --- a/changes/jve-pk-docs +++ /dev/null @@ -1,2 +0,0 @@ -- Updates the private key requirements to allow keys longer than 32 bytes -- Adds documentation around the new `FLEET_SERVER_PRIVATE_KEY` var \ No newline at end of file diff --git a/changes/post-apns-cert b/changes/post-apns-cert deleted file mode 100644 index a68cbeba1a..0000000000 --- a/changes/post-apns-cert +++ /dev/null @@ -1,2 +0,0 @@ -- Adds 2 new endpoints: `POST` and `DELETE /fleet/mdm/apple/apns_certificate`. These endpoints let - users manage APNS certificates in Fleet. \ No newline at end of file diff --git a/changes/save-certs-encrypted b/changes/save-certs-encrypted deleted file mode 100644 index a3955706e0..0000000000 --- a/changes/save-certs-encrypted +++ /dev/null @@ -1,2 +0,0 @@ -- Adds a new Fleet server config variable, `FLEET_SERVER_PRIVATE_KEY`. This variable contains the - private key used to encrypt the MDM certificates and keys stored in Fleet. \ No newline at end of file diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index a5657baa16..3f7e219e6a 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -8,7 +8,7 @@ version: v6.0.2 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.50.2 +appVersion: v4.51.0 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index fa79563ac2..0643a710c4 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -2,7 +2,7 @@ # All settings related to how Fleet is deployed in Kubernetes hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy -imageTag: v4.50.2 # Version of Fleet to deploy +imageTag: v4.51.0 # 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 5fd2ad6def..9e267bcf97 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.50.2" + default = "fleetdm/fleet:v4.51.0" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index c8912229ea..4da8bf454b 100644 --- a/infrastructure/dogfood/terraform/gcp/variables.tf +++ b/infrastructure/dogfood/terraform/gcp/variables.tf @@ -68,5 +68,5 @@ variable "redis_mem" { } variable "image" { - default = "fleet:v4.50.2" + default = "fleet:v4.51.0" } diff --git a/terraform/README.md b/terraform/README.md index 969ebb469d..da376de755 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -75,7 +75,7 @@ No resources. | [alb\_config](#input\_alb\_config) | n/a |
object({
name = optional(string, "fleet")
security_groups = optional(list(string), [])
access_logs = optional(map(string), {})
allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
allowed_ipv6_cidrs = optional(list(string), ["::/0"])
egress_cidrs = optional(list(string), ["0.0.0.0/0"])
egress_ipv6_cidrs = optional(list(string), ["::/0"])
extra_target_groups = optional(any, [])
https_listener_rules = optional(any, [])
tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01")
idle_timeout = optional(number, 60)
})
| `{}` | no | | [certificate\_arn](#input\_certificate\_arn) | n/a | `string` | n/a | yes | | [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module |
object({
autoscaling_capacity_providers = optional(any, {})
cluster_configuration = optional(any, {
execute_command_configuration = {
logging = "OVERRIDE"
log_configuration = {
cloud_watch_log_group_name = "/aws/ecs/aws-ec2"
}
}
})
cluster_name = optional(string, "fleet")
cluster_settings = optional(map(string), {
"name" : "containerInsights",
"value" : "enabled",
})
create = optional(bool, true)
default_capacity_provider_use_fargate = optional(bool, true)
fargate_capacity_providers = optional(any, {
FARGATE = {
default_capacity_provider_strategy = {
weight = 100
}
}
FARGATE_SPOT = {
default_capacity_provider_strategy = {
weight = 0
}
}
})
tags = optional(map(string))
})
|
{
"autoscaling_capacity_providers": {},
"cluster_configuration": {
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "/aws/ecs/aws-ec2"
},
"logging": "OVERRIDE"
}
},
"cluster_name": "fleet",
"cluster_settings": {
"name": "containerInsights",
"value": "enabled"
},
"create": true,
"default_capacity_provider_use_fargate": true,
"fargate_capacity_providers": {
"FARGATE": {
"default_capacity_provider_strategy": {
"weight": 100
}
},
"FARGATE_SPOT": {
"default_capacity_provider_strategy": {
"weight": 0
}
}
},
"tags": {}
}
| no | -| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.50.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
private_key_secret_name = optional(string, "fleet-server-private-key")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"private_key_secret_name": "fleet-server-private-key",
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | +| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.51.0")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
private_key_secret_name = optional(string, "fleet-server-private-key")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"private_key_secret_name": "fleet-server-private-key",
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | | [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. |
object({
mem = number
cpu = number
})
|
{
"cpu": 1024,
"mem": 2048
}
| no | | [rds\_config](#input\_rds\_config) | The config for the terraform-aws-modules/rds-aurora/aws module |
object({
name = optional(string, "fleet")
engine_version = optional(string, "8.0.mysql_aurora.3.04.2")
instance_class = optional(string, "db.t4g.large")
subnets = optional(list(string), [])
allowed_security_groups = optional(list(string), [])
allowed_cidr_blocks = optional(list(string), [])
apply_immediately = optional(bool, true)
monitoring_interval = optional(number, 10)
db_parameter_group_name = optional(string)
db_parameters = optional(map(string), {})
db_cluster_parameter_group_name = optional(string)
db_cluster_parameters = optional(map(string), {})
enabled_cloudwatch_logs_exports = optional(list(string), [])
master_username = optional(string, "fleet")
snapshot_identifier = optional(string)
cluster_tags = optional(map(string), {})
})
|
{
"allowed_cidr_blocks": [],
"allowed_security_groups": [],
"apply_immediately": true,
"cluster_tags": {},
"db_cluster_parameter_group_name": null,
"db_cluster_parameters": {},
"db_parameter_group_name": null,
"db_parameters": {},
"enabled_cloudwatch_logs_exports": [],
"engine_version": "8.0.mysql_aurora.3.04.2",
"instance_class": "db.t4g.large",
"master_username": "fleet",
"monitoring_interval": 10,
"name": "fleet",
"snapshot_identifier": null,
"subnets": []
}
| no | | [redis\_config](#input\_redis\_config) | n/a |
object({
name = optional(string, "fleet")
replication_group_id = optional(string)
elasticache_subnet_group_name = optional(string)
allowed_security_group_ids = optional(list(string), [])
subnets = optional(list(string))
availability_zones = optional(list(string))
cluster_size = optional(number, 3)
instance_type = optional(string, "cache.m5.large")
apply_immediately = optional(bool, true)
automatic_failover_enabled = optional(bool, false)
engine_version = optional(string, "6.x")
family = optional(string, "redis6.x")
at_rest_encryption_enabled = optional(bool, true)
transit_encryption_enabled = optional(bool, true)
parameter = optional(list(object({
name = string
value = string
})), [])
log_delivery_configuration = optional(list(map(any)), [])
tags = optional(map(string), {})
})
|
{
"allowed_security_group_ids": [],
"apply_immediately": true,
"at_rest_encryption_enabled": true,
"automatic_failover_enabled": false,
"availability_zones": null,
"cluster_size": 3,
"elasticache_subnet_group_name": null,
"engine_version": "6.x",
"family": "redis6.x",
"instance_type": "cache.m5.large",
"log_delivery_configuration": [],
"name": "fleet",
"parameter": [],
"replication_group_id": null,
"subnets": null,
"tags": {},
"transit_encryption_enabled": true
}
| no | diff --git a/terraform/addons/vuln-processing/variables.tf b/terraform/addons/vuln-processing/variables.tf index 69dd445fdf..07615950db 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.31.1") + image = optional(string, "fleetdm/fleet:v4.51.0") 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.31.1" + image = "fleetdm/fleet:v4.51.0" family = "fleet-vuln-processing" sidecars = [] extra_environment_variables = {} diff --git a/terraform/byo-vpc/README.md b/terraform/byo-vpc/README.md index 0b6e6a5c0b..131f4305cb 100644 --- a/terraform/byo-vpc/README.md +++ b/terraform/byo-vpc/README.md @@ -33,7 +33,7 @@ No requirements. |------|-------------|------|---------|:--------:| | [alb\_config](#input\_alb\_config) | n/a |
object({
name = optional(string, "fleet")
subnets = list(string)
security_groups = optional(list(string), [])
access_logs = optional(map(string), {})
certificate_arn = string
allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
allowed_ipv6_cidrs = optional(list(string), ["::/0"])
egress_cidrs = optional(list(string), ["0.0.0.0/0"])
egress_ipv6_cidrs = optional(list(string), ["::/0"])
extra_target_groups = optional(any, [])
https_listener_rules = optional(any, [])
tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01")
idle_timeout = optional(number, 60)
})
| n/a | yes | | [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module |
object({
autoscaling_capacity_providers = optional(any, {})
cluster_configuration = optional(any, {
execute_command_configuration = {
logging = "OVERRIDE"
log_configuration = {
cloud_watch_log_group_name = "/aws/ecs/aws-ec2"
}
}
})
cluster_name = optional(string, "fleet")
cluster_settings = optional(map(string), {
"name" : "containerInsights",
"value" : "enabled",
})
create = optional(bool, true)
default_capacity_provider_use_fargate = optional(bool, true)
fargate_capacity_providers = optional(any, {
FARGATE = {
default_capacity_provider_strategy = {
weight = 100
}
}
FARGATE_SPOT = {
default_capacity_provider_strategy = {
weight = 0
}
}
})
tags = optional(map(string))
})
|
{
"autoscaling_capacity_providers": {},
"cluster_configuration": {
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "/aws/ecs/aws-ec2"
},
"logging": "OVERRIDE"
}
},
"cluster_name": "fleet",
"cluster_settings": {
"name": "containerInsights",
"value": "enabled"
},
"create": true,
"default_capacity_provider_use_fargate": true,
"fargate_capacity_providers": {
"FARGATE": {
"default_capacity_provider_strategy": {
"weight": 100
}
},
"FARGATE_SPOT": {
"default_capacity_provider_strategy": {
"weight": 0
}
}
},
"tags": {}
}
| no | -| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.50.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
private_key_secret_name = optional(string, "fleet-server-private-key")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"private_key_secret_name": "fleet-server-private-key",
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | +| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.51.0")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
private_key_secret_name = optional(string, "fleet-server-private-key")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"private_key_secret_name": "fleet-server-private-key",
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | | [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. |
object({
mem = number
cpu = number
})
|
{
"cpu": 1024,
"mem": 2048
}
| no | | [rds\_config](#input\_rds\_config) | The config for the terraform-aws-modules/rds-aurora/aws module |
object({
name = optional(string, "fleet")
engine_version = optional(string, "8.0.mysql_aurora.3.04.2")
instance_class = optional(string, "db.t4g.large")
subnets = optional(list(string), [])
allowed_security_groups = optional(list(string), [])
allowed_cidr_blocks = optional(list(string), [])
apply_immediately = optional(bool, true)
monitoring_interval = optional(number, 10)
db_parameter_group_name = optional(string)
db_parameters = optional(map(string), {})
db_cluster_parameter_group_name = optional(string)
db_cluster_parameters = optional(map(string), {})
enabled_cloudwatch_logs_exports = optional(list(string), [])
master_username = optional(string, "fleet")
snapshot_identifier = optional(string)
cluster_tags = optional(map(string), {})
preferred_maintenance_window = optional(string, "thu:23:00-fri:00:00")
})
|
{
"allowed_cidr_blocks": [],
"allowed_security_groups": [],
"apply_immediately": true,
"cluster_tags": {},
"db_cluster_parameter_group_name": null,
"db_cluster_parameters": {},
"db_parameter_group_name": null,
"db_parameters": {},
"enabled_cloudwatch_logs_exports": [],
"engine_version": "8.0.mysql_aurora.3.04.2",
"instance_class": "db.t4g.large",
"master_username": "fleet",
"monitoring_interval": 10,
"name": "fleet",
"preferred_maintenance_window": "thu:23:00-fri:00:00",
"snapshot_identifier": null,
"subnets": []
}
| no | | [redis\_config](#input\_redis\_config) | n/a |
object({
name = optional(string, "fleet")
replication_group_id = optional(string)
elasticache_subnet_group_name = optional(string, "")
allowed_security_group_ids = optional(list(string), [])
subnets = list(string)
allowed_cidrs = list(string)
availability_zones = optional(list(string), [])
cluster_size = optional(number, 3)
instance_type = optional(string, "cache.m5.large")
apply_immediately = optional(bool, true)
automatic_failover_enabled = optional(bool, false)
engine_version = optional(string, "6.x")
family = optional(string, "redis6.x")
at_rest_encryption_enabled = optional(bool, true)
transit_encryption_enabled = optional(bool, true)
parameter = optional(list(object({
name = string
value = string
})), [])
log_delivery_configuration = optional(list(map(any)), [])
tags = optional(map(string), {})
})
|
{
"allowed_cidrs": null,
"allowed_security_group_ids": [],
"apply_immediately": true,
"at_rest_encryption_enabled": true,
"automatic_failover_enabled": false,
"availability_zones": [],
"cluster_size": 3,
"elasticache_subnet_group_name": "",
"engine_version": "6.x",
"family": "redis6.x",
"instance_type": "cache.m5.large",
"log_delivery_configuration": [],
"name": "fleet",
"parameter": [],
"replication_group_id": null,
"subnets": null,
"tags": {},
"transit_encryption_enabled": true
}
| no | diff --git a/terraform/byo-vpc/byo-db/README.md b/terraform/byo-vpc/byo-db/README.md index c1c0e0f820..d2c17644d1 100644 --- a/terraform/byo-vpc/byo-db/README.md +++ b/terraform/byo-vpc/byo-db/README.md @@ -28,7 +28,7 @@ No requirements. |------|-------------|------|---------|:--------:| | [alb\_config](#input\_alb\_config) | n/a |
object({
name = optional(string, "fleet")
subnets = list(string)
security_groups = optional(list(string), [])
access_logs = optional(map(string), {})
certificate_arn = string
allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
allowed_ipv6_cidrs = optional(list(string), ["::/0"])
egress_cidrs = optional(list(string), ["0.0.0.0/0"])
egress_ipv6_cidrs = optional(list(string), ["::/0"])
extra_target_groups = optional(any, [])
https_listener_rules = optional(any, [])
tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01")
idle_timeout = optional(number, 60)
})
| n/a | yes | | [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module |
object({
autoscaling_capacity_providers = optional(any, {})
cluster_configuration = optional(any, {
execute_command_configuration = {
logging = "OVERRIDE"
log_configuration = {
cloud_watch_log_group_name = "/aws/ecs/aws-ec2"
}
}
})
cluster_name = optional(string, "fleet")
cluster_settings = optional(map(string), {
"name" : "containerInsights",
"value" : "enabled",
})
create = optional(bool, true)
default_capacity_provider_use_fargate = optional(bool, true)
fargate_capacity_providers = optional(any, {
FARGATE = {
default_capacity_provider_strategy = {
weight = 100
}
}
FARGATE_SPOT = {
default_capacity_provider_strategy = {
weight = 0
}
}
})
tags = optional(map(string))
})
|
{
"autoscaling_capacity_providers": {},
"cluster_configuration": {
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "/aws/ecs/aws-ec2"
},
"logging": "OVERRIDE"
}
},
"cluster_name": "fleet",
"cluster_settings": {
"name": "containerInsights",
"value": "enabled"
},
"create": true,
"default_capacity_provider_use_fargate": true,
"fargate_capacity_providers": {
"FARGATE": {
"default_capacity_provider_strategy": {
"weight": 100
}
},
"FARGATE_SPOT": {
"default_capacity_provider_strategy": {
"weight": 0
}
}
},
"tags": {}
}
| no | -| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.50.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
private_key_secret_name = optional(string, "fleet-server-private-key")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"private_key_secret_name": "fleet-server-private-key",
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | +| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.51.0")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
private_key_secret_name = optional(string, "fleet-server-private-key")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"private_key_secret_name": "fleet-server-private-key",
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | | [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. |
object({
mem = number
cpu = number
})
|
{
"cpu": 1024,
"mem": 2048
}
| no | | [vpc\_id](#input\_vpc\_id) | n/a | `string` | n/a | yes | diff --git a/terraform/byo-vpc/byo-db/byo-ecs/README.md b/terraform/byo-vpc/byo-db/byo-ecs/README.md index b8bae183ae..0ee9d4b5dc 100644 --- a/terraform/byo-vpc/byo-db/byo-ecs/README.md +++ b/terraform/byo-vpc/byo-db/byo-ecs/README.md @@ -46,7 +46,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [ecs\_cluster](#input\_ecs\_cluster) | The name of the ECS cluster to use | `string` | n/a | yes | -| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.50.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
private_key_secret_name = optional(string, "fleet-server-private-key")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
})
redis = object({
address = string
use_tls = optional(bool, true)
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = object({
arn = string
})
extra_load_balancers = optional(list(any), [])
networking = object({
subnets = list(string)
security_groups = optional(list(string), null)
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balacners": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"private_key_secret_name": "fleet-server-private-key",
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | +| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.51.0")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
private_key_secret_name = optional(string, "fleet-server-private-key")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
})
redis = object({
address = string
use_tls = optional(bool, true)
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = object({
arn = string
})
extra_load_balancers = optional(list(any), [])
networking = object({
subnets = list(string)
security_groups = optional(list(string), null)
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
})
|
{
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balacners": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"private_key_secret_name": "fleet-server-private-key",
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
| no | | [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. |
object({
mem = number
cpu = number
})
|
{
"cpu": 1024,
"mem": 2048
}
| no | | [vpc\_id](#input\_vpc\_id) | n/a | `string` | `null` | no | diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf index f4301a00db..47a8f4a61b 100644 --- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf +++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf @@ -13,7 +13,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.50.2") + image = optional(string, "fleetdm/fleet:v4.51.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -97,7 +97,7 @@ variable "fleet_config" { default = { mem = 512 cpu = 256 - image = "fleetdm/fleet:v4.31.1" + image = "fleetdm/fleet:v4.51.0" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index e4821dba00..db2132225d 100644 --- a/terraform/byo-vpc/byo-db/variables.tf +++ b/terraform/byo-vpc/byo-db/variables.tf @@ -74,7 +74,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.50.2") + image = optional(string, "fleetdm/fleet:v4.51.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -172,7 +172,7 @@ variable "fleet_config" { default = { mem = 512 cpu = 256 - image = "fleetdm/fleet:v4.31.1" + image = "fleetdm/fleet:v4.51.0" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf index 597c43edb7..9175ef6bce 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.50.2" + fleet_image = "fleetdm/fleet:v4.51.0" domain_name = "example.com" } diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index 1d23bc3057..6fd1789b23 100644 --- a/terraform/byo-vpc/variables.tf +++ b/terraform/byo-vpc/variables.tf @@ -167,7 +167,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.50.2") + image = optional(string, "fleetdm/fleet:v4.51.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -265,7 +265,7 @@ variable "fleet_config" { default = { mem = 512 cpu = 256 - image = "fleetdm/fleet:v4.31.1" + image = "fleetdm/fleet:v4.51.0" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/example/main.tf b/terraform/example/main.tf index e430fd69fa..4a69b86f8d 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.50.2" - image = "fleetdm/fleet:v4.50.2" # override default to deploy the image you desire + # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.51.0" + image = "fleetdm/fleet:v4.51.0" # 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 e80d41e6b5..7b58e7fbbf 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -215,7 +215,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.50.2") + image = optional(string, "fleetdm/fleet:v4.51.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -313,7 +313,7 @@ variable "fleet_config" { default = { mem = 512 cpu = 256 - image = "fleetdm/fleet:v4.31.1" + image = "fleetdm/fleet:v4.51.0" family = "fleet" sidecars = [] depends_on = [] diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index 4ebd3b3761..2c6c831dfd 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.50.2", + "version": "v4.51.0", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" diff --git a/tools/release/publish_release.sh b/tools/release/publish_release.sh index 330f3ed8f9..9416e2b68c 100755 --- a/tools/release/publish_release.sh +++ b/tools/release/publish_release.sh @@ -743,7 +743,7 @@ if [ "$cherry_pick_resolved" = "false" ]; then prs_for_issue=`gh api repos/fleetdm/fleet/issues/$issue/timeline --paginate | jq -r '.[]' | $GREP_CMD "fleetdm/fleet/" | $GREP_CMD -oP "pulls\/\K(?:\d+)"` echo -n "https://github.com/fleetdm/fleet/issues/$issue" if [[ "$prs_for_issue" == "" ]]; then - echo -n "NO PR's found, please verify they are not missing in the issue, if no PR's were required for this ticket please reconsider adding it to this release." + echo -n " NO PR's found, please verify they are not missing in the issue, if no PR's were required for this ticket please reconsider adding it to this release." fi for val in $prs_for_issue; do echo -n " $val" From 1946fa64f074cb72e65842bdfe333dcdc5c874c3 Mon Sep 17 00:00:00 2001 From: Benjamin Edwards Date: Mon, 10 Jun 2024 13:31:50 -0400 Subject: [PATCH 022/119] update render blueprint (#19460) Update the render blueprint to also supply `FLEET_SERVER_PRIVATE_KEY` --- render.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/render.yaml b/render.yaml index 4916488818..bcf77cf770 100644 --- a/render.yaml +++ b/render.yaml @@ -8,6 +8,8 @@ services: preDeployCommand: "fleet prepare --no-prompt=true db" healthCheckPath: /healthz envVars: + - key: FLEET_SERVER_PRIVATE_KEY + generateValue: true - key: FLEET_MYSQL_ADDRESS fromService: name: fleet-mysql From 27b8a1364f1801cbaa9d8c7e2062cba27e620c62 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Mon, 10 Jun 2024 13:35:27 -0400 Subject: [PATCH 023/119] feat: new software installer and carves fields, kept original fields for backwards compat (#19597) > Related issue; #19526 # 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://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- changes/19526-installers-bucket | 2 + cmd/fleet/main.go | 25 ++- cmd/fleet/serve.go | 9 +- server/config/config.go | 236 +++++++++++++++++++--- server/config/config_test.go | 11 +- server/datastore/s3/carves.go | 3 +- server/datastore/s3/installer.go | 4 +- server/datastore/s3/s3.go | 2 +- server/datastore/s3/software_installer.go | 2 +- server/datastore/s3/testing_utils.go | 25 ++- 10 files changed, 266 insertions(+), 53 deletions(-) create mode 100644 changes/19526-installers-bucket diff --git a/changes/19526-installers-bucket b/changes/19526-installers-bucket new file mode 100644 index 0000000000..a90d901d23 --- /dev/null +++ b/changes/19526-installers-bucket @@ -0,0 +1,2 @@ +- Adds S3 config variables with a `carves_` and `software_installers` prefix, which are used to + configure buckets for those features. The existing non-prefixed variables are kept for backwards compatibility. \ No newline at end of file diff --git a/cmd/fleet/main.go b/cmd/fleet/main.go index 8a0148f50f..cf922702f3 100644 --- a/cmd/fleet/main.go +++ b/cmd/fleet/main.go @@ -73,14 +73,23 @@ func applyDevFlags(cfg *config.FleetConfig) { } cfg.S3 = config.S3Config{ - Bucket: "carves-dev", - Region: "minio", - Prefix: "dev-prefix", - EndpointURL: "localhost:9000", - AccessKeyID: "minio", - SecretAccessKey: "minio123!", - DisableSSL: true, - ForceS3PathStyle: true, + CarvesBucket: "carves-dev", + CarvesRegion: "minio", + CarvesPrefix: "dev-prefix", + CarvesEndpointURL: "localhost:9000", + CarvesAccessKeyID: "minio", + CarvesSecretAccessKey: "minio123!", + CarvesDisableSSL: true, + CarvesForceS3PathStyle: true, + + SoftwareInstallersBucket: "software-installers-dev", + SoftwareInstallersRegion: "minio", + SoftwareInstallersPrefix: "dev-prefix", + SoftwareInstallersEndpointURL: "localhost:9000", + SoftwareInstallersAccessKeyID: "minio", + SoftwareInstallersSecretAccessKey: "minio123!", + SoftwareInstallersDisableSSL: true, + SoftwareInstallersForceS3PathStyle: true, } cfg.Packaging.S3 = config.S3Config{ diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 1c809160a7..d7eab65e3e 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -196,7 +196,7 @@ the way that the Fleet server works. } ds = mds - if config.S3.Bucket != "" { + if config.S3.CarvesBucket != "" { carveStore, err = s3.NewCarveStore(config.S3, ds) if err != nil { initFatal(err, "initializing S3 carvestore") @@ -691,13 +691,16 @@ the way that the Fleet server works. var softwareInstallStore fleet.SoftwareInstallerStore if license.IsPremium() { profileMatcher := apple_mdm.NewProfileMatcher(redisPool) - if config.S3.Bucket != "" { + if config.S3.SoftwareInstallersBucket != "" { + if config.S3.BucketsAndPrefixesMatch() { + level.Warn(logger).Log("msg", "the S3 buckets and prefixes for carves and software installers appear to be identical, this can cause issues") + } store, err := s3.NewSoftwareInstallerStore(config.S3) if err != nil { initFatal(err, "initializing S3 software installer store") } softwareInstallStore = store - level.Info(logger).Log("msg", "using S3 software installer store", "bucket", config.S3.Bucket) + level.Info(logger).Log("msg", "using S3 software installer store", "bucket", config.S3.SoftwareInstallersBucket) } else { installerDir := os.TempDir() if dir := os.Getenv("FLEET_SOFTWARE_INSTALLER_STORE_DIR"); dir != "" { diff --git a/server/config/config.go b/server/config/config.go index 5cb401752e..feca80cefb 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -301,6 +301,120 @@ type S3Config struct { StsExternalID string `yaml:"sts_external_id"` DisableSSL bool `yaml:"disable_ssl"` ForceS3PathStyle bool `yaml:"force_s3_path_style"` + + CarvesBucket string `yaml:"carves_bucket"` + CarvesPrefix string `yaml:"carves_prefix"` + CarvesRegion string `yaml:"carves_region"` + CarvesEndpointURL string `yaml:"carves_endpoint_url"` + CarvesAccessKeyID string `yaml:"carves_access_key_id"` + CarvesSecretAccessKey string `yaml:"carves_secret_access_key"` + CarvesStsAssumeRoleArn string `yaml:"carves_sts_assume_role_arn"` + CarvesStsExternalID string `yaml:"carves_sts_external_id"` + CarvesDisableSSL bool `yaml:"carves_disable_ssl"` + CarvesForceS3PathStyle bool `yaml:"carves_force_s3_path_style"` + + SoftwareInstallersBucket string `yaml:"software_installers_bucket"` + SoftwareInstallersPrefix string `yaml:"software_installers_prefix"` + SoftwareInstallersRegion string `yaml:"software_installers_region"` + SoftwareInstallersEndpointURL string `yaml:"software_installers_endpoint_url"` + SoftwareInstallersAccessKeyID string `yaml:"software_installers_access_key_id"` + SoftwareInstallersSecretAccessKey string `yaml:"software_installers_secret_access_key"` + SoftwareInstallersStsAssumeRoleArn string `yaml:"software_installers_sts_assume_role_arn"` + SoftwareInstallersStsExternalID string `yaml:"software_installers_sts_external_id"` + SoftwareInstallersDisableSSL bool `yaml:"software_installers_disable_ssl"` + SoftwareInstallersForceS3PathStyle bool `yaml:"software_installers_force_s3_path_style"` +} + +func (s S3Config) BucketsAndPrefixesMatch() bool { + cb := s.CarvesBucket + if cb == "" { + cb = s.Bucket + } + + cp := s.CarvesPrefix + if cp == "" { + cp = s.Prefix + } + + return s.SoftwareInstallersBucket == cb && s.SoftwareInstallersPrefix == cp +} + +func (s S3Config) SoftwareInstallersToInternalCfg() S3ConfigInternal { + return S3ConfigInternal{ + Bucket: s.SoftwareInstallersBucket, + Prefix: s.SoftwareInstallersPrefix, + Region: s.SoftwareInstallersRegion, + EndpointURL: s.SoftwareInstallersEndpointURL, + AccessKeyID: s.SoftwareInstallersAccessKeyID, + SecretAccessKey: s.SoftwareInstallersSecretAccessKey, + StsAssumeRoleArn: s.SoftwareInstallersStsAssumeRoleArn, + StsExternalID: s.SoftwareInstallersStsExternalID, + DisableSSL: s.SoftwareInstallersDisableSSL, + ForceS3PathStyle: s.SoftwareInstallersForceS3PathStyle, + } +} + +// CarvesToInternalCfg creates an internal S3 config struct from the ingested S3 config. Note: we +// fall back to the deprecated fields without the `carves_` prefix for backwards compatibility. +func (s S3Config) CarvesToInternalCfg() S3ConfigInternal { + var internal S3ConfigInternal + + internal.Bucket = s.CarvesBucket + if s.CarvesBucket == "" { + internal.Bucket = s.Bucket + } + internal.Prefix = s.CarvesPrefix + if s.CarvesPrefix == "" { + internal.Prefix = s.Prefix + } + internal.Region = s.CarvesRegion + if s.CarvesRegion == "" { + internal.Region = s.Region + } + internal.EndpointURL = s.CarvesEndpointURL + if s.CarvesEndpointURL == "" { + internal.EndpointURL = s.EndpointURL + } + internal.AccessKeyID = s.CarvesAccessKeyID + if s.CarvesAccessKeyID == "" { + internal.AccessKeyID = s.AccessKeyID + } + internal.SecretAccessKey = s.CarvesSecretAccessKey + if s.CarvesSecretAccessKey == "" { + internal.SecretAccessKey = s.SecretAccessKey + } + internal.StsAssumeRoleArn = s.CarvesStsAssumeRoleArn + if s.CarvesStsAssumeRoleArn == "" { + internal.StsAssumeRoleArn = s.StsAssumeRoleArn + } + internal.StsExternalID = s.CarvesStsExternalID + if s.CarvesStsExternalID == "" { + internal.StsExternalID = s.StsExternalID + } + internal.DisableSSL = s.CarvesDisableSSL + if s.CarvesDisableSSL == false { + internal.DisableSSL = s.DisableSSL + } + internal.ForceS3PathStyle = s.CarvesForceS3PathStyle + if s.CarvesForceS3PathStyle == false { + internal.ForceS3PathStyle = s.ForceS3PathStyle + } + + return internal +} + +// S3ConfigInternal is used internally +type S3ConfigInternal struct { + Bucket string + Prefix string + Region string + EndpointURL string + AccessKeyID string + SecretAccessKey string + StsAssumeRoleArn string + StsExternalID string + DisableSSL bool + ForceS3PathStyle bool } // PubSubConfig defines configs the for Google PubSub logging plugin @@ -867,10 +981,7 @@ func (man Manager) addConfigs() { man.addConfigString("server.private_key", "", "Used for encrypting sensitive data, such as MDM certificates.") // Hide the sandbox flag as we don't want it to be discoverable for users for now - sandboxFlag := man.command.PersistentFlags().Lookup(flagNameFromConfigKey("server.sandbox_enabled")) - if sandboxFlag != nil { - sandboxFlag.Hidden = true - } + man.hideConfig("server.sandbox_enabled") // Auth man.addConfigInt("auth.bcrypt_cost", 12, @@ -1022,17 +1133,57 @@ func (man Manager) addConfigs() { man.addConfigString("lambda.audit_function", "", "Lambda function name for audit logs") + // S3 for file carving: Deprecated + man.addConfigString("s3.bucket", "", "Deprecated: Bucket where to store file carves") + man.addConfigString("s3.prefix", "", "Deprecated: Prefix under which carves are stored") + man.addConfigString("s3.region", "", "Deprecated: AWS Region (if blank region is derived)") + man.addConfigString("s3.endpoint_url", "", "Deprecated: AWS Service Endpoint to use (leave blank for default service endpoints)") + man.addConfigString("s3.access_key_id", "", "Deprecated: Access Key ID for AWS authentication") + man.addConfigString("s3.secret_access_key", "", "Deprecated: Secret Access Key for AWS authentication") + man.addConfigString("s3.sts_assume_role_arn", "", "Deprecated: ARN of role to assume for AWS") + man.addConfigString("s3.sts_external_id", "", "Deprecated: Optional unique identifier that can be used by the principal assuming the role to assert its identity.") + man.addConfigBool("s3.disable_ssl", false, "Deprecated: Disable SSL (typically for local testing)") + man.addConfigBool("s3.force_s3_path_style", false, "Deprecated: Set this to true to force path-style addressing, i.e., `http://s3.amazonaws.com/BUCKET/KEY`") + + // Hide deprecated S3 config options + for _, c := range []string{ + "s3.bucket", + "s3.prefix", + "s3.region", + "s3.endpoint_url", + "s3.access_key_id", + "s3.secret_access_key", + "s3.sts_assume_role_arn", + "s3.sts_external_id", + "s3.disable_ssl", + "s3.force_s3_path_style", + } { + man.hideConfig(c) + } + // S3 for file carving - man.addConfigString("s3.bucket", "", "Bucket where to store file carves") - man.addConfigString("s3.prefix", "", "Prefix under which carves are stored") - man.addConfigString("s3.region", "", "AWS Region (if blank region is derived)") - man.addConfigString("s3.endpoint_url", "", "AWS Service Endpoint to use (leave blank for default service endpoints)") - man.addConfigString("s3.access_key_id", "", "Access Key ID for AWS authentication") - man.addConfigString("s3.secret_access_key", "", "Secret Access Key for AWS authentication") - man.addConfigString("s3.sts_assume_role_arn", "", "ARN of role to assume for AWS") - man.addConfigString("s3.sts_external_id", "", "Optional unique identifier that can be used by the principal assuming the role to assert its identity.") - man.addConfigBool("s3.disable_ssl", false, "Disable SSL (typically for local testing)") - man.addConfigBool("s3.force_s3_path_style", false, "Set this to true to force path-style addressing, i.e., `http://s3.amazonaws.com/BUCKET/KEY`") + man.addConfigString("s3.carves_bucket", "", "Bucket where to store file carves") + man.addConfigString("s3.carves_prefix", "", "Prefix under which carves are stored") + man.addConfigString("s3.carves_region", "", "AWS Region (if blank region is derived)") + man.addConfigString("s3.carves_endpoint_url", "", "AWS Service Endpoint to use (leave blank for default service endpoints)") + man.addConfigString("s3.carves_access_key_id", "", "Access Key ID for AWS authentication") + man.addConfigString("s3.carves_secret_access_key", "", "Secret Access Key for AWS authentication") + man.addConfigString("s3.carves_sts_assume_role_arn", "", "ARN of role to assume for AWS") + man.addConfigString("s3.carves_sts_external_id", "", "Optional unique identifier that can be used by the principal assuming the role to assert its identity.") + man.addConfigBool("s3.carves_disable_ssl", false, "Disable SSL (typically for local testing)") + man.addConfigBool("s3.carves_force_s3_path_style", false, "Set this to true to force path-style addressing, i.e., `http://s3.amazonaws.com/BUCKET/KEY`") + + // S3 for software installers + man.addConfigString("s3.software_installers_bucket", "", "Bucket where to store uploaded software installers") + man.addConfigString("s3.software_installers_prefix", "", "Prefix under which software installers are stored") + man.addConfigString("s3.software_installers_region", "", "AWS Region (if blank region is derived)") + man.addConfigString("s3.software_installers_endpoint_url", "", "AWS Service Endpoint to use (leave blank for default service endpoints)") + man.addConfigString("s3.software_installers_access_key_id", "", "Access Key ID for AWS authentication") + man.addConfigString("s3.software_installers_secret_access_key", "", "Secret Access Key for AWS authentication") + man.addConfigString("s3.software_installers_sts_assume_role_arn", "", "ARN of role to assume for AWS") + man.addConfigString("s3.software_installers_sts_external_id", "", "Optional unique identifier that can be used by the principal assuming the role to assert its identity.") + man.addConfigBool("s3.software_installers_disable_ssl", false, "Disable SSL (typically for local testing)") + man.addConfigBool("s3.software_installers_force_s3_path_style", false, "Set this to true to force path-style addressing, i.e., `http://s3.amazonaws.com/BUCKET/KEY`") // PubSub man.addConfigString("pubsub.project", "", "Google Cloud Project to use") @@ -1161,6 +1312,13 @@ func (man Manager) addConfigs() { } } +func (man Manager) hideConfig(name string) { + flag := man.command.PersistentFlags().Lookup(flagNameFromConfigKey(name)) + if flag != nil { + flag.Hidden = true + } +} + // LoadConfig will load the config variables into a fully initialized // FleetConfig struct func (man Manager) LoadConfig() FleetConfig { @@ -1311,18 +1469,7 @@ func (man Manager) LoadConfig() FleetConfig { StsAssumeRoleArn: man.getConfigString("lambda.sts_assume_role_arn"), StsExternalID: man.getConfigString("lambda.sts_external_id"), }, - S3: S3Config{ - Bucket: man.getConfigString("s3.bucket"), - Prefix: man.getConfigString("s3.prefix"), - Region: man.getConfigString("s3.region"), - EndpointURL: man.getConfigString("s3.endpoint_url"), - AccessKeyID: man.getConfigString("s3.access_key_id"), - SecretAccessKey: man.getConfigString("s3.secret_access_key"), - StsAssumeRoleArn: man.getConfigString("s3.sts_assume_role_arn"), - StsExternalID: man.getConfigString("s3.sts_external_id"), - DisableSSL: man.getConfigBool("s3.disable_ssl"), - ForceS3PathStyle: man.getConfigBool("s3.force_s3_path_style"), - }, + S3: man.loadS3Config(), Email: EmailConfig{ EmailBackend: man.getConfigString("email.backend"), }, @@ -1442,6 +1589,43 @@ func (man Manager) LoadConfig() FleetConfig { return cfg } +func (man Manager) loadS3Config() S3Config { + return S3Config{ + CarvesBucket: man.getConfigString("s3.carves_bucket"), + CarvesPrefix: man.getConfigString("s3.carves_prefix"), + CarvesRegion: man.getConfigString("s3.carves_region"), + CarvesEndpointURL: man.getConfigString("s3.carves_endpoint_url"), + CarvesAccessKeyID: man.getConfigString("s3.carves_access_key_id"), + CarvesSecretAccessKey: man.getConfigString("s3.carves_secret_access_key"), + CarvesStsAssumeRoleArn: man.getConfigString("s3.carves_sts_assume_role_arn"), + CarvesStsExternalID: man.getConfigString("s3.carves_sts_external_id"), + CarvesDisableSSL: man.getConfigBool("s3.carves_disable_ssl"), + CarvesForceS3PathStyle: man.getConfigBool("s3.carves_force_s3_path_style"), + + Bucket: man.getConfigString("s3.bucket"), + Prefix: man.getConfigString("s3.prefix"), + Region: man.getConfigString("s3.region"), + EndpointURL: man.getConfigString("s3.endpoint_url"), + AccessKeyID: man.getConfigString("s3.access_key_id"), + SecretAccessKey: man.getConfigString("s3.secret_access_key"), + StsAssumeRoleArn: man.getConfigString("s3.sts_assume_role_arn"), + StsExternalID: man.getConfigString("s3.sts_external_id"), + DisableSSL: man.getConfigBool("s3.disable_ssl"), + ForceS3PathStyle: man.getConfigBool("s3.force_s3_path_style"), + + SoftwareInstallersBucket: man.getConfigString("s3.software_installers_bucket"), + SoftwareInstallersPrefix: man.getConfigString("s3.software_installers_prefix"), + SoftwareInstallersRegion: man.getConfigString("s3.software_installers_region"), + SoftwareInstallersEndpointURL: man.getConfigString("s3.software_installers_endpoint_url"), + SoftwareInstallersAccessKeyID: man.getConfigString("s3.software_installers_access_key_id"), + SoftwareInstallersSecretAccessKey: man.getConfigString("s3.software_installers_secret_access_key"), + SoftwareInstallersStsAssumeRoleArn: man.getConfigString("s3.software_installers_sts_assume_role_arn"), + SoftwareInstallersStsExternalID: man.getConfigString("s3.software_installers_sts_external_id"), + SoftwareInstallersDisableSSL: man.getConfigBool("s3.software_installers_disable_ssl"), + SoftwareInstallersForceS3PathStyle: man.getConfigBool("s3.software_installers_force_s3_path_style"), + } +} + // IsSet determines whether a given config key has been explicitly set by any // of the configuration sources. If false, the default value is being used. func (man Manager) IsSet(key string) bool { diff --git a/server/config/config_test.go b/server/config/config_test.go index b7baa4bb93..c2df315840 100644 --- a/server/config/config_test.go +++ b/server/config/config_test.go @@ -60,13 +60,22 @@ func TestConfigRoundtrip(t *testing.T) { case "AsyncHostCollectInterval", "AsyncHostCollectLockTimeout": // supports a duration or per-task config key_v.SetString("30s") + // These are deprecated field names in the S3 config. Set them to zero value, which leads to the new fields being populated instead. + case "Bucket", "Prefix", "Region", "EndpointURL", "AccessKeyID", "SecretAccessKey", "StsAssumeRoleArn", "StsExternalID": + key_v.SetString("") default: key_v.SetString(v.Elem().Type().Field(conf_index).Name + "_" + conf_v.Type().Field(key_index).Name) } case int: key_v.SetInt(int64(conf_index*100 + key_index)) case bool: - key_v.SetBool(true) + switch conf_v.Type().Field(key_index).Name { + // These are deprecated field names in the S3 config. Set them to zero value, which leads to the new fields being populated instead. + case "DisableSSL", "ForceS3PathStyle": + key_v.SetBool(false) + default: + key_v.SetBool(true) + } case time.Duration: d := time.Duration(conf_index*100 + key_index) key_v.Set(reflect.ValueOf(d)) diff --git a/server/datastore/s3/carves.go b/server/datastore/s3/carves.go index 5494e2c958..7aae963301 100644 --- a/server/datastore/s3/carves.go +++ b/server/datastore/s3/carves.go @@ -35,7 +35,7 @@ type CarveStore struct { // NewCarveStore creates a new store with the given config func NewCarveStore(config config.S3Config, metadatadb fleet.CarveStore) (*CarveStore, error) { - s3store, err := newS3store(config) + s3store, err := newS3store(config.CarvesToInternalCfg()) if err != nil { return nil, err } @@ -57,7 +57,6 @@ func (c *CarveStore) NewCarve(ctx context.Context, metadata *fleet.CarveMetadata Bucket: &c.bucket, Key: &objectKey, }) - if err != nil { // even if we fail to create the multipart upload, we still want to create // the carve in the database and register an error, this way the user can diff --git a/server/datastore/s3/installer.go b/server/datastore/s3/installer.go index 8d25ee4cb9..2df34b3359 100644 --- a/server/datastore/s3/installer.go +++ b/server/datastore/s3/installer.go @@ -37,7 +37,7 @@ type InstallerStore struct { // NewInstallerStore creates a new instance with the given S3 config func NewInstallerStore(config config.S3Config) (*InstallerStore, error) { - s3store, err := newS3store(config) + s3store, err := newS3store(config.CarvesToInternalCfg()) if err != nil { return nil, err } @@ -48,7 +48,6 @@ func NewInstallerStore(config config.S3Config) (*InstallerStore, error) { func (i *InstallerStore) Get(ctx context.Context, installer fleet.Installer) (io.ReadCloser, int64, error) { key := i.keyForInstaller(installer) req, err := i.s3client.GetObject(&s3.GetObjectInput{Bucket: &i.bucket, Key: &key}) - if err != nil { if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { @@ -78,7 +77,6 @@ func (i *InstallerStore) Put(ctx context.Context, installer fleet.Installer) (st func (i *InstallerStore) Exists(ctx context.Context, installer fleet.Installer) (bool, error) { key := i.keyForInstaller(installer) _, err := i.s3client.HeadObject(&s3.HeadObjectInput{Bucket: &i.bucket, Key: &key}) - if err != nil { if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { diff --git a/server/datastore/s3/s3.go b/server/datastore/s3/s3.go index 394da53ffa..0c67a64390 100644 --- a/server/datastore/s3/s3.go +++ b/server/datastore/s3/s3.go @@ -23,7 +23,7 @@ type s3store struct { } // newS3store initializes an S3 Datastore -func newS3store(config config.S3Config) (*s3store, error) { +func newS3store(config config.S3ConfigInternal) (*s3store, error) { conf := &aws.Config{} // Use default auth provire if no static credentials were provided diff --git a/server/datastore/s3/software_installer.go b/server/datastore/s3/software_installer.go index 1966326266..5b33d10127 100644 --- a/server/datastore/s3/software_installer.go +++ b/server/datastore/s3/software_installer.go @@ -21,7 +21,7 @@ type SoftwareInstallerStore struct { // NewSoftwareInstallerStore creates a new instance with the given S3 config. func NewSoftwareInstallerStore(config config.S3Config) (*SoftwareInstallerStore, error) { - s3store, err := newS3store(config) + s3store, err := newS3store(config.SoftwareInstallersToInternalCfg()) if err != nil { return nil, err } diff --git a/server/datastore/s3/testing_utils.go b/server/datastore/s3/testing_utils.go index 50e02f5758..7854d3b34f 100644 --- a/server/datastore/s3/testing_utils.go +++ b/server/datastore/s3/testing_utils.go @@ -42,14 +42,23 @@ func setupTestStore[T testBucketCreator](tb testing.TB, bucket, prefix string, n checkEnv(tb) store, err := newFn(config.S3Config{ - Bucket: bucket, - Prefix: prefix, - Region: "minio", - EndpointURL: testEndpoint, - AccessKeyID: accessKeyID, - SecretAccessKey: secretAccessKey, - ForceS3PathStyle: true, - DisableSSL: true, + SoftwareInstallersBucket: bucket, + SoftwareInstallersPrefix: prefix, + SoftwareInstallersRegion: "minio", + SoftwareInstallersEndpointURL: testEndpoint, + SoftwareInstallersAccessKeyID: accessKeyID, + SoftwareInstallersSecretAccessKey: secretAccessKey, + SoftwareInstallersForceS3PathStyle: true, + SoftwareInstallersDisableSSL: true, + + CarvesBucket: bucket, + CarvesPrefix: prefix, + CarvesRegion: "minio", + CarvesEndpointURL: testEndpoint, + CarvesAccessKeyID: accessKeyID, + CarvesSecretAccessKey: secretAccessKey, + CarvesForceS3PathStyle: true, + CarvesDisableSSL: true, }) require.Nil(tb, err) From 92198a22b889b8150eddeb8111c2c8a20eaeff33 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:46:16 -0700 Subject: [PATCH 024/119] Delete team policies: 404 for nonexistent team (#19516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #18993 - Return `404` when a user tries to delete team policies from a non-existent team – see [this precedent in the codebase](https://github.com/fleetdm/fleet/blob/6b3310aa51bbc54282a0f2cd7d527997448c961f/server/service/integration_core_test.go#L6212) for a 404 in this situation - Add missing authorization check for this action Screenshot 2024-06-04 at 6 22 02 PM - [x] Changes file added for user-visible changes in `changes/`, - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- ...3-404-when-no-team-on-delete-team-policies | 1 + server/service/integration_core_test.go | 3 +++ server/service/team_policies.go | 19 ++++++++++++------- 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 changes/18993-404-when-no-team-on-delete-team-policies diff --git a/changes/18993-404-when-no-team-on-delete-team-policies b/changes/18993-404-when-no-team-on-delete-team-policies new file mode 100644 index 0000000000..98a8b3e171 --- /dev/null +++ b/changes/18993-404-when-no-team-on-delete-team-policies @@ -0,0 +1 @@ +* Error with 404 when the user attempts to delete team policies for a non-existent team diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index a5af5c5d36..1f7b1feca3 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -6211,6 +6211,9 @@ func (s *integrationTestSuite) TestTeamPoliciesTeamNotExists() { teamPoliciesResponse := listTeamPoliciesResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", 9999999), nil, http.StatusNotFound, &teamPoliciesResponse) require.Len(t, teamPoliciesResponse.Policies, 0) + + deleteTeamPoliciesResponse := deleteTeamPoliciesResponse{} + s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/delete", 9999999), deleteTeamPoliciesRequest{IDs: []uint{1, 1000}}, http.StatusNotFound, &deleteTeamPoliciesResponse) } func (s *integrationTestSuite) TestSessionInfo() { diff --git a/server/service/team_policies.go b/server/service/team_policies.go index 62540aef94..81ebee7d40 100644 --- a/server/service/team_policies.go +++ b/server/service/team_policies.go @@ -269,6 +269,18 @@ func deleteTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fl } func (svc Service) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) { + if err := svc.authz.Authorize(ctx, &fleet.Policy{ + PolicyData: fleet.PolicyData{ + TeamID: ptr.Uint(teamID), + }, + }, fleet.ActionWrite); err != nil { + return nil, err + } + + if _, err := svc.ds.Team(ctx, teamID); err != nil { + return nil, ctxerr.Wrapf(ctx, err, "loading team %d", teamID) + } + if len(ids) == 0 { return nil, nil } @@ -277,13 +289,6 @@ func (svc Service) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []ui return nil, ctxerr.Wrap(ctx, err, "getting policies by ID") } - if err := svc.authz.Authorize(ctx, &fleet.Policy{ - PolicyData: fleet.PolicyData{ - TeamID: ptr.Uint(teamID), - }, - }, fleet.ActionWrite); err != nil { - return nil, err - } for _, policy := range policiesByID { if t := policy.PolicyData.TeamID; t == nil || *t != teamID { return nil, authz.ForbiddenWithInternal( From dd89ab6998992ad5813d918f69be3f96f3bcea12 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Mon, 10 Jun 2024 10:48:35 -0700 Subject: [PATCH 025/119] Update macos MDM migration demo script (#19621) --- it-and-security/lib/macos-mdm-migration.sh | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/it-and-security/lib/macos-mdm-migration.sh b/it-and-security/lib/macos-mdm-migration.sh index 0954f562c9..3ddab5f5eb 100755 --- a/it-and-security/lib/macos-mdm-migration.sh +++ b/it-and-security/lib/macos-mdm-migration.sh @@ -17,6 +17,8 @@ new_wallpaper_url="https://fleetdm.com/images/demo/fleet-desktop-migration.png" # Define the path where the new wallpaper will be saved new_wallpaper_path="/tmp/fleet-desktop-migration.png" +current_user=$(ls -l /dev/console | awk '{print $3}') + # Download the new wallpaper curl -o "$new_wallpaper_path" "$new_wallpaper_url" @@ -77,7 +79,7 @@ sleep 5 # Open Chrome with a specific URL and maximize the window and set full screen and unmute chrome_url="https://www.loom.com/share/e5f733b92773476690b8d4f38592b35d?t=254&sid=f993c904-bf49-40e4-b55c-26c81a91c60&autoplay=1" -osascript -e " +sudo -u "$current_user" osascript -e " tell application \"Google Chrome\" activate open location \"$chrome_url\" @@ -90,19 +92,6 @@ tell application \"Google Chrome\" end tell end tell" -# Wait for 1 second and press the "F" key -sleep 1 -osascript -e ' -tell application "System Events" - keystroke "F" -end tell' - -# Wait for 1 more second and press the "M" key -sleep 1 -osascript -e ' -tell application "System Events" - keystroke "M" -end tell' # Wait for 30 seconds sleep 30 From df44151309ff06b0f8fe62c3d8d11fbab90c3c96 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:14:50 -0400 Subject: [PATCH 026/119] [bug fix] Fleet UI: Activity readable without public IP (#19443) --- changes/19184-activity-human-readable | 1 + .../ActivityFeed/ActivityItem/ActivityItem.tests.tsx | 9 +++++++++ .../cards/ActivityFeed/ActivityItem/ActivityItem.tsx | 9 ++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 changes/19184-activity-human-readable diff --git a/changes/19184-activity-human-readable b/changes/19184-activity-human-readable new file mode 100644 index 0000000000..2021f81bca --- /dev/null +++ b/changes/19184-activity-human-readable @@ -0,0 +1 @@ +- Fix activity without public IP to be human readable diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx index 830d485425..f04e855af7 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx @@ -245,6 +245,15 @@ describe("Activity Feed", () => { screen.getByText("successfully logged in from public IP 192.168.0.1.") ).toBeInTheDocument(); }); + it("renders a user_logged_in type activity without public IP", () => { + const activity = createMockActivity({ + type: ActivityType.UserLoggedIn, + details: {}, + }); + render(); + + expect(screen.getByText("successfully logged in.")).toBeInTheDocument(); + }); it("renders a user_failed_login type activity globally", () => { const activity = createMockActivity({ diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index c3a2759329..d8deaf0fe7 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -182,7 +182,14 @@ const TAGGED_TEMPLATES = { return "was added to Fleet by SSO."; }, userLoggedIn: (activity: IActivity) => { - return `successfully logged in from public IP ${activity.details?.public_ip}.`; + return ( + <> + successfully logged in + {activity.details?.public_ip && + ` from public IP ${activity.details?.public_ip}`} + . + + ); }, userFailedLogin: (activity: IActivity) => { return ( From 4095595747a675eedce4ce6d82f066bd02ab1ef8 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:23:52 -0400 Subject: [PATCH 027/119] Update features.yml (#19622) - Remove "Separate file size options for query results vs. agent logs when using filesystem storage" (#11999) --- handbook/company/pricing-features-table.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/handbook/company/pricing-features-table.yml b/handbook/company/pricing-features-table.yml index d98d4f1038..c0b34f8ff7 100644 --- a/handbook/company/pricing-features-table.yml +++ b/handbook/company/pricing-features-table.yml @@ -921,9 +921,6 @@ productCategories: [Endpoint operations] pricingTableCategories: [Endpoint operations] buzzwords: [Real-time export,Ship logs] - waysToUse: - - description: Choose different file sizes for automated query results and agent logs. Coming soon (2024-04-22) #Customer-blanco - moreInfoUrl: https://github.com/fleetdm/fleet/issues/11999 - industryName: File carving (AWS S3) documentationUrl: https://fleetdm.com/docs/configuration/fleet-server-configuration#s-3-file-carving-backend tier: Free From a9a11e293a306a4f67d731b3c8c215ed44033b01 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Mon, 10 Jun 2024 16:03:34 -0300 Subject: [PATCH 028/119] Fixed a bug that prevented unused script contents to be cleaned up. (#19615) for #19500 # 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://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- server/datastore/mysql/scripts.go | 4 ++ server/datastore/mysql/scripts_test.go | 70 +++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/server/datastore/mysql/scripts.go b/server/datastore/mysql/scripts.go index 59e5a9235a..59afeb94af 100644 --- a/server/datastore/mysql/scripts.go +++ b/server/datastore/mysql/scripts.go @@ -1094,6 +1094,10 @@ WHERE SELECT 1 FROM host_script_results WHERE script_content_id = script_contents.id) AND NOT EXISTS ( SELECT 1 FROM scripts WHERE script_content_id = script_contents.id) + AND NOT EXISTS ( + SELECT 1 FROM software_installers si + WHERE script_contents.id IN (si.install_script_content_id, si.post_install_script_content_id) + ) ` _, err := ds.writer(ctx).ExecContext(ctx, deleteStmt) if err != nil { diff --git a/server/datastore/mysql/scripts_test.go b/server/datastore/mysql/scripts_test.go index 13c69f77ae..09736a0dd7 100644 --- a/server/datastore/mysql/scripts_test.go +++ b/server/datastore/mysql/scripts_test.go @@ -1,6 +1,7 @@ package mysql import ( + "bytes" "context" _ "embed" "fmt" @@ -1134,6 +1135,20 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) { res, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{ScriptContents: "echo something_else", SyncRequest: true}) require.NoError(t, err) + // create a software install that references scripts + swi, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + InstallScript: "install-script", + PreInstallQuery: "SELECT 1", + PostInstallScript: "post-install-script", + InstallerFile: bytes.NewReader([]byte("hello")), + StorageID: "storage1", + Filename: "file1", + Title: "file1", + Version: "1.0", + Source: "apps", + }) + require.NoError(t, err) + // delete our saved script without ever executing it require.NoError(t, ds.DeleteScript(ctx, s.ID)) @@ -1142,12 +1157,65 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) { stmt := `SELECT id, HEX(md5_checksum) as md5_checksum FROM script_contents` err = sqlx.SelectContext(ctx, ds.reader(ctx), &sc, stmt) require.NoError(t, err) - require.Len(t, sc, 2) + require.Len(t, sc, 4) // this should only remove the script_contents of the saved script, since the sync script is // still "in use" by the script execution require.NoError(t, ds.CleanupUnusedScriptContents(ctx)) + sc = []scriptContents{} + err = sqlx.SelectContext(ctx, ds.reader(ctx), &sc, stmt) + require.NoError(t, err) + require.Len(t, sc, 3) + require.ElementsMatch(t, []string{ + md5ChecksumScriptContent(res.ScriptContents), + md5ChecksumScriptContent("install-script"), + md5ChecksumScriptContent("post-install-script"), + }, []string{ + sc[0].Checksum, + sc[1].Checksum, + sc[2].Checksum, + }) + + // remove the software installer from the DB + err = ds.DeleteSoftwareInstaller(ctx, swi) + require.NoError(t, err) + + require.NoError(t, ds.CleanupUnusedScriptContents(ctx)) + + // validate that script contents still exist + sc = []scriptContents{} + err = sqlx.SelectContext(ctx, ds.reader(ctx), &sc, stmt) + require.NoError(t, err) + require.Len(t, sc, 1) + require.Equal(t, md5ChecksumScriptContent(res.ScriptContents), sc[0].Checksum) + + // create a software install without a post-install script + swi, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + PreInstallQuery: "SELECT 1", + InstallScript: "install-script", + InstallerFile: bytes.NewReader([]byte("hello")), + StorageID: "storage1", + Filename: "file1", + Title: "file1", + Version: "1.0", + Source: "apps", + }) + require.NoError(t, err) + + // run the cleanup function + require.NoError(t, ds.CleanupUnusedScriptContents(ctx)) + sc = []scriptContents{} + err = sqlx.SelectContext(ctx, ds.reader(ctx), &sc, stmt) + require.NoError(t, err) + require.Len(t, sc, 2) + + // remove the software installer from the DB + err = ds.DeleteSoftwareInstaller(ctx, swi) + require.NoError(t, err) + require.NoError(t, ds.CleanupUnusedScriptContents(ctx)) + + // validate that script contents still exist sc = []scriptContents{} err = sqlx.SelectContext(ctx, ds.reader(ctx), &sc, stmt) require.NoError(t, err) From 6a20231fc4532b897c36b1ac9e9417945c2dff42 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Mon, 10 Jun 2024 14:27:28 -0500 Subject: [PATCH 029/119] Added FLEET_CALENDAR_PERIODICITY for internal demo use. (#19559) #19491 Video demo: https://www.loom.com/share/c8fca008a9674cc685a5c209d9689271?sid=1f67e6c5-5e0b-4f10-9837-dc5d4c27f858 Changes file not added since this is an undocumented feature for internal use. New tests not created since this feature is for internal use, and will likely be removed in the near future. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [ ] Added/updated tests - [x] Manual QA for all new/changed functionality --- cmd/fleet/serve.go | 7 +++- server/config/config.go | 23 +++++++++++++ server/cron/calendar_cron.go | 33 ++++++++++++------- server/cron/calendar_cron_test.go | 13 +++++--- server/service/integration_enterprise_test.go | 5 ++- 5 files changed, 63 insertions(+), 18 deletions(-) diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index d7eab65e3e..90721bf545 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -863,7 +863,12 @@ the way that the Fleet server works. if license.IsPremium() { if err := cronSchedules.StartCronSchedule( func() (fleet.CronSchedule, error) { - return cron.NewCalendarSchedule(ctx, instanceID, ds, 5*time.Minute, logger) + if config.Calendar.Periodicity > 0 { + config.Calendar.SetAlwaysReloadEvent(true) + } else { + config.Calendar.Periodicity = 5 * time.Minute + } + return cron.NewCalendarSchedule(ctx, instanceID, ds, config.Calendar, logger) }, ); err != nil { initFatal(err, "failed to register calendar schedule") diff --git a/server/config/config.go b/server/config/config.go index feca80cefb..9e4cb6003a 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -538,6 +538,7 @@ type FleetConfig struct { Prometheus PrometheusConfig Packaging PackagingConfig MDM MDMConfig + Calendar CalendarConfig } type MDMConfig struct { @@ -612,6 +613,19 @@ type MDMConfig struct { microsoftWSTEPKeyPEM []byte } +type CalendarConfig struct { + Periodicity time.Duration + // Hide alwaysReloadEvent from YAML config + alwaysReloadEvent bool +} + +func (c *CalendarConfig) AlwaysReloadEvent() bool { + return c.alwaysReloadEvent +} +func (c *CalendarConfig) SetAlwaysReloadEvent(value bool) { + c.alwaysReloadEvent = value +} + type x509KeyPairConfig struct { certPath string certBytes []byte @@ -1298,6 +1312,12 @@ func (man Manager) addConfigs() { man.addConfigString("mdm.windows_wstep_identity_cert_bytes", "", "Microsoft WSTEP PEM-encoded certificate bytes") man.addConfigString("mdm.windows_wstep_identity_key_bytes", "", "Microsoft WSTEP PEM-encoded private key bytes") + // Calendar integration + man.addConfigDuration( + "calendar.periodicity", 0, + "How much time to wait between processing calendar integration.", + ) + // Hide Microsoft/Windows MDM flags as we don't want it to be discoverable for users for now betaMDMFlags := []string{ "mdm.windows_wstep_identity_cert", @@ -1579,6 +1599,9 @@ func (man Manager) LoadConfig() FleetConfig { WindowsWSTEPIdentityCertBytes: man.getConfigString("mdm.windows_wstep_identity_cert_bytes"), WindowsWSTEPIdentityKeyBytes: man.getConfigString("mdm.windows_wstep_identity_key_bytes"), }, + Calendar: CalendarConfig{ + Periodicity: man.getConfigDuration("calendar.periodicity"), + }, } // ensure immediately that the async config is valid for all known tasks diff --git a/server/cron/calendar_cron.go b/server/cron/calendar_cron.go index becabff31c..d4c855ebcc 100644 --- a/server/cron/calendar_cron.go +++ b/server/cron/calendar_cron.go @@ -11,6 +11,7 @@ import ( "time" "github.com/fleetdm/fleet/v4/ee/server/calendar" + "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service/schedule" @@ -23,11 +24,16 @@ const calendarConsumers = 18 const defaultDescription = "needs to make sure your device meets the organization's requirements." const defaultResolution = "During this maintenance window, you can expect updates to be applied automatically. Your device may be unavailable during this time." +type calendarConfig struct { + config.CalendarConfig + fleet.GoogleCalendarIntegration +} + func NewCalendarSchedule( ctx context.Context, instanceID string, ds fleet.Datastore, - interval time.Duration, + serverConfig config.CalendarConfig, logger kitlog.Logger, ) (*schedule.Schedule, error) { const ( @@ -35,7 +41,7 @@ func NewCalendarSchedule( ) logger = kitlog.With(logger, "cron", name) s := schedule.New( - ctx, name, instanceID, interval, ds, ds, + ctx, name, instanceID, serverConfig.Periodicity, ds, ds, schedule.WithAltLockID("calendar"), schedule.WithLogger(logger), schedule.WithJob( @@ -47,7 +53,7 @@ func NewCalendarSchedule( schedule.WithJob( "calendar_events", func(ctx context.Context) error { - return cronCalendarEvents(ctx, ds, logger) + return cronCalendarEvents(ctx, ds, serverConfig, logger) }, ), ) @@ -55,7 +61,7 @@ func NewCalendarSchedule( return s, nil } -func cronCalendarEvents(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger) error { +func cronCalendarEvents(ctx context.Context, ds fleet.Datastore, serverConfig config.CalendarConfig, logger kitlog.Logger) error { appConfig, err := ds.AppConfig(ctx) if err != nil { return fmt.Errorf("load app config: %w", err) @@ -78,9 +84,13 @@ func cronCalendarEvents(ctx context.Context, ds fleet.Datastore, logger kitlog.L return fmt.Errorf("list teams: %w", err) } + localConfig := calendarConfig{ + CalendarConfig: serverConfig, + GoogleCalendarIntegration: *googleCalendarIntegrationConfig, + } for _, team := range teams { if err := cronCalendarEventsForTeam( - ctx, ds, googleCalendarIntegrationConfig, *team, appConfig.OrgInfo.OrgName, domain, logger, + ctx, ds, localConfig, *team, appConfig.OrgInfo.OrgName, domain, logger, ); err != nil { level.Info(logger).Log("msg", "events calendar cron", "team_id", team.ID, "err", err) } @@ -101,7 +111,7 @@ func createUserCalendarFromConfig(ctx context.Context, config *fleet.GoogleCalen func cronCalendarEventsForTeam( ctx context.Context, ds fleet.Datastore, - calendarConfig *fleet.GoogleCalendarIntegration, + calendarConfig calendarConfig, team fleet.Team, orgName string, domain string, @@ -176,7 +186,7 @@ func cronCalendarEventsForTeam( // policies on one of its hosts, and possibly create a new calendar event if they have // another failing host on the same team. start := time.Now() - removeCalendarEventsFromPassingHosts(ctx, ds, calendarConfig, passingHosts, logger) + removeCalendarEventsFromPassingHosts(ctx, ds, &calendarConfig.GoogleCalendarIntegration, passingHosts, logger) level.Debug(logger).Log( "msg", "passing_hosts", "took", time.Since(start), ) @@ -201,7 +211,7 @@ func cronCalendarEventsForTeam( func processCalendarFailingHosts( ctx context.Context, ds fleet.Datastore, - calendarConfig *fleet.GoogleCalendarIntegration, + calendarConfig calendarConfig, orgName string, hosts []fleet.HostPolicyMembershipData, logger kitlog.Logger, @@ -248,7 +258,7 @@ func processCalendarFailingHosts( } } - userCalendar := createUserCalendarFromConfig(ctx, calendarConfig, logger) + userCalendar := createUserCalendarFromConfig(ctx, &calendarConfig.GoogleCalendarIntegration, logger) if err := userCalendar.Configure(host.Email); err != nil { level.Error(logger).Log("msg", "configure user calendar", "err", err) continue // continue with next host @@ -257,7 +267,7 @@ func processCalendarFailingHosts( switch { case err == nil && !expiredEvent: if err := processFailingHostExistingCalendarEvent( - ctx, ds, userCalendar, orgName, hostCalendarEvent, calendarEvent, host, &policyIDtoPolicy, logger, + ctx, ds, userCalendar, orgName, hostCalendarEvent, calendarEvent, host, &policyIDtoPolicy, calendarConfig, logger, ); err != nil { level.Info(logger).Log("msg", "process failing host existing calendar event", "err", err) continue // continue with next host @@ -313,13 +323,14 @@ func processFailingHostExistingCalendarEvent( calendarEvent *fleet.CalendarEvent, host fleet.HostPolicyMembershipData, policyIDtoPolicy *sync.Map, + calendarConfig calendarConfig, logger kitlog.Logger, ) error { updatedEvent := calendarEvent updated := false now := time.Now() - if shouldReloadCalendarEvent(now, calendarEvent, hostCalendarEvent) { + if calendarConfig.AlwaysReloadEvent() || shouldReloadCalendarEvent(now, calendarEvent, hostCalendarEvent) { var err error updatedEvent, _, err = calendar.GetAndUpdateEvent( calendarEvent, func(conflict bool) string { diff --git a/server/cron/calendar_cron_test.go b/server/cron/calendar_cron_test.go index 94c2576cd1..7a210ed884 100644 --- a/server/cron/calendar_cron_test.go +++ b/server/cron/calendar_cron_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/stretchr/testify/assert" "os" @@ -20,6 +21,8 @@ import ( "github.com/stretchr/testify/require" ) +var defaultCalendarConfig = config.CalendarConfig{Periodicity: 5 * time.Minute} + func TestGetPreferredCalendarEventDate(t *testing.T) { t.Parallel() date := func(year int, month time.Month, day int) time.Time { @@ -195,7 +198,7 @@ func TestEventForDifferentHost(t *testing.T) { return hcEvent, calEvent, nil } - err := cronCalendarEvents(ctx, ds, logger) + err := cronCalendarEvents(ctx, ds, defaultCalendarConfig, logger) require.NoError(t, err) } @@ -365,7 +368,7 @@ func TestCalendarEventsMultipleHosts(t *testing.T) { return nil, nil } - err := cronCalendarEvents(ctx, ds, logger) + err := cronCalendarEvents(ctx, ds, defaultCalendarConfig, logger) require.NoError(t, err) eventsMu.Lock() @@ -650,7 +653,7 @@ func TestCalendarEvents1KHosts(t *testing.T) { return nil, nil } - err := cronCalendarEvents(ctx, ds, logger) + err := cronCalendarEvents(ctx, ds, defaultCalendarConfig, logger) require.NoError(t, err) createdCalendarEvents := calendar.ListGoogleMockEvents() @@ -687,7 +690,7 @@ func TestCalendarEvents1KHosts(t *testing.T) { return nil } - err = cronCalendarEvents(ctx, ds, logger) + err = cronCalendarEvents(ctx, ds, defaultCalendarConfig, logger) require.NoError(t, err) createdCalendarEvents = calendar.ListGoogleMockEvents() @@ -925,7 +928,7 @@ func TestEventDescription(t *testing.T) { return nil, nil } - err := cronCalendarEvents(ctx, ds, logger) + err := cronCalendarEvents(ctx, ds, defaultCalendarConfig, logger) require.NoError(t, err) numberOfEvents := 7 diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index b1dba17e40..4009752a0f 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -25,6 +25,7 @@ import ( "github.com/fleetdm/fleet/v4/ee/server/calendar" "github.com/fleetdm/fleet/v4/pkg/optjson" + "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/cron" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/datastore/redis/redistest" @@ -84,7 +85,9 @@ func (s *integrationEnterpriseTestSuite) SetupSuite() { if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" { cronLog = kitlog.NewNopLogger() } - calendarSchedule, err = cron.NewCalendarSchedule(ctx, s.T().Name(), s.ds, 24*time.Hour, cronLog) + calendarSchedule, err = cron.NewCalendarSchedule( + ctx, s.T().Name(), s.ds, config.CalendarConfig{Periodicity: 24 * time.Hour}, cronLog, + ) return calendarSchedule, err } }, From 08c54d235bc2930b748777a58c5b15616bc77599 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Mon, 10 Jun 2024 14:27:55 -0500 Subject: [PATCH 030/119] Improved gitops test. (#19544) Some minor gitops test improvements. I was debugging a gitops read-after-write consistency issue that I ended up filing as #19543 --- cmd/fleetctl/gitops_test.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 0f943a29a8..fb2003c54b 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -719,12 +719,18 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) { ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, ) error { + assert.Empty(t, macProfiles) + assert.Empty(t, winProfiles) + return nil + } + ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { + assert.Empty(t, scripts) return nil } - ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil } ds.BulkSetPendingMDMHostProfilesFunc = func( ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string, ) error { + assert.Empty(t, profileUUIDs) return nil } ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { @@ -755,26 +761,26 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) { } ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { if tid == team.ID { - return team, nil + return savedTeam, nil } return nil, nil } ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { - if name == teamName { - return team, nil + if name == teamName && savedTeam != nil { + return savedTeam, nil } - return nil, nil + return nil, ¬FoundError{} + } + ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) { + newTeam.ID = team.ID + savedTeam = newTeam + enrolledTeamSecrets = newTeam.Secrets + return newTeam, nil } ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) { savedTeam = team return team, nil } - ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) ( - *fleet.MDMAppleDeclaration, error, - ) { - declaration.DeclarationUUID = uuid.NewString() - return declaration, nil - } ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error { return nil } From 3e19cd90a9eadfbcc8b331c3514c8d5e4370170b Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Mon, 10 Jun 2024 16:48:05 -0300 Subject: [PATCH 031/119] Log warning when hosts enroll with duplicate hardware UUIDs (#19475) #16393 - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [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 --- changes/16393-add-warning-log-duplicate-uuid | 1 + server/datastore/mysql/apple_mdm.go | 4 +- server/datastore/mysql/hosts.go | 105 ++++++++++++++----- server/datastore/mysql/hosts_test.go | 56 ++++++++++ 4 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 changes/16393-add-warning-log-duplicate-uuid diff --git a/changes/16393-add-warning-log-duplicate-uuid b/changes/16393-add-warning-log-duplicate-uuid new file mode 100644 index 0000000000..f1e8001baf --- /dev/null +++ b/changes/16393-add-warning-log-duplicate-uuid @@ -0,0 +1 @@ +* Added warning server log when hosts are enrolling with duplicate hardware identifiers. diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 17f643bd2a..4140977088 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -734,7 +734,7 @@ func ingestMDMAppleDeviceFromCheckinDB( // MDM is necessarily enabled if this gets called, always pass true for that // parameter. - matchID, _, err := matchHostDuringEnrollment(ctx, tx, mdmEnroll, true, "", mdmHost.UUID, mdmHost.HardwareSerial) + enrolledHostInfo, err := matchHostDuringEnrollment(ctx, tx, mdmEnroll, true, "", mdmHost.UUID, mdmHost.HardwareSerial) switch { case errors.Is(err, sql.ErrNoRows): return insertMDMAppleHostDB(ctx, tx, mdmHost, logger, appCfg) @@ -743,7 +743,7 @@ func ingestMDMAppleDeviceFromCheckinDB( return ctxerr.Wrap(ctx, err, "get mdm apple host by serial number or udid") default: - return updateMDMAppleHostDB(ctx, tx, matchID, mdmHost, appCfg) + return updateMDMAppleHostDB(ctx, tx, enrolledHostInfo.ID, mdmHost, appCfg) } } diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 190d65ba0c..07d80885de 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1697,6 +1697,22 @@ const ( mdmEnroll ) +// enrolledHostInfo contains information of an enrolled host to +// be used when enrolling orbit/osquery, MDM or just re-enrolling hosts +// (e.g. when a osquery.db is deleted from a host). +// +// NOTE: orbit and osquery running as part of fleetd on a device are identified +// with the same entry in the hosts table. +type enrolledHostInfo struct { + // ID is the identifier of the host. + ID uint + // LastEnrolledAt is the time the host last enrolled to Fleet. + LastEnrolledAt time.Time + // NodeKeySet indicates whether `node_key` is set (NOT NULL) for a osquery host + // or if `orbit_node_key` is set (NOT NULL) for a orbit host. + NodeKeySet bool +} + // Attempts to find the matching host ID by osqueryID, host UUID or serial // number. Any of those fields can be left empty if not available, and it will // use the best match in this order: @@ -1711,10 +1727,17 @@ const ( // able to match by serial in this scenario, since this is the only information // we get when enrolling hosts via Apple DEP) AND if the matched host is on the // macOS platform (darwin). -func matchHostDuringEnrollment(ctx context.Context, q sqlx.QueryerContext, enrollType enroll, isMDMEnabled bool, osqueryID, uuid, serial string) (uint, time.Time, error) { +func matchHostDuringEnrollment( + ctx context.Context, + q sqlx.QueryerContext, + enrollType enroll, + isMDMEnabled bool, + osqueryID, uuid, serial string, +) (*enrolledHostInfo, error) { type hostMatch struct { ID uint LastEnrolledAt time.Time `db:"last_enrolled_at"` + NodeKeySet bool `db:"node_key_set"` Priority int } @@ -1724,8 +1747,14 @@ func matchHostDuringEnrollment(ctx context.Context, q sqlx.QueryerContext, enrol rows []hostMatch ) + // For enrollType == mdmEnroll, nodeKeyColumn doesn't matter. + nodeKeyColumn := "node_key" + if enrollType == orbitEnroll { + nodeKeyColumn = "orbit_node_key" + } + if osqueryID != "" || uuid != "" { - _, _ = query.WriteString(`(SELECT id, last_enrolled_at, 1 priority FROM hosts WHERE osquery_host_id = ?)`) + _, _ = query.WriteString(fmt.Sprintf(`(SELECT id, last_enrolled_at, %s IS NOT NULL AS node_key_set, 1 priority FROM hosts WHERE osquery_host_id = ?)`, nodeKeyColumn)) osqueryHostID := osqueryID if osqueryID == "" { // special-case, if there's no osquery identifier, use the uuid @@ -1741,21 +1770,25 @@ func matchHostDuringEnrollment(ctx context.Context, q sqlx.QueryerContext, enrol if query.Len() > 0 { _, _ = query.WriteString(" UNION ") } - _, _ = query.WriteString(`(SELECT id, last_enrolled_at, 2 priority FROM hosts WHERE hardware_serial = ? AND (platform = 'darwin' OR platform = 'ios' OR platform = 'ipados') ORDER BY id LIMIT 1)`) + _, _ = query.WriteString(fmt.Sprintf(`(SELECT id, last_enrolled_at, %s IS NOT NULL AS node_key_set, 2 priority FROM hosts WHERE hardware_serial = ? AND (platform = 'darwin' OR platform = 'ios' OR platform = 'ipados') ORDER BY id LIMIT 1)`, nodeKeyColumn)) args = append(args, serial) } if err := sqlx.SelectContext(ctx, q, &rows, query.String(), args...); err != nil { - return 0, time.Time{}, ctxerr.Wrap(ctx, err, "match host during enrollment") + return nil, ctxerr.Wrap(ctx, err, "match host during enrollment") } if len(rows) == 0 { - return 0, time.Time{}, sql.ErrNoRows + return nil, sql.ErrNoRows } sort.Slice(rows, func(i, j int) bool { l, r := rows[i], rows[j] return l.Priority < r.Priority }) - return rows[0].ID, rows[0].LastEnrolledAt, nil + return &enrolledHostInfo{ + ID: rows[0].ID, + LastEnrolledAt: rows[0].LastEnrolledAt, + NodeKeySet: rows[0].NodeKeySet, + }, nil } func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInfo fleet.OrbitHostInfo, orbitNodeKey string, teamID *uint) (*fleet.Host, error) { @@ -1769,7 +1802,7 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf var host fleet.Host err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { - hostID, _, err := matchHostDuringEnrollment(ctx, tx, orbitEnroll, isMDMEnabled, hostInfo.OsqueryIdentifier, hostInfo.HardwareUUID, hostInfo.HardwareSerial) + enrolledHostInfo, err := matchHostDuringEnrollment(ctx, tx, orbitEnroll, isMDMEnabled, hostInfo.OsqueryIdentifier, hostInfo.HardwareUUID, hostInfo.HardwareSerial) // If the osquery identifier that osqueryd will use was not sent by Orbit, then use the hardware UUID as identifier // (using the hardware UUID is Orbit's default behavior). @@ -1780,6 +1813,17 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf switch { case err == nil: + if enrolledHostInfo.NodeKeySet { + // This means a orbit host already enrolled at this hosts entry. + // This can happen if two devices have duplicate hardware identifiers or + // if orbit's node key file was deleted from the device (e.g. uninstall+install). + level.Warn(ds.logger).Log( + "msg", "orbit host with duplicate identifier has enrolled in Fleet and will overwrite existing host data", + "identifier", hostInfo.HardwareUUID, + "host_id", enrolledHostInfo.ID, + ) + } + sqlUpdate := ` UPDATE hosts @@ -1788,6 +1832,7 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf uuid = COALESCE(NULLIF(uuid, ''), ?), osquery_host_id = COALESCE(NULLIF(osquery_host_id, ''), ?), hardware_serial = COALESCE(NULLIF(hardware_serial, ''), ?), + last_enrolled_at = NOW(), team_id = ? WHERE id = ?` _, err := tx.ExecContext(ctx, sqlUpdate, @@ -1796,15 +1841,15 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf osqueryIdentifier, hostInfo.HardwareSerial, teamID, - hostID, + enrolledHostInfo.ID, ) if err != nil { return ctxerr.Wrap(ctx, err, "orbit enroll error updating host details") } - host.ID = hostID + host.ID = enrolledHostInfo.ID // clear any host_mdm_actions following re-enrollment here - if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, hostID); err != nil { + if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, enrolledHostInfo.ID); err != nil { return ctxerr.Wrap(ctx, err, "orbit enroll error clearing host_mdm_actions") } @@ -1871,7 +1916,7 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf return &host, nil } -// EnrollHost enrolls a host +// EnrollHost enrolls the osquery agent to Fleet. func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryHostID, hardwareUUID, hardwareSerial, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error) { if osqueryHostID == "" { return nil, ctxerr.New(ctx, "missing osquery host identifier") @@ -1881,11 +1926,11 @@ func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryH err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { zeroTime := time.Unix(0, 0).Add(24 * time.Hour) - matchedID, lastEnrolledAt, err := matchHostDuringEnrollment(ctx, tx, osqueryEnroll, isMDMEnabled, osqueryHostID, hardwareUUID, hardwareSerial) + var hostID uint + enrolledHostInfo, err := matchHostDuringEnrollment(ctx, tx, osqueryEnroll, isMDMEnabled, osqueryHostID, hardwareUUID, hardwareSerial) switch { case err != nil && !errors.Is(err, sql.ErrNoRows): return ctxerr.Wrap(ctx, err, "check existing") - case errors.Is(err, sql.ErrNoRows): // Create new host record. We always create newly enrolled hosts with refetch_requested = true // so that the frontend automatically starts background checks to update the page whenever @@ -1908,30 +1953,42 @@ func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryH level.Info(ds.logger).Log("hostIDError", err.Error()) return ctxerr.Wrap(ctx, err, "insert host") } - hostID, _ := result.LastInsertId() + lastInsertID, _ := result.LastInsertId() const sqlHostDisplayName = ` INSERT INTO host_display_names (host_id, display_name) VALUES (?, '') ` - _, err = tx.ExecContext(ctx, sqlHostDisplayName, hostID) + _, err = tx.ExecContext(ctx, sqlHostDisplayName, lastInsertID) if err != nil { return ctxerr.Wrap(ctx, err, "insert host_display_names") } - matchedID = uint(hostID) - + hostID = uint(lastInsertID) default: + hostID = enrolledHostInfo.ID + // Prevent hosts from enrolling too often with the same identifier. // Prior to adding this we saw many hosts (probably VMs) with the // same identifier competing for enrollment and causing perf issues. - if cooldown > 0 && time.Since(lastEnrolledAt) < cooldown { + if cooldown > 0 && time.Since(enrolledHostInfo.LastEnrolledAt) < cooldown { return backoff.Permanent(ctxerr.Errorf(ctx, "host identified by %s enrolling too often", osqueryHostID)) } - if err := deleteAllPolicyMemberships(ctx, tx, []uint{matchedID}); err != nil { + if enrolledHostInfo.NodeKeySet { + // This means a osquery host already enrolled at this hosts entry. + // This can happen if two devices have duplicate hardware identifiers or + // if osquery.db was deleted from the device (e.g. uninstall+install). + level.Warn(ds.logger).Log( + "msg", "osquery host with duplicate identifier has enrolled in Fleet and will overwrite existing host data", + "identifier", hardwareUUID, + "host_id", enrolledHostInfo.ID, + ) + } + + if err := deleteAllPolicyMemberships(ctx, tx, []uint{enrolledHostInfo.ID}); err != nil { return ctxerr.Wrap(ctx, err, "cleanup policy membership on re-enroll") } // clear any host_mdm_actions following re-enrollment here - if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, matchedID); err != nil { + if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, enrolledHostInfo.ID); err != nil { return ctxerr.Wrap(ctx, err, "error clearing host_mdm_actions") } @@ -1946,7 +2003,7 @@ func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryH hardware_serial = COALESCE(NULLIF(hardware_serial, ''), ?) WHERE id = ? ` - _, err := tx.ExecContext(ctx, sqlUpdate, nodeKey, teamID, osqueryHostID, hardwareUUID, hardwareSerial, matchedID) + _, err := tx.ExecContext(ctx, sqlUpdate, nodeKey, teamID, osqueryHostID, hardwareUUID, hardwareSerial, enrolledHostInfo.ID) if err != nil { return ctxerr.Wrap(ctx, err, "update host") } @@ -1955,7 +2012,7 @@ func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryH _, err = tx.ExecContext(ctx, ` INSERT INTO host_seen_times (host_id, seen_time) VALUES (?, ?) ON DUPLICATE KEY UPDATE seen_time = VALUES(seen_time)`, - matchedID, time.Now().UTC()) + hostID, time.Now().UTC()) if err != nil { return ctxerr.Wrap(ctx, err, "new host seen time") } @@ -2012,11 +2069,11 @@ func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryH WHERE h.id = ? LIMIT 1 ` - err = sqlx.GetContext(ctx, tx, &host, sqlSelect, matchedID) + err = sqlx.GetContext(ctx, tx, &host, sqlSelect, hostID) if err != nil { return ctxerr.Wrap(ctx, err, "getting the host to return") } - _, err = tx.ExecContext(ctx, `INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, (SELECT id FROM labels WHERE name = 'All Hosts' AND label_type = 1))`, matchedID) + _, err = tx.ExecContext(ctx, `INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, (SELECT id FROM labels WHERE name = 'All Hosts' AND label_type = 1))`, hostID) if err != nil { return ctxerr.Wrap(ctx, err, "insert new host into all hosts label") } diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 26f8dc41c8..634b50d61d 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -8069,6 +8069,62 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) { scenarioC(platform) }) } + + // Scenario D: + // - Fleet with MDM disabled. + // - two linux|darwin|windows hosts with the same hardware identifiers (e.g. two cloned VMs). + // - fleetd running with host identifier set to uuid (default). + // - orbit enrolls first, then osquery + // Expected output: The two fleetd instances should be enrolled as one host. + scenarioD := func(platform string) { + dupUUID := uuid.New().String() + dupHWSerial := uuid.New().String() + + h1Orbit, err := ds.EnrollOrbit(ctx, false, fleet.OrbitHostInfo{ + HardwareUUID: dupUUID, + HardwareSerial: dupHWSerial, + Platform: platform, + }, uuid.New().String(), nil) + require.NoError(t, err) + h1OrbitFetched, err := ds.Host(ctx, h1Orbit.ID) + require.NoError(t, err) + time.Sleep(1 * time.Second) // to test the update of last_enrolled_at + h1Osquery, err := ds.EnrollHost(ctx, false, dupUUID, dupUUID, dupHWSerial, uuid.New().String(), nil, 0) + require.NoError(t, err) + h1OsqueryFetched, err := ds.Host(ctx, h1Osquery.ID) + require.NoError(t, err) + require.NotEqual(t, h1OrbitFetched.LastEnrolledAt, h1OsqueryFetched.LastEnrolledAt) + require.Equal(t, h1Orbit.ID, h1Osquery.ID) + time.Sleep(1 * time.Second) // to test the update of last_enrolled_at + h2Orbit, err := ds.EnrollOrbit(ctx, false, fleet.OrbitHostInfo{ + HardwareUUID: dupUUID, + HardwareSerial: dupHWSerial, + Platform: platform, + }, uuid.New().String(), nil) + require.NoError(t, err) + h2OrbitFetched, err := ds.Host(ctx, h2Orbit.ID) + require.NoError(t, err) + require.NotEqual(t, h1OsqueryFetched.LastEnrolledAt, h2OrbitFetched.LastEnrolledAt) + time.Sleep(1 * time.Second) // to test the update of last_enrolled_at + h2Osquery, err := ds.EnrollHost(ctx, false, dupUUID, dupUUID, dupHWSerial, uuid.New().String(), nil, 0) + require.NoError(t, err) + require.Equal(t, h2Orbit.ID, h2Osquery.ID) + h2OsqueryFetched, err := ds.Host(ctx, h2Osquery.ID) + require.NoError(t, err) + require.NotEqual(t, h2OrbitFetched.LastEnrolledAt, h2OsqueryFetched.LastEnrolledAt) + + // the hosts compete for the host entry (all have same row id) + require.Equal(t, h1Orbit.ID, h2Orbit.ID) + require.Equal(t, h1Orbit.ID, h1Osquery.ID) + require.Equal(t, h2Orbit.ID, h2Osquery.ID) + } + for _, platform := range []string{"ubuntu", "windows", "darwin"} { + platform := platform + t.Run("scenarioD_"+platform, func(t *testing.T) { + t.Parallel() + scenarioD(platform) + }) + } } func testHostsEnrollUpdatesMissingInfo(t *testing.T, ds *Datastore) { From 5f65ea831cf726f53372ae8d5c277f4ccf077514 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Mon, 10 Jun 2024 16:49:27 -0300 Subject: [PATCH 032/119] Disable AI features on non-new installations upgrading to 4.51.X (#19482) #19365 Assuming we release this fix in 4.51.0: - Migration from a version without the feature (< 4.50.0) to 4.51.0: Should disable (set `ai_features_disabled=true`). - Migration from a version with the feature (>= 4.50.X < 4.51.0) to 4.51.0: Should keep `ai_features_disabled` as-is. - New installation of Fleet: Should come with AI features enabled (`ai_features_disabled=false`). From https://github.com/fleetdm/fleet/issues/19365#issuecomment-2145825363. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [X] Manual QA for all new/changed functionality --- changes/19365-disable-ai-migration | 1 + .../20240430111727_CleanupQueryResults.go | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 changes/19365-disable-ai-migration diff --git a/changes/19365-disable-ai-migration b/changes/19365-disable-ai-migration new file mode 100644 index 0000000000..41e096d242 --- /dev/null +++ b/changes/19365-disable-ai-migration @@ -0,0 +1 @@ +* Disabled AI features on non-new installations upgrading from < 4.50.X to >= 4.51.X. diff --git a/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults.go b/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults.go index 063a150617..c0be097472 100644 --- a/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults.go +++ b/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults.go @@ -2,7 +2,10 @@ package tables import ( "database/sql" + "encoding/json" "fmt" + + "github.com/pkg/errors" ) func init() { @@ -22,6 +25,67 @@ func Up_20240430111727(tx *sql.Tx) error { if err != nil { return fmt.Errorf("failed to delete query_results %w", err) } + + // + // The following "fix" was introduced after this migration was released in 4.50.0. + // We are adding it here to disable the AI features (set ai_features_disabled=true) + // for non-new installations that are upgrading from < 4.50.0 using the new version + // of this migration to be released in 4.51.X. + // + if err := fixDisableAIForNonNewInstallation(tx); err != nil { + return fmt.Errorf("failed to update ai_features_disabled: %w", err) + } + + return nil +} + +func fixDisableAIForNonNewInstallation(tx *sql.Tx) error { + var usersCount int + row := tx.QueryRow(`SELECT COUNT(*) FROM users;`) + if err := row.Scan(&usersCount); err != nil { + return fmt.Errorf("select count users: %w", err) + } + if usersCount == 0 { + return nil + } + + // + // At least a "setup" user was configured, + // thus we assume this is not a new installation. + // + + var raw json.RawMessage + row = tx.QueryRow(`SELECT json_value FROM app_config_json LIMIT 1;`) + if err := row.Scan(&raw); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil + } + return fmt.Errorf("select app_config_json: %w", err) + } + + var config map[string]interface{} + if err := json.Unmarshal(raw, &config); err != nil { + return fmt.Errorf("unmarshal appconfig: %w", err) + } + + ss, ok := config["server_settings"] + if !ok { + return errors.New("missing server_settings") + } + serverSettings, ok := ss.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid type for server_settings: %T", ss) + } + serverSettings["ai_features_disabled"] = true + + b, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("marshal updated appconfig: %w", err) + } + if _, err := tx.Exec(`UPDATE app_config_json SET json_value = ? WHERE id = 1;`, b); err != nil { + return fmt.Errorf("update app_config_json: %w", err) + } + return nil } From 7eb3628fe6e88f8472287bf76ca9b24f7da4df9c Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Mon, 10 Jun 2024 16:49:45 -0300 Subject: [PATCH 033/119] Support RPM upgrades on fleetd packages (#19494) #18534 - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [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. - [x] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). --- changes/18534-support-rpm-upgrade | 1 + orbit/pkg/packaging/linux_shared.go | 56 +++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 changes/18534-support-rpm-upgrade diff --git a/changes/18534-support-rpm-upgrade b/changes/18534-support-rpm-upgrade new file mode 100644 index 0000000000..8294440820 --- /dev/null +++ b/changes/18534-support-rpm-upgrade @@ -0,0 +1 @@ +* Added support for upgrades to fleetd RPMs packages. diff --git a/orbit/pkg/packaging/linux_shared.go b/orbit/pkg/packaging/linux_shared.go index 8e0d67aff9..c7199b8163 100644 --- a/orbit/pkg/packaging/linux_shared.go +++ b/orbit/pkg/packaging/linux_shared.go @@ -15,6 +15,7 @@ import ( "github.com/fleetdm/fleet/v4/pkg/secure" "github.com/goreleaser/nfpm/v2" "github.com/goreleaser/nfpm/v2/files" + "github.com/goreleaser/nfpm/v2/rpm" "github.com/rs/zerolog/log" ) @@ -85,6 +86,8 @@ func buildNFPM(opt Options, pkger nfpm.Packager) (string, error) { // Write files + _, isRPM := pkger.(*rpm.RPM) + if err := writeSystemdUnit(opt, rootDir); err != nil { return "", fmt.Errorf("write systemd unit: %w", err) } @@ -110,9 +113,16 @@ func buildNFPM(opt Options, pkger nfpm.Packager) (string, error) { return "", fmt.Errorf("write preremove script: %w", err) } postRemovePath := filepath.Join(tmpDir, "postremove.sh") - if err := writePostRemove(opt, postRemovePath); err != nil { + if err := writePostRemove(postRemovePath); err != nil { return "", fmt.Errorf("write postremove script: %w", err) } + var postTransPath string + if isRPM { + postTransPath = filepath.Join(tmpDir, "posttrans.sh") + if err := writeRPMPostTrans(opt, postTransPath); err != nil { + return "", fmt.Errorf("write RPM posttrans script: %w", err) + } + } if opt.FleetCertificate != "" { if err := writeFleetServerCertificate(opt, orbitRoot); err != nil { @@ -194,6 +204,11 @@ func buildNFPM(opt Options, pkger nfpm.Packager) (string, error) { log.Debug().Interface("file", c).Msg("added file") } + rpmInfo := nfpm.RPM{} + if _, ok := pkger.(*rpm.RPM); ok { + rpmInfo.Scripts.PostTrans = postTransPath + } + // Build package info := &nfpm.Info{ Name: "fleet-osquery", @@ -211,6 +226,7 @@ func buildNFPM(opt Options, pkger nfpm.Packager) (string, error) { PreRemove: preRemovePath, PostRemove: postRemovePath, }, + RPM: rpmInfo, }, } filename := pkger.ConventionalFileName(info) @@ -367,7 +383,7 @@ pkill fleet-desktop || true return nil } -func writePostRemove(opt Options, path string) error { +func writePostRemove(path string) error { if err := os.WriteFile(path, []byte(`#!/bin/sh # For RPM during uninstall, $1 is 0 @@ -381,3 +397,39 @@ fi return nil } + +// postTransTemplate contains the template for RPM posttrans scriptlet (used when upgrading). +// See https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/. +// +// We cannot rely on "$1" because it's always "0" for RPM < 4.12 +// (see https://github.com/rpm-software-management/rpm/commit/ab069ec876639d46d12dd76dad54fd8fb762e43d) +// thus we check if orbit service is enabled, and if not we enable it (because posttrans +// will run both on "install" and "upgrade"). +var postTransTemplate = template.Must(template.New("posttrans").Parse(`#!/bin/sh + +# Exit on error +set -e + +if ! systemctl is-enabled orbit >/dev/null 2>&1; then + # If we have a systemd, daemon-reload away now + if command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload >/dev/null 2>&1 +{{ if .StartService -}} + systemctl restart orbit.service 2>&1 + systemctl enable orbit.service 2>&1 +{{- end}} + fi +fi +`)) + +// writeRPMPostTrans sets the posttrans scriptlets necessary to support RPM upgrades. +func writeRPMPostTrans(opt Options, path string) error { + var contents bytes.Buffer + if err := postTransTemplate.Execute(&contents, opt); err != nil { + return fmt.Errorf("execute template: %w", err) + } + if err := os.WriteFile(path, contents.Bytes(), constant.DefaultFileMode); err != nil { + return fmt.Errorf("write file: %w", err) + } + return nil +} From a37d0692b1aa40106a77ab453604b70b45a531f6 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Mon, 10 Jun 2024 16:49:59 -0300 Subject: [PATCH 034/119] Fix fleetctl preview bug caused by creating enroll secrets (#19497) #19129 - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Manual QA for all new/changed functionality --- changes/19129-fleetctl-preview-enroll-secrets | 1 + cmd/fleetctl/preview.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changes/19129-fleetctl-preview-enroll-secrets diff --git a/changes/19129-fleetctl-preview-enroll-secrets b/changes/19129-fleetctl-preview-enroll-secrets new file mode 100644 index 0000000000..3851e61a90 --- /dev/null +++ b/changes/19129-fleetctl-preview-enroll-secrets @@ -0,0 +1 @@ +* Fixed bug in `fleetctl preview` caused by creating enroll secrets. diff --git a/cmd/fleetctl/preview.go b/cmd/fleetctl/preview.go index 41aa155dd2..a48734871c 100644 --- a/cmd/fleetctl/preview.go +++ b/cmd/fleetctl/preview.go @@ -407,8 +407,8 @@ Use the stop and reset subcommands to manage the server and dependencies once st return fmt.Errorf("Error retrieving enroll secret: %w", err) } - if len(secrets.Secrets) != 1 { - return errors.New("Expected 1 active enroll secret") + if len(secrets.Secrets) == 0 { + return errors.New("Expected at least one enroll secret") } // disable analytics collection for preview From 3dbdbc1bcf15519422f079e63ca4931d21ff924a Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Mon, 10 Jun 2024 17:02:35 -0300 Subject: [PATCH 035/119] Add support for iOS/iPadOS to osquery-perf (#19522) #18119 - [X] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. Sample on how to simulate 50 iPads and 50 iPhones: ```sh go run ./cmd/osquery-perf -host_count 100 -os_templates iphone_14.6.tmpl:50,ipad_13.18.tmpl:50 -mdm_scep_challenge <...> ``` --- cmd/osquery-perf/agent.go | 62 ++++++++++++++++++- cmd/osquery-perf/ipad_13.18.tmpl | 0 cmd/osquery-perf/iphone_14.6.tmpl | 0 pkg/mdm/mdmtest/apple.go | 35 ++++++++++- .../service/integration_mdm_lifecycle_test.go | 8 +-- .../service/integration_mdm_profiles_test.go | 2 +- server/service/integration_mdm_test.go | 54 ++++++++-------- 7 files changed, 123 insertions(+), 38 deletions(-) create mode 100644 cmd/osquery-perf/ipad_13.18.tmpl create mode 100644 cmd/osquery-perf/iphone_14.6.tmpl diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 6875b07150..b919a4c000 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -567,7 +567,7 @@ func newAgent( SCEPChallenge: mdmSCEPChallenge, SCEPURL: serverAddress + apple_mdm.SCEPPath, MDMURL: serverAddress + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") // Have the osquery agent match the MDM device serial number and UUID. serialNumber = macMDMClient.SerialNumber hostUUID = macMDMClient.UUID @@ -2150,6 +2150,54 @@ func (a *agent) submitLogs(results []resultLog) error { return nil } +func runAppleIDeviceMDMLoop(i int, stats *Stats, model string, serverURL string, mdmSCEPChallenge string, mdmCheckInInterval time.Duration) { + udid := mdmtest.RandUDID() + + mdmClient := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{ + SCEPChallenge: mdmSCEPChallenge, + SCEPURL: serverURL + apple_mdm.SCEPPath, + MDMURL: serverURL + apple_mdm.MDMPath, + }, model) + mdmClient.UUID = udid + mdmClient.SerialNumber = mdmtest.RandSerialNumber() + deviceName := fmt.Sprintf("%s-%d", model, i) + productName := model + + if err := mdmClient.Enroll(); err != nil { + log.Printf("%s MDM enroll failed: %s", model, err) + stats.IncrementMDMErrors() + return + } + + stats.IncrementMDMEnrollments() + + mdmCheckInTicker := time.Tick(mdmCheckInInterval) + + for range mdmCheckInTicker { + mdmCommandPayload, err := mdmClient.Idle() + if err != nil { + log.Printf("MDM Idle request failed: %s: %s", model, err) + stats.IncrementMDMErrors() + continue + } + stats.IncrementMDMSessions() + + for mdmCommandPayload != nil { + stats.IncrementMDMCommandsReceived() + if mdmCommandPayload.Command.RequestType == "DeviceInformation" { + mdmCommandPayload, err = mdmClient.AcknowledgeDeviceInformation(udid, mdmCommandPayload.CommandUUID, deviceName, productName) + } else { + mdmCommandPayload, err = mdmClient.Acknowledge(mdmCommandPayload.CommandUUID) + } + if err != nil { + log.Printf("MDM Acknowledge request failed: %s: %s", model, err) + stats.IncrementMDMErrors() + break + } + } + } +} + // rows returns a set of rows for use in tests for query results. func rows(num int) string { b := strings.Builder{} @@ -2197,6 +2245,8 @@ func main() { "windows_11_22H2_2861.tmpl": true, "windows_11_22H2_3007.tmpl": true, "ubuntu_22.04.tmpl": true, + "iphone_14.6.tmpl": true, + "ipad_13.18.tmpl": true, } allowedTemplateNames := make([]string, 0, len(validTemplateNames)) for k := range validTemplateNames { @@ -2349,6 +2399,16 @@ func main() { tmpl = tmplss[i%len(tmplss)] } + if tmpl.Name() == "iphone_14.6.tmpl" || tmpl.Name() == "ipad_13.18.tmpl" { + model := "iPhone 14,6" + if tmpl.Name() == "ipad_13.18.tmpl" { + model = "iPad 13,18" + } + go runAppleIDeviceMDMLoop(i, stats, model, *serverURL, *mdmSCEPChallenge, *mdmCheckInInterval) + time.Sleep(sleepTime) + continue + } + a := newAgent(i+1, *serverURL, *enrollSecret, diff --git a/cmd/osquery-perf/ipad_13.18.tmpl b/cmd/osquery-perf/ipad_13.18.tmpl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/osquery-perf/iphone_14.6.tmpl b/cmd/osquery-perf/iphone_14.6.tmpl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pkg/mdm/mdmtest/apple.go b/pkg/mdm/mdmtest/apple.go index d4463575d4..8d18c1c2a1 100644 --- a/pkg/mdm/mdmtest/apple.go +++ b/pkg/mdm/mdmtest/apple.go @@ -137,11 +137,11 @@ func NewTestMDMClientAppleDEP(serverURL string, depURLToken string, opts ...Test // NewTestMDMClientAppleDirect will create a simulated device that will not fetch the enrollment // profile from Fleet. The enrollment information is to be provided in the enrollInfo. -func NewTestMDMClientAppleDirect(enrollInfo AppleEnrollInfo, opts ...TestMDMAppleClientOption) *TestAppleMDMClient { +func NewTestMDMClientAppleDirect(enrollInfo AppleEnrollInfo, model string, opts ...TestMDMAppleClientOption) *TestAppleMDMClient { c := TestAppleMDMClient{ UUID: strings.ToUpper(uuid.New().String()), SerialNumber: RandSerialNumber(), - Model: "MacBookPro16,1", + Model: model, EnrollInfo: enrollInfo, } @@ -392,6 +392,9 @@ func (c *TestAppleMDMClient) Authenticate() error { "EnrollmentID": "testenrollmentid-" + c.UUID, "SerialNumber": c.SerialNumber, } + if strings.HasPrefix(c.Model, "iPhone") || strings.HasPrefix(c.Model, "iPad") { + payload["ProductName"] = c.Model + } _, err := c.request("application/x-apple-aspen-mdm-checkin", payload) return err } @@ -480,6 +483,23 @@ func (c *TestAppleMDMClient) Acknowledge(cmdUUID string) (*mdm.Command, error) { return c.sendAndDecodeCommandResponse(payload) } +func (c *TestAppleMDMClient) AcknowledgeDeviceInformation(udid, cmdUUID, deviceName, productName string) (*mdm.Command, error) { + payload := map[string]any{ + "Status": "Acknowledged", + "UDID": udid, + "CommandUUID": cmdUUID, + "QueryResponses": map[string]interface{}{ + "AvailableDeviceCapacity": float64(51.53312768), + "DeviceCapacity": float64(64), + "DeviceName": deviceName, + "OSVersion": "17.5.1", + "ProductName": productName, + "WiFiMAC": "ff:ff:ff:ff:ff:ff", + }, + } + return c.sendAndDecodeCommandResponse(payload) +} + func (c *TestAppleMDMClient) GetBootstrapToken() ([]byte, error) { payload := map[string]any{ "MessageType": "GetBootstrapToken", @@ -648,7 +668,11 @@ const serialLetters = "0123456789ABCDEFGHJKMNPQRSTUVWXYZ" // RandSerialNumber returns a fake random serial number. func RandSerialNumber() string { - b := make([]byte, 12) + return randStr(12) +} + +func randStr(n int) string { + b := make([]byte, n) for i := range b { //nolint:gosec // not used for crypto, only to generate random serial for testing b[i] = serialLetters[mrand.Intn(len(serialLetters))] @@ -656,6 +680,11 @@ func RandSerialNumber() string { return string(b) } +// RandUDID returns a fake random iOS/iPadOS 17+ UDID. +func RandUDID() string { + return fmt.Sprintf("%s-%s", randStr(8), randStr(16)) +} + type scepClient interface { scepserver.Service Supports(cap string) bool diff --git a/server/service/integration_mdm_lifecycle_test.go b/server/service/integration_mdm_lifecycle_test.go index 7bd789f56d..75fad197f0 100644 --- a/server/service/integration_mdm_lifecycle_test.go +++ b/server/service/integration_mdm_lifecycle_test.go @@ -134,12 +134,10 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsApple() { dupeClient := mdmtest.NewTestMDMClientAppleDirect( mdmtest.AppleEnrollInfo{ - SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }, - ) + }, "MacBookPro16,1") dupeClient.UUID = device.UUID dupeClient.SerialNumber = device.SerialNumber dupeClient.Model = device.Model @@ -159,12 +157,10 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsApple() { dupeClient := mdmtest.NewTestMDMClientAppleDirect( mdmtest.AppleEnrollInfo{ - SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }, - ) + }, "MacBookPro16,1") dupeClient.UUID = device.UUID dupeClient.SerialNumber = device.SerialNumber dupeClient.Model = device.Model diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go index 8a46f3ec95..6a3a28104a 100644 --- a/server/service/integration_mdm_profiles_test.go +++ b/server/service/integration_mdm_profiles_test.go @@ -1898,7 +1898,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") // enroll the device with orbit var resp EnrollOrbitResponse diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 4463ab2cda..4f95a5395d 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -425,7 +425,7 @@ func (s *integrationMDMTestSuite) TestGetBootstrapToken() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") err := mdmDevice.Enroll() require.NoError(t, err) @@ -776,13 +776,13 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() { SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, } - mdmDeviceA := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo) + mdmDeviceA := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1") err := mdmDeviceA.Enroll() require.NoError(t, err) s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(), fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), 0) - mdmDeviceB := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo) + mdmDeviceB := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1") err = mdmDeviceB.Enroll() require.NoError(t, err) s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(), @@ -873,7 +873,7 @@ func (s *integrationMDMTestSuite) TestDeviceMultipleAuthMessages() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") err := mdmDevice.Enroll() require.NoError(t, err) @@ -1008,7 +1008,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") err := mdmDevice.Enroll() require.NoError(t, err) @@ -2341,7 +2341,7 @@ func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") err := mdmDevice.Enroll() require.NoError(t, err) @@ -2754,24 +2754,24 @@ func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() { MDMURL: s.server.URL + apple_mdm.MDMPath, } noTeamDevices := []deviceWithResponse{ - {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Offline", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Offline", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Pending", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Pending", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, + {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Offline", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Offline", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Pending", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Pending", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, } teamDevices := []deviceWithResponse{ - {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Offline", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, - {"Pending", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)}, + {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Offline", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, + {"Pending", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")}, } expectedSerialsByTeamAndStatus := make(map[uint]map[fleet.MDMBootstrapPackageStatus][]string) @@ -4375,7 +4375,7 @@ func (s *integrationMDMTestSuite) TestSSO() { mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{ SCEPChallenge: s.scepChallenge, - }) + }, "MacBookPro16,1") var lastSubmittedProfile *godep.Profile s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -5292,7 +5292,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") mdmDevice.SerialNumber = h.HardwareSerial mdmDevice.UUID = h.UUID err = mdmDevice.Enroll() @@ -5349,7 +5349,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") mdmDevice.SerialNumber = h2.HardwareSerial mdmDevice.UUID = h2.UUID err = mdmDevice.Enroll() @@ -5372,7 +5372,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") mdmDevice.SerialNumber = h3.HardwareSerial mdmDevice.UUID = h3.UUID err = mdmDevice.Enroll() @@ -5393,7 +5393,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") mdmDevice.SerialNumber = h4.HardwareSerial mdmDevice.UUID = h4.UUID err = mdmDevice.Enroll() @@ -7797,7 +7797,7 @@ func (s *integrationMDMTestSuite) TestManualEnrollmentCommands() { SCEPChallenge: s.scepChallenge, SCEPURL: s.server.URL + apple_mdm.SCEPPath, MDMURL: s.server.URL + apple_mdm.MDMPath, - }) + }, "MacBookPro16,1") err := mdmDevice.Enroll() require.NoError(t, err) s.runWorker() From 01dd0c7c4d4e91995b40551752c28f05f9c7e813 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Mon, 10 Jun 2024 17:02:49 -0300 Subject: [PATCH 036/119] Update osquery flags to `5.12.2` (#19338) #17375 Updating the osquery flags for 5.12.2 And making the changes so that we can just run `cd server/fleet/ && go generate` on a macOS host every time we need to do this. Manual tested by setting `logger_tls_backoff_max` in Fleet agent settings (which is a 5.12.0 flag). - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Victor Lyuboslavsky --- changes/17365-update-osquery-flags | 1 + server/fleet/agent_options.go | 309 +----------------- server/fleet/agent_options_generated.go | 306 +++++++++++++++++ server/fleet/agent_options_test.go | 10 + tools/osquery-agent-options/README.md | 8 +- tools/osquery-agent-options/main.go | 145 +++++++- ...flags.txt => osquery_5.12.2_codeflags.txt} | 1 - 7 files changed, 459 insertions(+), 321 deletions(-) create mode 100644 changes/17365-update-osquery-flags create mode 100644 server/fleet/agent_options_generated.go rename tools/osquery-agent-options/{osquery_5.11.0_codeflags.txt => osquery_5.12.2_codeflags.txt} (99%) diff --git a/changes/17365-update-osquery-flags b/changes/17365-update-osquery-flags new file mode 100644 index 0000000000..5fb147911a --- /dev/null +++ b/changes/17365-update-osquery-flags @@ -0,0 +1 @@ +* Update osquery flags with new flags added on 5.12.X. \ No newline at end of file diff --git a/server/fleet/agent_options.go b/server/fleet/agent_options.go index ef38d7d23c..53e2d4674a 100644 --- a/server/fleet/agent_options.go +++ b/server/fleet/agent_options.go @@ -9,6 +9,8 @@ import ( "strings" ) +//go:generate go run ../../tools/osquery-agent-options agent_options_generated.go + type AgentOptions struct { // Config is the base config options. Config json.RawMessage `json:"config"` @@ -153,9 +155,6 @@ func validateJSONAgentOptionsExtensions(ctx context.Context, ds Datastore, optsE // JSON definition of the available configuration options in osquery. // See https://osquery.readthedocs.io/en/stable/deployment/configuration/#configuration-specification -// -// NOTE: Update the following line with the version used for validation. -// Current version: 5.11.0 type osqueryAgentOptions struct { Options osqueryOptions `json:"options"` @@ -212,310 +211,6 @@ type osqueryAgentOptions struct { } `json:"events"` } -// NOTE: generate automatically with `go run ./tools/osquery-agent-options/main.go` -type osqueryOptions struct { - AuditAllowConfig bool `json:"audit_allow_config"` - AuditAllowFimEvents bool `json:"audit_allow_fim_events"` - AuditAllowProcessEvents bool `json:"audit_allow_process_events"` - AuditAllowSockets bool `json:"audit_allow_sockets"` - AuditAllowUserEvents bool `json:"audit_allow_user_events"` - AugeasLenses string `json:"augeas_lenses"` - AwsAccessKeyId string `json:"aws_access_key_id"` - AwsDebug bool `json:"aws_debug"` - AwsDisableImdsv1Fallback bool `json:"aws_disable_imdsv1_fallback"` - AwsEnableProxy bool `json:"aws_enable_proxy"` - AwsFirehoseEndpoint string `json:"aws_firehose_endpoint"` - AwsFirehosePeriod uint64 `json:"aws_firehose_period"` - AwsFirehoseRegion string `json:"aws_firehose_region"` - AwsFirehoseStream string `json:"aws_firehose_stream"` - AwsImdsv2RequestAttempts uint32 `json:"aws_imdsv2_request_attempts"` - AwsImdsv2RequestInterval uint32 `json:"aws_imdsv2_request_interval"` - AwsKinesisDisableLogStatus bool `json:"aws_kinesis_disable_log_status"` - AwsKinesisEndpoint string `json:"aws_kinesis_endpoint"` - AwsKinesisPeriod uint64 `json:"aws_kinesis_period"` - AwsKinesisRandomPartitionKey bool `json:"aws_kinesis_random_partition_key"` - AwsKinesisRegion string `json:"aws_kinesis_region"` - AwsKinesisStream string `json:"aws_kinesis_stream"` - AwsProfileName string `json:"aws_profile_name"` - AwsProxyHost string `json:"aws_proxy_host"` - AwsProxyPassword string `json:"aws_proxy_password"` - AwsProxyPort uint32 `json:"aws_proxy_port"` - AwsProxyScheme string `json:"aws_proxy_scheme"` - AwsProxyUsername string `json:"aws_proxy_username"` - AwsRegion string `json:"aws_region"` - AwsSecretAccessKey string `json:"aws_secret_access_key"` - AwsSessionToken string `json:"aws_session_token"` - AwsStsArnRole string `json:"aws_sts_arn_role"` - AwsStsRegion string `json:"aws_sts_region"` - AwsStsSessionName string `json:"aws_sts_session_name"` - AwsStsTimeout uint64 `json:"aws_sts_timeout"` - BufferedLogMax uint64 `json:"buffered_log_max"` - DecorationsTopLevel bool `json:"decorations_top_level"` - DisableAudit bool `json:"disable_audit"` - DisableCaching bool `json:"disable_caching"` - DisableDatabase bool `json:"disable_database"` - DisableDecorators bool `json:"disable_decorators"` - DisableDistributed bool `json:"disable_distributed"` - DisableEvents bool `json:"disable_events"` - DisableHashCache bool `json:"disable_hash_cache"` - DisableLogging bool `json:"disable_logging"` - DistributedDenylistDuration uint64 `json:"distributed_denylist_duration"` - DistributedInterval uint64 `json:"distributed_interval"` - DistributedLoginfo bool `json:"distributed_loginfo"` - DistributedPlugin string `json:"distributed_plugin"` - DistributedTlsMaxAttempts uint64 `json:"distributed_tls_max_attempts"` - DistributedTlsReadEndpoint string `json:"distributed_tls_read_endpoint"` - DistributedTlsWriteEndpoint string `json:"distributed_tls_write_endpoint"` - DockerSocket string `json:"docker_socket"` - EnableFileEvents bool `json:"enable_file_events"` - EnableForeign bool `json:"enable_foreign"` - EnableNumericMonitoring bool `json:"enable_numeric_monitoring"` - Ephemeral bool `json:"ephemeral"` - EsFimEnableOpenEvents bool `json:"es_fim_enable_open_events"` - EventsExpiry uint64 `json:"events_expiry"` - EventsMax uint64 `json:"events_max"` - EventsOptimize bool `json:"events_optimize"` - ExperimentList string `json:"experiment_list"` - ExtensionsDefaultIndex bool `json:"extensions_default_index"` - HashCacheMax uint32 `json:"hash_cache_max"` - HostIdentifier string `json:"host_identifier"` - IgnoreTableExceptions bool `json:"ignore_table_exceptions"` - KeychainAccessCache bool `json:"keychain_access_cache"` - KeychainAccessInterval uint32 `json:"keychain_access_interval"` - LoggerEventType bool `json:"logger_event_type"` - LoggerKafkaAcks string `json:"logger_kafka_acks"` - LoggerKafkaBrokers string `json:"logger_kafka_brokers"` - LoggerKafkaCompression string `json:"logger_kafka_compression"` - LoggerKafkaTopic string `json:"logger_kafka_topic"` - LoggerMinStatus int32 `json:"logger_min_status"` - LoggerMinStderr int32 `json:"logger_min_stderr"` - LoggerNumerics bool `json:"logger_numerics"` - LoggerPath string `json:"logger_path"` - LoggerRotate bool `json:"logger_rotate"` - LoggerRotateMaxFiles uint64 `json:"logger_rotate_max_files"` - LoggerRotateSize uint64 `json:"logger_rotate_size"` - LoggerSnapshotEventType bool `json:"logger_snapshot_event_type"` - LoggerSyslogFacility int32 `json:"logger_syslog_facility"` - LoggerSyslogPrependCee bool `json:"logger_syslog_prepend_cee"` - LoggerTlsCompress bool `json:"logger_tls_compress"` - LoggerTlsEndpoint string `json:"logger_tls_endpoint"` - LoggerTlsMaxLines uint64 `json:"logger_tls_max_lines"` - LoggerTlsMaxLinesize uint64 `json:"logger_tls_max_linesize"` - LoggerTlsPeriod uint64 `json:"logger_tls_period"` - Nullvalue string `json:"nullvalue"` - NumericMonitoringFilesystemPath string `json:"numeric_monitoring_filesystem_path"` - NumericMonitoringPlugins string `json:"numeric_monitoring_plugins"` - NumericMonitoringPreAggregationTime uint64 `json:"numeric_monitoring_pre_aggregation_time"` - PackDelimiter string `json:"pack_delimiter"` - PackRefreshInterval uint64 `json:"pack_refresh_interval"` - ReadMax uint64 `json:"read_max"` - ScheduleDefaultInterval uint64 `json:"schedule_default_interval"` - ScheduleEpoch uint64 `json:"schedule_epoch"` - ScheduleLognames bool `json:"schedule_lognames"` - ScheduleMaxDrift uint64 `json:"schedule_max_drift"` - ScheduleReload uint64 `json:"schedule_reload"` - ScheduleSplayPercent uint64 `json:"schedule_splay_percent"` - ScheduleTimeout uint64 `json:"schedule_timeout"` - SpecifiedIdentifier string `json:"specified_identifier"` - TableDelay uint64 `json:"table_delay"` - ThriftStringSizeLimit int32 `json:"thrift_string_size_limit"` - ThriftTimeout uint32 `json:"thrift_timeout"` - ThriftVerbose bool `json:"thrift_verbose"` - TlsDisableStatusLog bool `json:"tls_disable_status_log"` - Verbose bool `json:"verbose"` - WorkerThreads int32 `json:"worker_threads"` - YaraDelay uint32 `json:"yara_delay"` - - // embed the os-specific structs - OsqueryCommandLineFlagsLinux - OsqueryCommandLineFlagsWindows - OsqueryCommandLineFlagsMacOS - OsqueryCommandLineFlagsHidden -} - -// NOTE: generate automatically with `go run ./tools/osquery-agent-options/main.go` -type osqueryCommandLineFlags struct { - AlarmTimeout uint64 `json:"alarm_timeout"` - AuditAllowConfig bool `json:"audit_allow_config"` - AuditAllowFimEvents bool `json:"audit_allow_fim_events"` - AuditAllowProcessEvents bool `json:"audit_allow_process_events"` - AuditAllowSockets bool `json:"audit_allow_sockets"` - AuditAllowUserEvents bool `json:"audit_allow_user_events"` - AugeasLenses string `json:"augeas_lenses"` - AwsAccessKeyId string `json:"aws_access_key_id"` - AwsDebug bool `json:"aws_debug"` - AwsDisableImdsv1Fallback bool `json:"aws_disable_imdsv1_fallback"` - AwsEnableProxy bool `json:"aws_enable_proxy"` - AwsEnforceFips bool `json:"aws_enforce_fips"` - AwsFirehoseEndpoint string `json:"aws_firehose_endpoint"` - AwsFirehosePeriod uint64 `json:"aws_firehose_period"` - AwsFirehoseRegion string `json:"aws_firehose_region"` - AwsFirehoseStream string `json:"aws_firehose_stream"` - AwsImdsv2RequestAttempts uint32 `json:"aws_imdsv2_request_attempts"` - AwsImdsv2RequestInterval uint32 `json:"aws_imdsv2_request_interval"` - AwsKinesisDisableLogStatus bool `json:"aws_kinesis_disable_log_status"` - AwsKinesisEndpoint string `json:"aws_kinesis_endpoint"` - AwsKinesisPeriod uint64 `json:"aws_kinesis_period"` - AwsKinesisRandomPartitionKey bool `json:"aws_kinesis_random_partition_key"` - AwsKinesisRegion string `json:"aws_kinesis_region"` - AwsKinesisStream string `json:"aws_kinesis_stream"` - AwsProfileName string `json:"aws_profile_name"` - AwsProxyHost string `json:"aws_proxy_host"` - AwsProxyPassword string `json:"aws_proxy_password"` - AwsProxyPort uint32 `json:"aws_proxy_port"` - AwsProxyScheme string `json:"aws_proxy_scheme"` - AwsProxyUsername string `json:"aws_proxy_username"` - AwsRegion string `json:"aws_region"` - AwsSecretAccessKey string `json:"aws_secret_access_key"` - AwsSessionToken string `json:"aws_session_token"` - AwsStsArnRole string `json:"aws_sts_arn_role"` - AwsStsRegion string `json:"aws_sts_region"` - AwsStsSessionName string `json:"aws_sts_session_name"` - AwsStsTimeout uint64 `json:"aws_sts_timeout"` - BufferedLogMax uint64 `json:"buffered_log_max"` - CarverBlockSize uint32 `json:"carver_block_size"` - CarverCompression bool `json:"carver_compression"` - CarverContinueEndpoint string `json:"carver_continue_endpoint"` - CarverDisableFunction bool `json:"carver_disable_function"` - CarverExpiry uint32 `json:"carver_expiry"` - CarverStartEndpoint string `json:"carver_start_endpoint"` - ConfigAcceleratedRefresh uint64 `json:"config_accelerated_refresh"` - ConfigCheck bool `json:"config_check"` - ConfigDump bool `json:"config_dump"` - ConfigEnableBackup bool `json:"config_enable_backup"` - ConfigPath string `json:"config_path"` - ConfigPlugin string `json:"config_plugin"` - ConfigRefresh uint64 `json:"config_refresh"` - ConfigTlsEndpoint string `json:"config_tls_endpoint"` - ConfigTlsMaxAttempts uint64 `json:"config_tls_max_attempts"` - Daemonize bool `json:"daemonize"` - DatabaseDump bool `json:"database_dump"` - DatabasePath string `json:"database_path"` - DecorationsTopLevel bool `json:"decorations_top_level"` - DisableAudit bool `json:"disable_audit"` - DisableCaching bool `json:"disable_caching"` - DisableCarver bool `json:"disable_carver"` - DisableDatabase bool `json:"disable_database"` - DisableDecorators bool `json:"disable_decorators"` - DisableDistributed bool `json:"disable_distributed"` - DisableEnrollment bool `json:"disable_enrollment"` - DisableEvents bool `json:"disable_events"` - DisableExtensions bool `json:"disable_extensions"` - DisableHashCache bool `json:"disable_hash_cache"` - DisableLogging bool `json:"disable_logging"` - DisableReenrollment bool `json:"disable_reenrollment"` - DisableTables string `json:"disable_tables"` - DisableWatchdog bool `json:"disable_watchdog"` - DistributedDenylistDuration uint64 `json:"distributed_denylist_duration"` - DistributedInterval uint64 `json:"distributed_interval"` - DistributedLoginfo bool `json:"distributed_loginfo"` - DistributedPlugin string `json:"distributed_plugin"` - DistributedTlsMaxAttempts uint64 `json:"distributed_tls_max_attempts"` - DistributedTlsReadEndpoint string `json:"distributed_tls_read_endpoint"` - DistributedTlsWriteEndpoint string `json:"distributed_tls_write_endpoint"` - DockerSocket string `json:"docker_socket"` - EnableExtensionsWatchdog bool `json:"enable_extensions_watchdog"` - EnableFileEvents bool `json:"enable_file_events"` - EnableForeign bool `json:"enable_foreign"` - EnableNumericMonitoring bool `json:"enable_numeric_monitoring"` - EnableTables string `json:"enable_tables"` - EnableWatchdogDebug bool `json:"enable_watchdog_debug"` - EnrollAlways bool `json:"enroll_always"` - EnrollSecretEnv string `json:"enroll_secret_env"` - EnrollSecretPath string `json:"enroll_secret_path"` - EnrollTlsEndpoint string `json:"enroll_tls_endpoint"` - Ephemeral bool `json:"ephemeral"` - EsFimEnableOpenEvents bool `json:"es_fim_enable_open_events"` - EventsExpiry uint64 `json:"events_expiry"` - EventsMax uint64 `json:"events_max"` - EventsOptimize bool `json:"events_optimize"` - ExperimentList string `json:"experiment_list"` - ExtensionsAutoload string `json:"extensions_autoload"` - ExtensionsDefaultIndex bool `json:"extensions_default_index"` - ExtensionsInterval string `json:"extensions_interval"` - ExtensionsRequire string `json:"extensions_require"` - ExtensionsSocket string `json:"extensions_socket"` - ExtensionsTimeout string `json:"extensions_timeout"` - Force bool `json:"force"` - HashCacheMax uint32 `json:"hash_cache_max"` - HostIdentifier string `json:"host_identifier"` - IgnoreTableExceptions bool `json:"ignore_table_exceptions"` - Install bool `json:"install"` - KeychainAccessCache bool `json:"keychain_access_cache"` - KeychainAccessInterval uint32 `json:"keychain_access_interval"` - LoggerEventType bool `json:"logger_event_type"` - LoggerKafkaAcks string `json:"logger_kafka_acks"` - LoggerKafkaBrokers string `json:"logger_kafka_brokers"` - LoggerKafkaCompression string `json:"logger_kafka_compression"` - LoggerKafkaTopic string `json:"logger_kafka_topic"` - LoggerMinStatus int32 `json:"logger_min_status"` - LoggerMinStderr int32 `json:"logger_min_stderr"` - LoggerMode string `json:"logger_mode"` - LoggerNumerics bool `json:"logger_numerics"` - LoggerPath string `json:"logger_path"` - LoggerPlugin string `json:"logger_plugin"` - LoggerRotate bool `json:"logger_rotate"` - LoggerRotateMaxFiles uint64 `json:"logger_rotate_max_files"` - LoggerRotateSize uint64 `json:"logger_rotate_size"` - LoggerSnapshotEventType bool `json:"logger_snapshot_event_type"` - LoggerStderr bool `json:"logger_stderr"` - LoggerSyslogFacility int32 `json:"logger_syslog_facility"` - LoggerSyslogPrependCee bool `json:"logger_syslog_prepend_cee"` - LoggerTlsCompress bool `json:"logger_tls_compress"` - LoggerTlsEndpoint string `json:"logger_tls_endpoint"` - LoggerTlsMaxLines uint64 `json:"logger_tls_max_lines"` - LoggerTlsMaxLinesize uint64 `json:"logger_tls_max_linesize"` - LoggerTlsPeriod uint64 `json:"logger_tls_period"` - Logtostderr bool `json:"logtostderr"` - Nullvalue string `json:"nullvalue"` - NumericMonitoringFilesystemPath string `json:"numeric_monitoring_filesystem_path"` - NumericMonitoringPlugins string `json:"numeric_monitoring_plugins"` - NumericMonitoringPreAggregationTime uint64 `json:"numeric_monitoring_pre_aggregation_time"` - PackDelimiter string `json:"pack_delimiter"` - PackRefreshInterval uint64 `json:"pack_refresh_interval"` - Pidfile string `json:"pidfile"` - ProxyHostname string `json:"proxy_hostname"` - ReadMax uint64 `json:"read_max"` - ScheduleDefaultInterval uint64 `json:"schedule_default_interval"` - ScheduleEpoch uint64 `json:"schedule_epoch"` - ScheduleLognames bool `json:"schedule_lognames"` - ScheduleMaxDrift uint64 `json:"schedule_max_drift"` - ScheduleReload uint64 `json:"schedule_reload"` - ScheduleSplayPercent uint64 `json:"schedule_splay_percent"` - ScheduleTimeout uint64 `json:"schedule_timeout"` - SpecifiedIdentifier string `json:"specified_identifier"` - Stderrthreshold int32 `json:"stderrthreshold"` - TableDelay uint64 `json:"table_delay"` - ThriftStringSizeLimit int32 `json:"thrift_string_size_limit"` - ThriftTimeout uint32 `json:"thrift_timeout"` - ThriftVerbose bool `json:"thrift_verbose"` - TlsClientCert string `json:"tls_client_cert"` - TlsClientKey string `json:"tls_client_key"` - TlsDisableStatusLog bool `json:"tls_disable_status_log"` - TlsEnrollMaxAttempts uint64 `json:"tls_enroll_max_attempts"` - TlsEnrollMaxInterval uint64 `json:"tls_enroll_max_interval"` - TlsHostname string `json:"tls_hostname"` - TlsServerCerts string `json:"tls_server_certs"` - TlsSessionReuse bool `json:"tls_session_reuse"` - TlsSessionTimeout uint32 `json:"tls_session_timeout"` - Uninstall bool `json:"uninstall"` - Verbose bool `json:"verbose"` - WatchdogDelay uint64 `json:"watchdog_delay"` - WatchdogForcedShutdownDelay uint64 `json:"watchdog_forced_shutdown_delay"` - WatchdogLatencyLimit uint64 `json:"watchdog_latency_limit"` - WatchdogLevel int32 `json:"watchdog_level"` - WatchdogMemoryLimit uint64 `json:"watchdog_memory_limit"` - WatchdogUtilizationLimit uint64 `json:"watchdog_utilization_limit"` - WorkerThreads int32 `json:"worker_threads"` - YaraDelay uint32 `json:"yara_delay"` - - // embed the os-specific structs - OsqueryCommandLineFlagsLinux - OsqueryCommandLineFlagsWindows - OsqueryCommandLineFlagsMacOS - OsqueryCommandLineFlagsHidden -} - // the following structs are for OS-specific command-line flags supported by // osquery. They are exported so they can be used by the // tools/osquery-agent-options script. diff --git a/server/fleet/agent_options_generated.go b/server/fleet/agent_options_generated.go new file mode 100644 index 0000000000..427c390188 --- /dev/null +++ b/server/fleet/agent_options_generated.go @@ -0,0 +1,306 @@ +// Automatically generated by tools/osquery-agent-options for osquery 5.12.2. DO NOT EDIT! +// To update flags for a new osquery version, update the osqueryVersion variable in +// "tools/osquery-agent-options/main.go" and run "cd server/fleet/ && go generate". +package fleet + +type osqueryOptions struct { + AuditAllowConfig bool `json:"audit_allow_config"` + AuditAllowFimEvents bool `json:"audit_allow_fim_events"` + AuditAllowProcessEvents bool `json:"audit_allow_process_events"` + AuditAllowSockets bool `json:"audit_allow_sockets"` + AuditAllowUserEvents bool `json:"audit_allow_user_events"` + AugeasLenses string `json:"augeas_lenses"` + AwsAccessKeyId string `json:"aws_access_key_id"` + AwsDebug bool `json:"aws_debug"` + AwsDisableImdsv1Fallback bool `json:"aws_disable_imdsv1_fallback"` + AwsEnableProxy bool `json:"aws_enable_proxy"` + AwsFirehoseEndpoint string `json:"aws_firehose_endpoint"` + AwsFirehosePeriod uint64 `json:"aws_firehose_period"` + AwsFirehoseRegion string `json:"aws_firehose_region"` + AwsFirehoseStream string `json:"aws_firehose_stream"` + AwsImdsv2RequestAttempts uint32 `json:"aws_imdsv2_request_attempts"` + AwsImdsv2RequestInterval uint32 `json:"aws_imdsv2_request_interval"` + AwsKinesisDisableLogStatus bool `json:"aws_kinesis_disable_log_status"` + AwsKinesisEndpoint string `json:"aws_kinesis_endpoint"` + AwsKinesisPeriod uint64 `json:"aws_kinesis_period"` + AwsKinesisRandomPartitionKey bool `json:"aws_kinesis_random_partition_key"` + AwsKinesisRegion string `json:"aws_kinesis_region"` + AwsKinesisStream string `json:"aws_kinesis_stream"` + AwsProfileName string `json:"aws_profile_name"` + AwsProxyHost string `json:"aws_proxy_host"` + AwsProxyPassword string `json:"aws_proxy_password"` + AwsProxyPort uint32 `json:"aws_proxy_port"` + AwsProxyScheme string `json:"aws_proxy_scheme"` + AwsProxyUsername string `json:"aws_proxy_username"` + AwsRegion string `json:"aws_region"` + AwsSecretAccessKey string `json:"aws_secret_access_key"` + AwsSessionToken string `json:"aws_session_token"` + AwsStsArnRole string `json:"aws_sts_arn_role"` + AwsStsRegion string `json:"aws_sts_region"` + AwsStsSessionName string `json:"aws_sts_session_name"` + AwsStsTimeout uint64 `json:"aws_sts_timeout"` + BufferedLogMax uint64 `json:"buffered_log_max"` + DecorationsTopLevel bool `json:"decorations_top_level"` + DisableAudit bool `json:"disable_audit"` + DisableCaching bool `json:"disable_caching"` + DisableDatabase bool `json:"disable_database"` + DisableDecorators bool `json:"disable_decorators"` + DisableDistributed bool `json:"disable_distributed"` + DisableEvents bool `json:"disable_events"` + DisableHashCache bool `json:"disable_hash_cache"` + DisableLogging bool `json:"disable_logging"` + DistributedDenylistDuration uint64 `json:"distributed_denylist_duration"` + DistributedInterval uint64 `json:"distributed_interval"` + DistributedLoginfo bool `json:"distributed_loginfo"` + DistributedPlugin string `json:"distributed_plugin"` + DistributedTlsMaxAttempts uint64 `json:"distributed_tls_max_attempts"` + DistributedTlsReadEndpoint string `json:"distributed_tls_read_endpoint"` + DistributedTlsWriteEndpoint string `json:"distributed_tls_write_endpoint"` + DockerSocket string `json:"docker_socket"` + EnableFileEvents bool `json:"enable_file_events"` + EnableForeign bool `json:"enable_foreign"` + EnableNumericMonitoring bool `json:"enable_numeric_monitoring"` + Ephemeral bool `json:"ephemeral"` + EsFimEnableOpenEvents bool `json:"es_fim_enable_open_events"` + EventsExpiry uint64 `json:"events_expiry"` + EventsMax uint64 `json:"events_max"` + EventsOptimize bool `json:"events_optimize"` + ExperimentList string `json:"experiment_list"` + ExtensionsDefaultIndex bool `json:"extensions_default_index"` + HashCacheMax uint32 `json:"hash_cache_max"` + HostIdentifier string `json:"host_identifier"` + IgnoreTableExceptions bool `json:"ignore_table_exceptions"` + KeychainAccessCache bool `json:"keychain_access_cache"` + KeychainAccessInterval uint32 `json:"keychain_access_interval"` + LoggerEventType bool `json:"logger_event_type"` + LoggerKafkaAcks string `json:"logger_kafka_acks"` + LoggerKafkaBrokers string `json:"logger_kafka_brokers"` + LoggerKafkaCompression string `json:"logger_kafka_compression"` + LoggerKafkaTopic string `json:"logger_kafka_topic"` + LoggerMinStatus int32 `json:"logger_min_status"` + LoggerMinStderr int32 `json:"logger_min_stderr"` + LoggerNumerics bool `json:"logger_numerics"` + LoggerPath string `json:"logger_path"` + LoggerRotate bool `json:"logger_rotate"` + LoggerRotateMaxFiles uint64 `json:"logger_rotate_max_files"` + LoggerRotateSize uint64 `json:"logger_rotate_size"` + LoggerSnapshotEventType bool `json:"logger_snapshot_event_type"` + LoggerSyslogFacility int32 `json:"logger_syslog_facility"` + LoggerSyslogPrependCee bool `json:"logger_syslog_prepend_cee"` + LoggerTlsBackoffMax uint64 `json:"logger_tls_backoff_max"` + LoggerTlsCompress bool `json:"logger_tls_compress"` + LoggerTlsEndpoint string `json:"logger_tls_endpoint"` + LoggerTlsMaxLines uint64 `json:"logger_tls_max_lines"` + LoggerTlsMaxLinesize uint64 `json:"logger_tls_max_linesize"` + LoggerTlsPeriod uint64 `json:"logger_tls_period"` + Nullvalue string `json:"nullvalue"` + NumericMonitoringFilesystemPath string `json:"numeric_monitoring_filesystem_path"` + NumericMonitoringPlugins string `json:"numeric_monitoring_plugins"` + NumericMonitoringPreAggregationTime uint64 `json:"numeric_monitoring_pre_aggregation_time"` + PackDelimiter string `json:"pack_delimiter"` + PackRefreshInterval uint64 `json:"pack_refresh_interval"` + ReadMax uint64 `json:"read_max"` + ScheduleDefaultInterval uint64 `json:"schedule_default_interval"` + ScheduleEpoch uint64 `json:"schedule_epoch"` + ScheduleLognames bool `json:"schedule_lognames"` + ScheduleMaxDrift uint64 `json:"schedule_max_drift"` + ScheduleReload uint64 `json:"schedule_reload"` + ScheduleSplayPercent uint64 `json:"schedule_splay_percent"` + ScheduleTimeout uint64 `json:"schedule_timeout"` + SpecifiedIdentifier string `json:"specified_identifier"` + TableDelay uint64 `json:"table_delay"` + ThriftStringSizeLimit int32 `json:"thrift_string_size_limit"` + ThriftTimeout uint32 `json:"thrift_timeout"` + ThriftVerbose bool `json:"thrift_verbose"` + TlsDisableStatusLog bool `json:"tls_disable_status_log"` + Verbose bool `json:"verbose"` + YaraDelay uint32 `json:"yara_delay"` + + // embed the os-specific structs + OsqueryCommandLineFlagsLinux + OsqueryCommandLineFlagsWindows + OsqueryCommandLineFlagsMacOS + OsqueryCommandLineFlagsHidden +} + +type osqueryCommandLineFlags struct { + AlarmTimeout uint64 `json:"alarm_timeout"` + AuditAllowConfig bool `json:"audit_allow_config"` + AuditAllowFimEvents bool `json:"audit_allow_fim_events"` + AuditAllowProcessEvents bool `json:"audit_allow_process_events"` + AuditAllowSockets bool `json:"audit_allow_sockets"` + AuditAllowUserEvents bool `json:"audit_allow_user_events"` + AugeasLenses string `json:"augeas_lenses"` + AwsAccessKeyId string `json:"aws_access_key_id"` + AwsDebug bool `json:"aws_debug"` + AwsDisableImdsv1Fallback bool `json:"aws_disable_imdsv1_fallback"` + AwsEnableProxy bool `json:"aws_enable_proxy"` + AwsEnforceFips bool `json:"aws_enforce_fips"` + AwsFirehoseEndpoint string `json:"aws_firehose_endpoint"` + AwsFirehosePeriod uint64 `json:"aws_firehose_period"` + AwsFirehoseRegion string `json:"aws_firehose_region"` + AwsFirehoseStream string `json:"aws_firehose_stream"` + AwsImdsv2RequestAttempts uint32 `json:"aws_imdsv2_request_attempts"` + AwsImdsv2RequestInterval uint32 `json:"aws_imdsv2_request_interval"` + AwsKinesisDisableLogStatus bool `json:"aws_kinesis_disable_log_status"` + AwsKinesisEndpoint string `json:"aws_kinesis_endpoint"` + AwsKinesisPeriod uint64 `json:"aws_kinesis_period"` + AwsKinesisRandomPartitionKey bool `json:"aws_kinesis_random_partition_key"` + AwsKinesisRegion string `json:"aws_kinesis_region"` + AwsKinesisStream string `json:"aws_kinesis_stream"` + AwsProfileName string `json:"aws_profile_name"` + AwsProxyHost string `json:"aws_proxy_host"` + AwsProxyPassword string `json:"aws_proxy_password"` + AwsProxyPort uint32 `json:"aws_proxy_port"` + AwsProxyScheme string `json:"aws_proxy_scheme"` + AwsProxyUsername string `json:"aws_proxy_username"` + AwsRegion string `json:"aws_region"` + AwsSecretAccessKey string `json:"aws_secret_access_key"` + AwsSessionToken string `json:"aws_session_token"` + AwsStsArnRole string `json:"aws_sts_arn_role"` + AwsStsRegion string `json:"aws_sts_region"` + AwsStsSessionName string `json:"aws_sts_session_name"` + AwsStsTimeout uint64 `json:"aws_sts_timeout"` + BufferedLogMax uint64 `json:"buffered_log_max"` + CarverBlockSize uint32 `json:"carver_block_size"` + CarverCompression bool `json:"carver_compression"` + CarverContinueEndpoint string `json:"carver_continue_endpoint"` + CarverDisableFunction bool `json:"carver_disable_function"` + CarverExpiry uint32 `json:"carver_expiry"` + CarverStartEndpoint string `json:"carver_start_endpoint"` + ConfigAcceleratedRefresh uint64 `json:"config_accelerated_refresh"` + ConfigCheck bool `json:"config_check"` + ConfigDump bool `json:"config_dump"` + ConfigEnableBackup bool `json:"config_enable_backup"` + ConfigPath string `json:"config_path"` + ConfigPlugin string `json:"config_plugin"` + ConfigRefresh uint64 `json:"config_refresh"` + ConfigTlsEndpoint string `json:"config_tls_endpoint"` + ConfigTlsMaxAttempts uint64 `json:"config_tls_max_attempts"` + Daemonize bool `json:"daemonize"` + DatabaseDump bool `json:"database_dump"` + DatabasePath string `json:"database_path"` + DecorationsTopLevel bool `json:"decorations_top_level"` + DisableAudit bool `json:"disable_audit"` + DisableCaching bool `json:"disable_caching"` + DisableCarver bool `json:"disable_carver"` + DisableDatabase bool `json:"disable_database"` + DisableDecorators bool `json:"disable_decorators"` + DisableDistributed bool `json:"disable_distributed"` + DisableEnrollment bool `json:"disable_enrollment"` + DisableEvents bool `json:"disable_events"` + DisableExtensions bool `json:"disable_extensions"` + DisableHashCache bool `json:"disable_hash_cache"` + DisableLogging bool `json:"disable_logging"` + DisableReenrollment bool `json:"disable_reenrollment"` + DisableTables string `json:"disable_tables"` + DisableWatchdog bool `json:"disable_watchdog"` + DistributedDenylistDuration uint64 `json:"distributed_denylist_duration"` + DistributedInterval uint64 `json:"distributed_interval"` + DistributedLoginfo bool `json:"distributed_loginfo"` + DistributedPlugin string `json:"distributed_plugin"` + DistributedTlsMaxAttempts uint64 `json:"distributed_tls_max_attempts"` + DistributedTlsReadEndpoint string `json:"distributed_tls_read_endpoint"` + DistributedTlsWriteEndpoint string `json:"distributed_tls_write_endpoint"` + DockerSocket string `json:"docker_socket"` + EnableExtensionsWatchdog bool `json:"enable_extensions_watchdog"` + EnableFileEvents bool `json:"enable_file_events"` + EnableForeign bool `json:"enable_foreign"` + EnableNumericMonitoring bool `json:"enable_numeric_monitoring"` + EnableTables string `json:"enable_tables"` + EnableWatchdogDebug bool `json:"enable_watchdog_debug"` + EnrollAlways bool `json:"enroll_always"` + EnrollSecretEnv string `json:"enroll_secret_env"` + EnrollSecretPath string `json:"enroll_secret_path"` + EnrollTlsEndpoint string `json:"enroll_tls_endpoint"` + Ephemeral bool `json:"ephemeral"` + EsFimEnableOpenEvents bool `json:"es_fim_enable_open_events"` + EventsExpiry uint64 `json:"events_expiry"` + EventsMax uint64 `json:"events_max"` + EventsOptimize bool `json:"events_optimize"` + ExperimentList string `json:"experiment_list"` + ExtensionsAutoload string `json:"extensions_autoload"` + ExtensionsDefaultIndex bool `json:"extensions_default_index"` + ExtensionsInterval string `json:"extensions_interval"` + ExtensionsRequire string `json:"extensions_require"` + ExtensionsSocket string `json:"extensions_socket"` + ExtensionsTimeout string `json:"extensions_timeout"` + Force bool `json:"force"` + HashCacheMax uint32 `json:"hash_cache_max"` + HostIdentifier string `json:"host_identifier"` + IgnoreTableExceptions bool `json:"ignore_table_exceptions"` + Install bool `json:"install"` + KeychainAccessCache bool `json:"keychain_access_cache"` + KeychainAccessInterval uint32 `json:"keychain_access_interval"` + LoggerEventType bool `json:"logger_event_type"` + LoggerKafkaAcks string `json:"logger_kafka_acks"` + LoggerKafkaBrokers string `json:"logger_kafka_brokers"` + LoggerKafkaCompression string `json:"logger_kafka_compression"` + LoggerKafkaTopic string `json:"logger_kafka_topic"` + LoggerMinStatus int32 `json:"logger_min_status"` + LoggerMinStderr int32 `json:"logger_min_stderr"` + LoggerMode string `json:"logger_mode"` + LoggerNumerics bool `json:"logger_numerics"` + LoggerPath string `json:"logger_path"` + LoggerPlugin string `json:"logger_plugin"` + LoggerRotate bool `json:"logger_rotate"` + LoggerRotateMaxFiles uint64 `json:"logger_rotate_max_files"` + LoggerRotateSize uint64 `json:"logger_rotate_size"` + LoggerSnapshotEventType bool `json:"logger_snapshot_event_type"` + LoggerStderr bool `json:"logger_stderr"` + LoggerSyslogFacility int32 `json:"logger_syslog_facility"` + LoggerSyslogPrependCee bool `json:"logger_syslog_prepend_cee"` + LoggerTlsBackoffMax uint64 `json:"logger_tls_backoff_max"` + LoggerTlsCompress bool `json:"logger_tls_compress"` + LoggerTlsEndpoint string `json:"logger_tls_endpoint"` + LoggerTlsMaxLines uint64 `json:"logger_tls_max_lines"` + LoggerTlsMaxLinesize uint64 `json:"logger_tls_max_linesize"` + LoggerTlsPeriod uint64 `json:"logger_tls_period"` + Logtostderr bool `json:"logtostderr"` + Nullvalue string `json:"nullvalue"` + NumericMonitoringFilesystemPath string `json:"numeric_monitoring_filesystem_path"` + NumericMonitoringPlugins string `json:"numeric_monitoring_plugins"` + NumericMonitoringPreAggregationTime uint64 `json:"numeric_monitoring_pre_aggregation_time"` + PackDelimiter string `json:"pack_delimiter"` + PackRefreshInterval uint64 `json:"pack_refresh_interval"` + Pidfile string `json:"pidfile"` + ProxyHostname string `json:"proxy_hostname"` + ReadMax uint64 `json:"read_max"` + ScheduleDefaultInterval uint64 `json:"schedule_default_interval"` + ScheduleEpoch uint64 `json:"schedule_epoch"` + ScheduleLognames bool `json:"schedule_lognames"` + ScheduleMaxDrift uint64 `json:"schedule_max_drift"` + ScheduleReload uint64 `json:"schedule_reload"` + ScheduleSplayPercent uint64 `json:"schedule_splay_percent"` + ScheduleTimeout uint64 `json:"schedule_timeout"` + SpecifiedIdentifier string `json:"specified_identifier"` + Stderrthreshold int32 `json:"stderrthreshold"` + TableDelay uint64 `json:"table_delay"` + ThriftStringSizeLimit int32 `json:"thrift_string_size_limit"` + ThriftTimeout uint32 `json:"thrift_timeout"` + ThriftVerbose bool `json:"thrift_verbose"` + TlsClientCert string `json:"tls_client_cert"` + TlsClientKey string `json:"tls_client_key"` + TlsDisableStatusLog bool `json:"tls_disable_status_log"` + TlsEnrollMaxAttempts uint64 `json:"tls_enroll_max_attempts"` + TlsEnrollMaxInterval uint64 `json:"tls_enroll_max_interval"` + TlsHostname string `json:"tls_hostname"` + TlsServerCerts string `json:"tls_server_certs"` + TlsSessionReuse bool `json:"tls_session_reuse"` + TlsSessionTimeout uint32 `json:"tls_session_timeout"` + Uninstall bool `json:"uninstall"` + Verbose bool `json:"verbose"` + WatchdogDelay uint64 `json:"watchdog_delay"` + WatchdogForcedShutdownDelay uint64 `json:"watchdog_forced_shutdown_delay"` + WatchdogLatencyLimit uint64 `json:"watchdog_latency_limit"` + WatchdogLevel int32 `json:"watchdog_level"` + WatchdogMemoryLimit uint64 `json:"watchdog_memory_limit"` + WatchdogUtilizationLimit uint64 `json:"watchdog_utilization_limit"` + YaraDelay uint32 `json:"yara_delay"` + + // embed the os-specific structs + OsqueryCommandLineFlagsLinux + OsqueryCommandLineFlagsWindows + OsqueryCommandLineFlagsMacOS + OsqueryCommandLineFlagsHidden +} diff --git a/server/fleet/agent_options_test.go b/server/fleet/agent_options_test.go index 103901d7a1..0e199298b6 100644 --- a/server/fleet/agent_options_test.go +++ b/server/fleet/agent_options_test.go @@ -184,6 +184,16 @@ func TestValidateAgentOptions(t *testing.T) { "orbit": "foobar" } }`, true, ``}, + {"setting osquery 5.12.X flag in config.options and command_line_flags", `{ + "config": { + "options": { + "logger_tls_backoff_max": 100 + } + }, + "command_line_flags": { + "logger_tls_backoff_max": 200 + } + }`, true, ``}, } for _, c := range cases { diff --git a/tools/osquery-agent-options/README.md b/tools/osquery-agent-options/README.md index 62b0e5bb3f..081f2e7f79 100644 --- a/tools/osquery-agent-options/README.md +++ b/tools/osquery-agent-options/README.md @@ -1,12 +1,12 @@ # osquery-agent-options -This directory contains a script (a Go command) that generates the struct needed to unmarshal the Agent Options' `options` values that the current version of osquery supports. It extracts this information from `osqueryd --help` to identify which osquery command-line flags can be set via the options and which are only for the command-line (i.e. require a restart), and running a query in `osqueryi` to get the data type of those options. +This directory contains a script (a Go command) that generates the struct needed to unmarshal the Agent Options' `options` values that the current version of osquery supports. It extracts this information from `osqueryd --help` to identify which osquery command-line flags can be set via the options and which are only for the command-line (i.e. require a restart), and running a query in `osqueryi` (`osqueryd -S`) to get the data type of those options. -It prints the resulting Go code to stdout (the `osqueryOptions` and the `osqueryCommandLineFlags` structs), you can just copy it and insert it in the proper location in the source code to replace the existing struct (in `server/fleet/agent_options.go`). +It writes the resulting Go code to stdout (the `osqueryOptions` and the `osqueryCommandLineFlags` structs) to a file provided as argument. -Note that the latest version of osquery should be installed for this tool to work properly (`osqueryd` and `osqueryi` must be in your $PATH). +This command only supports macOS. -The system that you use to run this on makes a difference. On 5.11.0, this flow was run on macOS. +Whenever there's a new version of osquery, just update the variable `osqueryVersion`. ## OS-specific flags diff --git a/tools/osquery-agent-options/main.go b/tools/osquery-agent-options/main.go index bc644b56ad..1426cceba2 100644 --- a/tools/osquery-agent-options/main.go +++ b/tools/osquery-agent-options/main.go @@ -1,26 +1,41 @@ package main import ( + "archive/tar" "bufio" "bytes" + "compress/gzip" "encoding/json" + "errors" + "fmt" + "io" "log" + "net/http" + "net/url" "os" "os/exec" + "path/filepath" "regexp" + "runtime" "strings" "text/template" + "github.com/fleetdm/fleet/v4/orbit/pkg/constant" + "github.com/fleetdm/fleet/v4/pkg/download" "github.com/fleetdm/fleet/v4/server/fleet" ) var ( - rxOption = regexp.MustCompile(`\-\-(\w+)\s`) + rxOption = regexp.MustCompile(`\-\-(\w+)\s`) + osqueryVersion = "5.12.2" structTpl = template.Must(template.New("struct").Funcs(template.FuncMap{ "camelCase": camelCaseOptionName, - }).Parse(` -// NOTE: generate automatically with ` + "`go run ./tools/osquery-agent-options/main.go`" + ` + }).Parse(`// Automatically generated by tools/osquery-agent-options for osquery {{ .OsqueryVersion }}. DO NOT EDIT! +// To update flags for a new osquery version, update the osqueryVersion variable in +// "tools/osquery-agent-options/main.go" and run "cd server/fleet/ && go generate". +package fleet + type osqueryOptions struct { {{ range $name, $type := .Options }} {{camelCase $name}} {{$type}} ` + "`json:\"{{$name}}\"`" + `{{end}} @@ -31,7 +46,6 @@ type osqueryOptions struct { {{ range $name, $type := .Options }} OsqueryCommandLineFlagsHidden } -// NOTE: generate automatically with ` + "`go run ./tools/osquery-agent-options/main.go`" + ` type osqueryCommandLineFlags struct { {{ range $name, $type := .Flags }} {{camelCase $name}} {{$type}} ` + "`json:\"{{$name}}\"`" + `{{end}} @@ -45,11 +59,35 @@ type osqueryCommandLineFlags struct { {{ range $name, $type := .Flags }} ) type templateData struct { - Options map[string]string - Flags map[string]string + OsqueryVersion string + Options map[string]string + Flags map[string]string } func main() { + fmt.Printf("Generating osquery flags for version: %s\n", osqueryVersion) + if runtime.GOOS != "darwin" { + log.Fatal("Currently only supported on macOS") + } + urlStr := fmt.Sprintf("https://tuf.fleetctl.com/targets/osqueryd/macos-app/%s/osqueryd.app.tar.gz", osqueryVersion) + osqueryTUFURL, err := url.Parse(urlStr) + if err != nil { + log.Fatalf("parse osquery TUF URL: %q: %s", urlStr, err) + } + tmpDir, err := os.MkdirTemp("", "") + if err != nil { + log.Fatalf("create temp dir: %s", err) + } + defer os.RemoveAll(tmpDir) + osquerydAppTarGzPath := filepath.Join(tmpDir, "osqueryd.app.tar.gz") + if err := download.Download(http.DefaultClient, osqueryTUFURL, osquerydAppTarGzPath); err != nil { + log.Fatalf("download osqueryd.app.tar.gz to %s: %s", osquerydAppTarGzPath, err) + } + if err := extractTarGz(osquerydAppTarGzPath); err != nil { + log.Fatalf("extract tar.gz %q: %s", osquerydAppTarGzPath, err) + } + osquerydPath := filepath.Join(filepath.Dir(osquerydAppTarGzPath), "osquery.app", "Contents", "MacOS", "osqueryd") + // marshal/unmarshal the OS-specific structs into a map so we have all their // keys and we can ignore them in the auto-generated structs (because we // can't auto- generate those, we'd only see the ones that exist on the @@ -71,7 +109,7 @@ func main() { } // get the list of flags that are valid as configuration options - b, err = exec.Command("osqueryd", "--help").Output() + b, err = exec.Command(osquerydPath, "--help").Output() if err != nil { log.Fatalf("failed to run osqueryd --help: %v", err) } @@ -118,7 +156,7 @@ func main() { Name string Type string } - b, err = exec.Command("osqueryi", "--json", "SELECT name, type FROM osquery_flags").Output() + b, err = exec.Command(osquerydPath, "-S", "--json", "SELECT name, type FROM osquery_flags").Output() if err != nil { log.Fatalf("failed to run osqueryi query: %v", err) } @@ -159,9 +197,24 @@ func main() { } } - if err := structTpl.Execute(os.Stdout, templateData{Options: validOptions, Flags: validFlags}); err != nil { + outputFilePath := os.Args[1] + outputFile, err := os.OpenFile(outputFilePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) + if err != nil { + log.Fatalf("open output file %q: %s", outputFilePath, err) + } + defer outputFile.Close() + + if err := structTpl.Execute(outputFile, templateData{ + OsqueryVersion: osqueryVersion, + Options: validOptions, + Flags: validFlags, + }); err != nil { log.Fatalf("failed to execute template: %v", err) } + + if err := outputFile.Close(); err != nil { + log.Fatalf("close file %q: %s", outputFilePath, err) + } } func camelCaseOptionName(s string) string { @@ -171,3 +224,77 @@ func camelCaseOptionName(s string) string { } return strings.Join(parts, "") } + +// sanitizeArchivePath sanitizes the archive file pathing from "G305: Zip Slip vulnerability" +func sanitizeArchivePath(d, t string) (string, error) { + v := filepath.Join(d, t) + if strings.HasPrefix(v, filepath.Clean(d)) { + return v, nil + } + + return "", fmt.Errorf("%s: %s", "content filepath is tainted", t) +} + +// extractTagGz extracts the contents of the provided tar.gz file. +func extractTarGz(path string) error { + tarGzFile, err := os.OpenFile(path, os.O_RDONLY, 0o755) + if err != nil { + return fmt.Errorf("open %q: %w", path, err) + } + defer tarGzFile.Close() + + gzipReader, err := gzip.NewReader(tarGzFile) + if err != nil { + return fmt.Errorf("gzip reader %q: %w", path, err) + } + defer gzipReader.Close() + + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + switch { + case err == nil: + // OK + case errors.Is(err, io.EOF): + return nil + default: + return fmt.Errorf("tar reader %q: %w", path, err) + } + + // Prevent zip-slip attack. + if strings.Contains(header.Name, "..") { + return fmt.Errorf("invalid path in tar.gz: %q", header.Name) + } + + targetPath, err := sanitizeArchivePath(filepath.Dir(path), header.Name) + if err != nil { + return fmt.Errorf("sanitize failed: %s", err) + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(targetPath, constant.DefaultDirMode); err != nil { + return fmt.Errorf("mkdir %q: %w", header.Name, err) + } + case tar.TypeReg: + err := func() error { + outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, header.FileInfo().Mode()) + if err != nil { + return fmt.Errorf("failed to create %q: %w", header.Name, err) + } + defer outFile.Close() + + // Ignoring G110 because we are using this on tooling. + if _, err := io.Copy(outFile, tarReader); err != nil { //nolint:gosec + return fmt.Errorf("failed to copy %q: %w", header.Name, err) + } + return nil + }() + if err != nil { + return err + } + default: + return fmt.Errorf("unknown flag type %q: %d", header.Name, header.Typeflag) + } + } +} diff --git a/tools/osquery-agent-options/osquery_5.11.0_codeflags.txt b/tools/osquery-agent-options/osquery_5.12.2_codeflags.txt similarity index 99% rename from tools/osquery-agent-options/osquery_5.11.0_codeflags.txt rename to tools/osquery-agent-options/osquery_5.12.2_codeflags.txt index 36f32fcfa5..9b69ddc6c4 100644 --- a/tools/osquery-agent-options/osquery_5.11.0_codeflags.txt +++ b/tools/osquery-agent-options/osquery_5.12.2_codeflags.txt @@ -1,4 +1,3 @@ - alarm_timeout allow_unsafe alsologtostderr From 7a4a3c49390a2cd1dfe9fa004aaf5b9526f11dca Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:21:58 -0400 Subject: [PATCH 037/119] Fleet UI: Fix dot problem so UI renders responses for columns with dot notation (#19528) --- changes/19528-dot-notation-bug-on-queries | 1 + .../hosts/details/HostQueryReport/HQRTable/HQRTableConfig.tsx | 2 +- .../details/components/QueryReport/QueryReportTableConfig.tsx | 2 +- .../edit/components/QueryResults/QueryResultsTableConfig.tsx | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changes/19528-dot-notation-bug-on-queries diff --git a/changes/19528-dot-notation-bug-on-queries b/changes/19528-dot-notation-bug-on-queries new file mode 100644 index 0000000000..2475c7d866 --- /dev/null +++ b/changes/19528-dot-notation-bug-on-queries @@ -0,0 +1 @@ +* Fix queries with dot notation in the column name to show results \ No newline at end of file diff --git a/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTableConfig.tsx b/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTableConfig.tsx index d5252ec5df..4939263100 100644 --- a/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTableConfig.tsx +++ b/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTableConfig.tsx @@ -31,7 +31,7 @@ const generateColumnConfigs = (rows: IWebSocketData[]): IHQRTTableColumn[] => isSortedDesc={headerProps.column.isSortedDesc} /> ), - accessor: colName, + accessor: (data) => data[colName], Cell: (cellProps: ITableStringCellProps) => { if (typeof cellProps?.cell?.value !== "string") return null; diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx index b46383ecf6..0294dffe5c 100644 --- a/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx +++ b/frontend/pages/queries/details/components/QueryReport/QueryReportTableConfig.tsx @@ -65,7 +65,7 @@ const generateReportColumnConfigsFromResults = ( isSortedDesc={headerProps.column.isSortedDesc} /> ), - accessor: key, + accessor: (data) => data[key], Cell: (cellProps: ITableCellProps) => { if (typeof cellProps.cell.value !== "string") return null; diff --git a/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx index fd29edceb4..3f45c702d6 100644 --- a/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx +++ b/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx @@ -50,7 +50,7 @@ const generateColumnConfigsFromRows = >( isSortedDesc={headerProps.column.isSortedDesc} /> ), - accessor: colName, + accessor: (data) => data[colName], Cell: (cellProps: CellProps) => { const val = cellProps?.cell?.value; return !!val?.length && val.length > 300 From aa60ce05372d761b59ff704e713f1d59dbaa2a4d Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 10 Jun 2024 15:24:23 -0500 Subject: [PATCH 038/119] Website: Update modals on mobile safari (#19628) Related to: https://github.com/fleetdm/fleet/issues/19584 Closes: https://github.com/fleetdm/fleet/issues/19624 Changes: - Updated the modal component to remove a workaround for an ios 11 bug that has been resolved in the versions of IOS that the Fleet website supports --- .../assets/js/components/modal.component.js | 74 ------------------- 1 file changed, 74 deletions(-) diff --git a/website/assets/js/components/modal.component.js b/website/assets/js/components/modal.component.js index 8d8e682d3c..ce047ba382 100644 --- a/website/assets/js/components/modal.component.js +++ b/website/assets/js/components/modal.component.js @@ -34,7 +34,6 @@ parasails.registerComponent('modal', { // but still.... better safe than sorry!) _bsModalIsAnimatingOut: false, - isMobileSafari: false,//« more on this below originalScrollPosition: undefined,//« more on this below }; }, @@ -60,25 +59,6 @@ parasails.registerComponent('modal', { // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ beforeMount: function() { - // If this is mobile safari, make note of it. - this.isMobileSafari = (typeof bowser !== 'undefined') && bowser.mobile && bowser.safari; - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ^^So there's a bug in mobile safari that misplaces the caret when the keyboard opening - // causes the page to scroll, so we need to do some special tricks to keep it from getting ugly. - // It's only in iOS 11... we think. Hopefully it will be fixed. - // In the mean time, we have to get wacky. - // - // > More info about the bug here: - // > https://github.com/twbs/bootstrap/issues/24835#issuecomment-345974819 - // > https://stackoverflow.com/questions/46567233/how-to-fix-the-ios-11-input-element-in-fixed-modals-bug?rq=1 - // - // FUTURE: maybe the bug will be fixed and we can remove this someday? - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if(this.isMobileSafari) { - // Get our original scroll position before opening the modal and save it for later. - this.originalScrollPosition = $(window).scrollTop(); - } }, mounted: function(){ // ^^ Note that this is not an `async function`. @@ -100,26 +80,6 @@ parasails.registerComponent('modal', { // the parent logic can use this event to update its scope.) $(this.$el).on('hide.bs.modal', ()=>{ - // Undo any mobile safari workarounds we may have added. - // (i.e. shed the wackiness) - if(this.isMobileSafari) { - // Remove style overrides on our modal dialog. - $(this.$el).css({ - 'overflow-y': '', - 'position': '', - 'left': '', - 'top': '', - }); - - // Beckon to our siblings so they come out of hiding - this.$get().parent().children().not(this.$el).css({ - 'display': '' - }); - - // Scroll to our original position when the modal was summoned. - window.scrollTo(0, this.originalScrollPosition); - }//fi - this._bsModalIsAnimatingOut = true; this.$emit('close'); @@ -131,40 +91,6 @@ parasails.registerComponent('modal', { // us to do cool things like auto-focus the first input in a form modal. $(this.$el).on('shown.bs.modal', ()=>{ - // If this is mobile safari, let's get wacky. - if(this.isMobileSafari) { - // Scroll to the top of the page. - window.scrollTo(0, 0); - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ^^FUTURE: Don't actually do this -- instead, try setting `top` of the - // modal to whatever the original scrollTop of our window was. This - // eliminates the need for auto-scrolling to the top and ripping you out - // of the context you were in before the modal opens. It would also allow - // us to keep the nice animation when opening/closing modals on iOS. - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Hide siblings to lop off any extra space at the bottom. - this.$get().parent().children().not(this.$el).css({ - 'display': 'none' - }); - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ^^FUTURE: Instead of just hiding siblings, which isn't perfect and won't - // always work for everyone, try grabbing outerHeight of the modal element - // and using that to set an explicit height for the body. - // (but also be sure to handle the case where the body is short!) - // But for now, this should work as long as we have sticky footer styles. - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Hard code some style overrides on our modal dialog. - // Without these, it gets weird. - $(this.$el).css({ - 'overflow-y': 'auto!important', - 'position': 'absolute', - 'left': '0', - 'top': '0', - }); - }//fi - // Focus our "focus-first" field, if relevant. // (but not on mobile, because it can get weird) if(typeof bowser !== 'undefined' && !bowser.mobile && this.$find('[focus-first]').length > 0) { From 3fa3f507b53a7e7b0f2bd64b2d706844feea936a Mon Sep 17 00:00:00 2001 From: JD Date: Mon, 10 Jun 2024 14:24:37 -0700 Subject: [PATCH 039/119] Article: Fleet 4.51.0 release (#19630) --- articles/fleet-4.51.0.md | 114 ++++++++++++++++++ .../articles/fleet-4.51.0-1600x900@2x.png | Bin 0 -> 51826 bytes 2 files changed, 114 insertions(+) create mode 100644 articles/fleet-4.51.0.md create mode 100644 website/assets/images/articles/fleet-4.51.0-1600x900@2x.png diff --git a/articles/fleet-4.51.0.md b/articles/fleet-4.51.0.md new file mode 100644 index 0000000000..7f43b9f66b --- /dev/null +++ b/articles/fleet-4.51.0.md @@ -0,0 +1,114 @@ +# Fleet 4.51.0 | Global activity webhook, macOS TCC table, and software self-service. + +![Fleet 4.51.0](../website/assets/images/articles/fleet-4.51.0-1600x900@2x.png) + +Fleet 4.51.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.51.0) or continue reading to get the highlights. +For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. + +## Highlights + +* Global activity webhook +* macOS TCC table +* Software self-service +* Simplified APNs and ABM token uploads + + +## Global activity webhook + +Fleet adds webhook support for global activities, broadening automation and real-time notification capabilities. This feature allows IT administrators to set up webhooks triggered by specific events within Fleet, such as changes in MDM features or re-enrollment activities. This also supports reporting mechanisms, enabling administrators to monitor the alignment between the number of devices enrolled and employees onboarded. + +This update enhances operational efficiency by automating workflows and providing timely data, helping administrators manage device configurations and compliance more effectively. By leveraging webhooks for these critical events, Fleet ensures that administrators can maintain continuous oversight and respond swiftly to changes, ultimately bolstering the organization's device management and security frameworks. + + +## macOS TCC table + +Fleet adds to its monitoring capabilities for macOS devices with support for querying the macOS TCC (Transparency, Consent, and Control) databases. This gives administrators valuable insights into applications' permissions on individual devices, particularly concerning accessing sensitive user data. The TCC framework is a critical component of macOS, designed to safeguard user privacy by managing app permissions across the system. With this update, Fleet enables IT teams to audit and verify that applications comply with organizational policies and privacy standards by accessing detailed, granular permission settings. This capability is essential for maintaining stringent security and privacy protocols, ensuring that only authorized applications can access sensitive information, and enhancing organizations' overall security posture by utilizing macOS within their fleets. + + +## Software self-service + +Fleet aims to streamline the software installation process across organizations through software self-service. IT administrators can easily add software packages to Fleet and make them available for end-users to install via Fleet Desktop. Administrators can offer a curated list of pre-approved and organizationally vetted software directly to users, simplifying the installation process and ensuring compliance with organizational software standards. This addition not only empowers users by providing them with the autonomy to install necessary applications as needed but also ensures that all software deployed across the organization is secure and authorized, thereby maintaining high standards of IT security and operational efficiency. + + +## Simplified APNs and ABM token uploads + +Fleet has simplified the integration of Apple Push Notification service (APNs) certificates and Apple Business Manager (ABM) tokens directly through its user interface. This update marks a significant shift from the previous requirement of using `fleetctl` commands and environmental variables for these tasks. IT administrators can effortlessly upload APNs certificates and ABM tokens via the Fleet UI, enhancing the setup process for managing Apple devices within their networks. This streamlined approach reduces the complexity of configuring necessary services for device management. It accelerates the deployment process, allowing administrators to focus more on strategic tasks than manual configurations. \ + + +For self-managed users, the integration of these certificates requires a server private key, which is essential for activating macOS MDM features within Fleet. See Fleet's documentation for guidance on [configuring a private key](https://fleetdm.com/learn-more-about/fleet-server-private-key), which provides detailed instructions and best practices. + + + +## Changes + +### Endpoint Operations +- Added support for environment variables in configuration profiles for GitOps. +- `fleetctl gitops --dry-run` now errors on duplicate (or conflicting) global/team enroll secrets. +- Added `activities_webhook` configuration option to allow for a webhook to be called when an activity is recorded. This can be used to send activity data to external services. If the webhook response is a 429 error code, the webhook retries for up to 30 minutes. +- Added Tuxedo OS to the Linux distribution platform list. + +### Device Management (MDM) +- **NOTE:** Added new required Fleet server config environment variable when MDM is enabled, + `FLEET_SERVER_PRIVATE_KEY`. This variable contains the private key used to encrypt the MDM + certificates and keys stored in Fleet. Learm more at + https://fleetdm.com/learn-more-about/fleet-server-private-key. +- Added MDM support for iPhone/iPad. +- Added software self-service support. +- Added query parameter `self_service` to filter the list of software titles and the list of a host's software so that only those available to install via self-service are returned. +- Added the device-authenticated endpoint `POST /device/{token}/software/install/{software_title_id}` to self-install software. +- Added new endpoints to configure ABM keypairs and tokens. +- Added `GET /fleet/mdm/apple/request_csr` endpoint, which returns the signed APNS CSR needed to activate Apple MDM. +- Added the ability to automatically log off and lock out `Administrator` users on Windows hosts. +- Added clearer error messages when attempting to set up Apple MDM without a server private key configured. +- Added UI for the global and host activities for self-service software installation. +- Updated UI to support new workflows for macOS MDM setup and credentials. +- Updated UI to support software self-service features. +- Updated UI controls page language and hid CTA button for users without access to turn on MDM. + +### Vulnerability Management +- Updated the CIS policies for Windows 11 Enterprise from v2.0.0 (03-07-2023) to v3.0.0 (02-22-2024). +- Fleet now detects Ubuntu kernel vulnerabilities from the Canonical OVAL feed. +- Fleet now detects and reports vulnerabilities on Firefox ESR editions on macOS. + +### Bug fixes and improvements +- Fixed a bug that might prevent enqueuing commands to renew SCEP certificates if the host was enrolled more than once. +- Prevented the `host_id`s field from being returned from the list labels endpoint. +- Improved software ingestion performance by deduplicating incoming software. +- Placed all form field label tooltips on top. +- Fixed a number of related issues with the filtering and sorting of the queries table. +- Added various optimizations to the rendering of the queries table. +- Fixed host query page styling bugs. +- Fixed a UI bug where "Wipe" action was not being hidden from observers. +- Fixed UI bug for builtin label names for selecting targets. +- Removed references to Administrator accounts in the comments of the Windows lock script. + +## Fleet 4.50.2 (May 31, 2024) + +### Bug fixes + +* Fixed a critical bug where S3 operation were not possible on a different AWS account. + +## Fleet 4.50.1 (May 29, 2024) + +### Bug fixes + +* Fixed a bug that might prevent enqueing commands to renew SCEP certificates if the host was enrolled more than once. +* Fixed a bug by preventing the `host_id`s field from being returned from the list labels endpoint. +* Fixed a number of related issues with the filtering and sorting of the queries table. +* Added various optimizations to the rendering of the queries table. +* Fixed a bug where Bulk Host Delete and Transfer now support status and labelID filters together. +* Added the ability to automatically log off and lock out `Administrator` users on Windows hosts. +* Removed references to Administrator accounts in the comments of the Windows lock script. + + + +## Ready to upgrade? + +Visit our [Upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs for instructions on updating to Fleet 4.51.0. + + + + + + + diff --git a/website/assets/images/articles/fleet-4.51.0-1600x900@2x.png b/website/assets/images/articles/fleet-4.51.0-1600x900@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..573d63cb76082f093b40092cec9187469f30b673 GIT binary patch literal 51826 zcmeFZcRZE<|37}o%m^iWmC6bUQPwG0DP&|*WD~M74hMy-Ly0IOd&`y?SxNTDo@MXN zanAR;4yX6)^Y`!e`>*SEOXoV*^?W`bkH_;d?vKag$!k?*c?wcSQUCxb6cz5O0RTAy z0EkIQh`?{qkM%^s|4!R0=r{rZ?Pcg6o}${NHSkM3M>Y97KwdlZJa|K3e*3{~04P9` zp`H)|Kr6B0-P;;3@Ro*e;{j9vAXvN>DTle+Ti)Dn;+Sv4_x%sl-2( z_!lH`I`A(@o=D=KO8ip^;9rnDQG$O#5@!wnRN|jX{0ovNI`A(@;@rYNmH4L;|AHjW z8vX^z6G{A2iT@8Okx*S3{4@LWrUZId@2q%1onX@yv2dy>mXAEIquQd%o;)HK)-aai ziFqz){5V^)FH1ajg_f^+-YLCr>}vLMNU=El%bdbBwGwp^{iQ;lz?eLux^B&H{7)3- zWB?gpVq_k zQ(#-;?}eBlVE=mqe)xVG_TSq#=MewBy~GLN{pVgm`Tw*r!ao88|9Sdv;eVq3r(CfA zSOoCToc=kGe=hT1kob>Jfc*;+|ANH7An_lI0RExOf4KTzCh;$6`;Slfm!|$p%>REZ zMd;!KK;mtut@zOP#g2G!`C=pID?_*1C29C1^~5_qNOjW)sXMG#1g+yg5QJ}yxOuCq zve_=P6xcMvid}ab-grxpZqC+!H%Z1g#fu3u7jdRf{Teg44Zmt_9g5zObuiKL*mAxa zRYW&9?#m))!hL z;snU<@PE3v$7PHhKB$-utKqfkYL+i?^~|F6wPA}3TsAr9@M1v&XJu=pY1WVI&~tV$ zu*7qzr*z9Er(*a1*0|@DTUv3il|E^o=JpLARVi)gr50>y*1XGh^x@g-u}`5TbJJ>E zk8X!mRY;C`&dUn6$QBl)3$f}}UhLQJqW+<)H>JjPC*0g$zJo;`$6sO>RAxfl-=(o39)f+U zphjd#?a^SlSx@dml{?{-`G%9t`!vDqcW^baL~1N$YF2u>FWDRm7J3{b6bnc;T6c9# z78KOiys}rntQHD*c~V_yc{MsVz+$9ZcKu+OPL{UuV$0h@Ipali1Wuq9G*G22(dpnu z+-nM>kD>ixRY;*m#+H@bZG(F)DVsw+^ao3|L8iP!7qc${Zb$j?LGuM&>`ApI4iXh! znl^LuAvr-*GZ6{sIFY~PT_Ax3*WFTaP!JAk<2t6MuYb*&o!5Ihj~=y5oJ1ZZ60XIH zd3K-q7JaCViHff zyw25{jpdRz1qq_fnbVd9zWtr7N-wgWI5blEC;=C z`nI6mJHl-GIV1I^*UW=`Ja1a+>7a)S9^;)dmv1(CZpME+D*$BOg%b4?5jO8)7mexK z{atcR=~FRC=_se&yXT$qv_BO${gQ=7w#{b;M}u>^n$;aHJ@}NG{OI?t-z0Y%Tg6v`vfRLZDED?Y(&&oK z3)}1Nbp9**^H_S~wvFD0?u;#<2hgQqfgdoV_xYSMgD!^88I%cT*{r#;27=Dx;rX&% z=Vlo{-lWzWlv@o^ntkN@IC7depvteBPNnIMD9X6kS-hvBFFIxVUPNibJK$X;{;V&= zhK9tGmJY;eo1^=4L*!IF+l~LKTH+FvFcBbrCe>idH&nvMQ#&Wb9P2FRkr^X z=FT`d&HGOWDxb-$h4s0P(+%3P&Yd1}!0_~LB zhO;{1m$qK@1WOz*Q4-&}m&7Ws1Ot;g5j!yTjd8 z*<95!{nBIEB*sy>^np1yuF~E5*uetV!u?_fEC%Z!E9O9lwp{Hd6X3p3d2amJcUvRaz`d{1tf}3^$*Ki-hSE>Z8CA7Gi1LL8m+NLRNmOOn|gqg&7y} zB40!5n-QG1$cMOk$(p$b{_XMS&moIk@F^r&#lyc=Tb-uuAFtuUJ#M`N@$!|p5dBd3hb|#I?t9G(keDa% ze=4hhZteI|mBvn$kr9#EQ&)e*{15K8ViK=H4&$q-sK>=UK3j(NmX@u|lUD&QvSV>QV53HOhR2-SU+HT2 zH2L@mXLFd`jZ*<+A0%x5Ct6CZRnT1wC;K2qHQxpjO!nc0kP*A%i+jNH3m%*(W!$d* z#xk{Fyd{F4PG07IgS}s^w18 z+p!PdW%g$K zCXT|EhtKOQ!+&%vdCH}!G{-ya;w*scg!*Lh4<;`62%bxsDB_1-M1MZ@+)LiY2ANZe zd_b_eyODADcZFcA#(nrUEb*plU@Ibxemp@mWWTXJ+HY69RCM)JVCN*Nv{K-N3)uG; z!(i~u_+Uf=8#5Q42R)5w*^%&zvhOm)(;!M~$s%EaxN!A&GNX+Xf8^`3TmV2+FpLf| z7pc1q@Gm^U^zcOtE^TQrt5mr>q_mU3ylq+eJ|OSwDj_7n_*N*1{$y;TJ~noYw%wN{ z$AmV&ahoQNHSzRDv;PUD+aRWl?kPA+AgD*kzPi!2)`eS-i^)zG6152GBRJwPfxo$0 zY-8a^$z`gGVsFPzJ zC`C5b&E_I#b|eoqA{(bGDG6s+zVZLh@p9VfK#P?fe&2XW8GUbXrH-islQO9VYb`_z z=4)45Z2b;4KAMi1Oa@=WUaE3f_pu`9dZT&GF_g(C()K7>wSnz0{>Z4SrL`Cw)};l@ z@y79EdIcMPe^r-UHcO99qJHO$_j_W~o8?@9e=_y?b@`Zn=|AVm*!6aicR7ZnZag`{ zDJYi+q|4!V3VaJZtfwWDXnt^->S6XwD>1W_$z30rnL-DY%F%+WuMGl>pS$og{xlI- z#dVGm*RffMWfgk5pMH2y49RULFYr55nzVUmCCIM>bBXc$4rk3d(;FbNG#W4EWc&__ z2FzyE|8i*tamoH}dE7d5Ff+}9Kpc~lO6wCjnul5c_8v@>LLZM8sWd(%lv>>Bdi|@y z`x0Xv8Ar2qHs@cyvHc=7Zr{M@nm$I&G7oF(5F6gzgJuDb-|MRl-wV1j3OU3L!v+%I zJXur2;%A!1)-y8-!7VN6IDImSDj}ANxURUL>qABwoMA8Wk5tMUR&x6eJUNk}O%M-7 zJ$>Y)LFJ~iMpD$8o7=V?))9Bl$GwjW&ac{Yb$D^`H%rpEtz^C%>dg$!{hfJy(6cxi%M;F5Fez zTyJLLRK{5>OcNWkikl3x9}%cEuwizkOB-Wi(lWBX%X}*l3D3IAzPT>H9+Q=2Mcv|w zDlo5#wH7{M4mOPiCYhR-Pl3tPWka-GuApO5#d>qqK1s@YTfV#;8q&_IJOn zV=$=N8+TEH`AgKGU!C9cqw`|5`zXe6G9#dg1uXX__u~K15B8s}W`e>s*DG+__x8Q* zLPz!VlBwh3>5&}brOkbdH==k>-P}W|Oud7I&zYXwMMK>p^iAI6p^_uH!I!J8#)rS| zdcudt#3bN6xFxdgL6If0DYGorcqa9aF3{}^Ul9qP=XkJQgz2T_7>-SjI8QB0twA%) zIciPBQ8YN8`C^-RTzdBi?JQi_W8U7>5f(B)D{@1>k0ChL5pxDJ*zUKU?(LCCn~z&W zL#Ib1TXfdT8uW1!5=35({9uMkw_N>&)iS6LIp96Q3`r{H+=DuCFcoO{X|#9ms?ONC zLFU2r=S!#`oW?sd8f#(boyPUfiJ)H+;j8A4a=bh}n(KZEt(fC>T$l(OVRU6t1qWka zw72J(G5g+NVsC>nw6;C+du32G-(Nnta;Wx_y?J*f2=2YD!7T1-_5{MW0mNE&jSiDC z%FY!zuA74z4~h+E?THasP3q>o6JDbC2SknsFWF`H3KqXd4aHEVYva!!HTajW?2KnA zkz7RWSu8O4(o8lZ4cT6E8ewGbx3}TwgC%33*Sm)=*(GJnjhBxcOBR(ZU9z|hF%FL8 z(gVip(lvv zI$xqO|N}THmb_ zfiR|g3nW)^7`7?!w7B>MGlEnPzhzL?AAl6B+B-5lAF&C zXAbDI8d>25&n!aAak@s!4uvFO;tg+MvOvTbrvBD)>Oz;KH!I1Ej|Qu%}&75!Ey+y&E(1P#?E2 zUBdCS$Q13vy;e3UZE@U_Xl%^~Kv3FxEl>OK?KZUf{LKORv9JgL*es+_TwzlXr_fbi z1!)KX=&Mr0B{qH&zhZ;#z)Mvg3r&IcMTqzWi5-O;<~g$CXFy`j@u(^#YERE;Yoibk zX=m$`XTZ%9+Bp+2Bj!^P0@Cyb21+;*TV$|EAI7)0x4DWAC|{1+#))gD2Z}0yaPh^1 z7dx4D!8m8lE=a}>J)l?u=h7C3K7towf+z&2$AtLZd@~n)ijc@qrH&i4s zRX&qX;`j5PSd)Bji+|cNW}SPquRl<+bKgv;$UJv@oVRP-6@Dxt#0E9idboTTdeWQT zMH^wa9%AC=X#_-m387cOOV#icJM=$clrI}MmRIO5 z@&kaChYw%X6GP|===#lhEDP1>_Gd`4p1gIC zMDCWDHU}3hmW)ac=e;mnEE?np?R(x!-E|%19jusM#c}weZH>ct>tSt0LS2~Fc;K?f zSYW%BAu=x>IUtGAg%y?WEd0Q&VsQo{6qfmbL7cl?3_ak^rXqG^>w`q6Tx${1^NzC3 z7WEY9f2K=Ep4V^U-?;x)t|8?uGGyMIxo>m-D`vi8Ax3NZdqQoLWZuwcPBMSkV9E~i zKZ59FCV+u9(XIFd>DW}uX;!mYRuqN&7J)w8I_lo}WJzb{6HaqV=cvuQ!9)cvo)M~; z&dM;W!c6Um-t7vY+52Tg8kVv=E!6ZhA)URYn{6rZog^}Dc!wy`X9uTd7W`NqxI-kK zKlQX2T5V{1y~<|)qxq{>d*D_5H>Km4+?zV|GYPeo!lh2t^yDL3>))n(gpl9s8T}3% z$H-q^Qu6*RB!&5P<2DuPgF~EU2(#gVAUUyVbF;SO5BP1wDIPL@3H~U3m*>T-LRZ=I)i#?$ z=GAdd$3hA#=nGguqpH2%+j1b8sb#}Yu_s`TFe?4p-8oWd?~vE|UZ_Y5$uxSx$k_+X z5!GXg>C6n_@|)7mI#_bRA3rnc&w=%An7Yy=@`J#a3y|t1S#(<~+p-TxuMPg{-ZmC~ zOGw8Oz1WZRQ+)Ey4qaBTp8mryB>(8KJS@K05|c61)c(`XZE2+Uu)NyZ+{t;lRZ1CY zAem6RTnpd);+L9g`ZR<=GHZ8xX|HcF^Sy+n=hZ4NoYj4;!K$Uc(lP@#43x!GsYS{F zdS6L8`V}mG@Sw+)!|-%2WwkBTddT{EQtfupiYJh z)nfDW%5D_PmqM7qvvOQG+W$365_K+T1FVq<+5{Uq}CqM(dPoouu0UPM=(_XA^KX>$mZjdsQ^2y>zrwN&BrM$3o;_p!ym$J!YSJY-=0o4n+!C& zb`+946-yaaIsXzBucz}SxWNMRCrPF_^&TYY-_QNG|t zOT zwMIJj0cjQFjqXyXPBfnvYPu_=Yv1L7w|(8j{U#HvKTj{=m9!cOu4*`!0ELEdvdp7o z9k|8ND+gnx^;ng>7`OYy%zYm*IUl{69!d_cA?hCeS!@1?)Tc8rsKoj3j4)_yr+wxT zhsYD*>VNRpw^XoI>fTfnOLvUr`_k# zI&6MZ$rc#EW`N6Dzkb5{O~yZ<2Q+T|rFc6Zzuau3Y5&bbvglUVcXVpXF$XzR$e=Z3!q&BJ=VLz#YS0 z%(t+<{z*`$%R@VJW{fqo{}EcV4|-h+YNL|F0_V-iDKkpGzrAvo0hG{Tv|bED z$6w(>0H}I|b|M=s)DBJS>GFmpr*AQLy1gYZ$;ge0dMKE;oxtKO&KDFof`78<_$s*oxfJ2M zq&MyDIm6nGfTwK_(WeKa`%Q!qs^aPI67gmx!`|VT?G2j;8z_k@8^4}Kh`No)n;>cJ z=YEtY2N3q&vp#)_omVRtR}@Ng!OFAZhe@UEScs=Qno+IEvK&zXIdGcMAWdsyli5Ew zDy-xo6=n~BFcMc(!Kv%LE$^cqK6Fya)B6k2Y>y$@OiOtI=oxZRE6Bfg!v}2vfMB+? z+hfBBAwEK}9B((A%VX9Ayisoa>&$d0?cecTSEh}s@eILqG@DMJ#g4=a#+ z-{M13u!|eQImF8O4+9vv-GQgmO+Wwdq@7fHE+jr%zhiPc#ghRB)2z>2zu+g z_&AbVZiybV4RZ1IF%{&Q7^h`O`KE&@(CwX0%z`G-A$l*sT5PC;V?^;fgwJ7=5OOZp?o5{-8uEjQ&vyz z|7gLzE=F}cF(_qTC?Zefs&QS(#|wY@Tgbz=OwY7dm4ojx3Bm^S#3w^w%UG5virl~L z`j+W}kn(PP?ocA$Q}4K8L!@&D<|4R5#UZ38uOskJTucF*xA=?fssLlPp2jTmm$04f z7LXVm9Yk2n-gylq`8>!TnY-=ax^d1}_$tvcDMnvBWVLWFe|?M%0*ng~)|}hkW|N|k zwco9^L*{9Xlc>@E>6;rV6~(>$`13Kn?-*glh%J!b-6RsVz3E`yQex_|qv2A7E~Ds9 z3hwO_c}x}~?s^`*4lq_42!2ST3v(Zqek57+w=A2;dhCs?cWA@;#6h?uz5bKyEFb$2 zF8l%+0+3wU>0-YSUOHNlwZddr{Gkm`mHw6aw=|8Yz{1bx7eo-yQb`b`Z;n53p?OH^ zeHGQD)MaxEL<j>NGfmRot~#`V7FQq;$5jZE`6tUqSfzdZ5#=?H!Xa=Lhx-gss5bnnj)^s_| zdA)IUgvoUG7ZPm6!&(y;`$Ub=g&U$}Dd9ir?QAG;Lo5ie%(~W~8$>-y1Cnw|mtF|g z?8OPl#^ku0sX)v;e8i1%n5XlOe`SChl}L>hqW(R+*S$n)H^Apd_z7523%__Y?(Csz zZG#IHKOuQBmX1!(hM8cP|EN{s4tDU!!73$R5M(*6NoUazo{j7Sg}`e-@)X@oXENGh zM3#7qkB zb|f=J>)V{HK}q-cuKYZU&fsz2KmP8Q_Z`RF)>42p->8TXMH`MWO9} zzP_kf_s$!=Qh2qc+eVG#>1c;HH5vaceqAJYyR>wVbiu7vZl!rSm$5RoaL2lrC)hn-pcoIXt+Md?D0c9&rHJOy{_Lt3AGBe~F_H(p zdD^@6)&(<^%3)AyyRj0CsgaaA>{P-7Oy3fT>w$;&L$+$DYSvETi9X_Q(9M{ZnyK{aw)TmH~L>z3$t9=G*z=!p%+ z`f9EYnUbNe)$c&1@9!f~$^8fg}X@3-S@v1N=c#9f6H+eP$gH+v`+ifY+j+nULy{+>q z$;RY3J&Ri>=)!dtn@4#|&Dms<4Q^FRLitK^%OB5~3he@`2lg+5Jo!_m6WZ|?I4Jzm z@wRDpGRU){Z!_u|l|7TeCMo_YY54T_EtmB>rIMYjGtZK`f$2tvuI?)1Xux%pWQdIG z&jh}p!)RwLNP>-7Lh*-`SHuRjUX9U$kpJG%cx=yS_7*UDHV-7c$S^-N>u6q~AByfR z#-@40ti%1C?12-+fUM?e&yV9{QSgyDGi2Qxud?EwaXsC0wnprpgA+Ohh+^yxTEA=Tm73b>8~)tcJ0+)!Fb91UBn%ae~nXvv3^ zP|#}jLM>}oPkQ*W;TL;T&7c%Hl7DA#7=J$P^|^Lm%ZS$Px?A^4u8LHnFPDSZOHO~> ztU(ga7%+}0-wxdDunkt4{iro`VH>S+>VlS`gJP+dI{ge9GkAf#9+9%`IKQ-62xwlW z>ay9Pe5tL1H38MDSQD^JfyoUi+uXcB=GW#qdsx)0>u$E#QTSE! zcHJ#D*5cC{vHrK!I;X86rCM!!jYNF?uv%uW6vHa$;j(Fgt z^8v?Jm+0cLb_dBq4woyA&p@Eb#GpEt9fW!@uVA1g=pnc}cmss3n(zeBABTZ3<>_rC z7^bu5O{h}Co4}$=OX<@ChHoYtnX7@0?yP!9R>rp~Zt{KO=F_DEpfB^LR;gaol_-4X z3b{i-mB3Q0dhml3krifgTefVRj#n%HyHwhj)NQBbbp9Xp8vW|dImaQZMH(CBd(|GB zZ2MzcO5w{)1d10Twj4-3Q)cZ3*QW%e(*)Jj<%eqaR)@X!&2yW{f7G)pT9H^Xc-?hc zcHa(={%Xavl;7fmw8HrAD_agP zc8pYXZ)3NB{W$MS+iL4iAZ&b7&vJ?th?KSBe1GV%>`><>M4mPhfeLp~p_J+)%Aw z9j>W+`;QSkW*ch{0LAmW*tf4`bR!PB^GDv;`u8p7UoWpYSoeT7GVxbVdQ)LM_MJqB zlc|Yb-`@7jB+pBqj*v>z?iMVH-;kj?o^L?~+FKA`HxWp0@UgrVF7_Jc+a$bAnh5&s zw%UWjrT022`$cwC4enT2gQz=(w}2|gAo~{hrZlPX%Ez5KZ7*|Le)fdCD%6|3m1(2r zA*AfpnA49y+XX@71Qrv3#|wOf6UPcrtA(m~ja}W>7owNX`uA@pcPACC`DYgrQ3Y{S z6Q`Mox|Z(VfyMzDBG3*40ODIXQ6C$;NVY!F-9dARjMh-P!@(x8c$IGKno6*8ovE#Y zch9i@CGF!9Y6&*Lf#+yQc33nLu3O$wJp)>nL;uW0PgQTrgmk#~@85L9zdJqduR7hw zQkW=*lN1-B6zH(Dtgb55#rH1Ufos%j3|(wZPc_wW@wBTnt(iU!qXAe90sxNzh~pPd z`*B1uA7G@2950k%vpZEP{4m2mFzuSFvIp%(SKe`;K2Cr^QIo!V6!=P1|GY5LX+%6~ zoWaq&jJV`oy+)=Yr^rGlbMYd?Gw^ki;kci%#XVYp-YjbyAm~cb)s<_s_3rUN(T#c; z*}nj9n^4)(S=-64QvoPRX$%z|KmRwq&j=$rmY-Bgl&@2>`y^#|=Rxw1RVRLC|9ki^ z*N7cwi_Iyn^&h8m8YikS!A?7NE#JJ7C(*by<@BlB0U3p>-5i(R!NMJnk!{wAd_N~d zMMdb6ie(5FgS*;Zo>$!8_CJ0LYE9}Hy+GEQI;JiH_KxLpsE66O`b9HyR zFaCNv)ib~sWES23P*=s$pzagZDJ%g9eDRCesbk--5tlrl{|MqXzRl#A)84AQ+yr11 z7W)vWJ|+{9CX%;(X_C?^D@KOBo~<|bq<&HL4gv*WaV7GlH2u||^rxuNk3XjXEhF(F z#|&U|X8?GRaTcwByhg7^lA;q!2EJ=kHhLXfk;5+2xrAHhBI-$zXU_w7*O zoRu>hy~DO;Z34gDqU-sz480s+)hTQ8q&~&PA=tSA3pps@fC5y%mZQw^F89ks>konN z{WQILr0RUQ!YEAW#LDlj`tIELwjit#6H8WzhhoYjwTM)jEhgK-%C2rKbHd*_Jh?0e?UuNsXn3+Uv!$2rv3b2$Y2zwQtflKw)avZK|Cx-$Fbc4EMdpB3veU5V=$RN$knwnv#qrx2EN?v zmn@ND>R(w#RPe0SW2XmvOtCDHZwHJIId z!%bp)EW15l-5Cw-#LcZbZsg8xuGi_Me%jvA@qXUY{N%Ha3UPFQ$6J+LBF zBhoj_Ip6DJZ2Ai`+aGf>Y`D%WO3@!iOMebJ*x_8!+ zd5b#KJ#I60*CfK_N-c%vB+b=ZjJuY1WyiEt>@-iS!fkcrJ&R0aW$uU{=4U#EbjRMR zHJAJeLiZlKi5G{mn;Gv!;K#zTxin(&fzO6wEy@~%X9w%O|~-R=qZ)wP49@`rV$^-%UB9~3&;5l$dB=g?H|;U`{L3w+GuI$S007Udv4e(YN$%Y-FE~}!5%v%Sw=JKb9-hKwDz(#oOLKz&qN&o!#iK2* zr|Pz^@cVe8-kwDFhm2u7Pr`e>B6P_i)S{DoyGT)HF`?Gft8Krmv z=pOy0++-DfFGX4c%Tf_%5FZupo_k=2)%(Z@Z0huad(UButro-gfMr|tv>!9P?{Ml} zBa+M)fZ?a&&02$TBKD!Fs-zv*N-MAnzHN2QqKRFBQM=6Zh@* z<;W~Lnv3q}E9XD)dRyWRrb*X_%#Rc^2h`v6_d@l0r4_epvA1s_)ju3pC%|LJU}RC{ z%G~B4ln$#|fJbO0d%5a<+<1kw!pB40(s}W%%7T`gB!Z|sf4@jJl~rS0tB4}kd&fVU z@m6vCkW;Lzsic8JUwztXhSYJ#x(BYNlUaqBc2GMCZyf6d*ITHAtV(4f^<0@2r9i>% zre%q*h#aQ%76%Y@4)aSpP`Hn+gKF=Dq5@%N8VtG^u(>oIo5pRcSFr2+hJ0n%wB3>zfH=Yg6TX**eyZ1ElF2BnA0EV|SCV1UE>)JzZNB(?)t`d zUPeXomhyhRe33kmc%yX;T`;pkOrE{_lf9=W*ssu+KJ3 zW$L(`zTg<&fzkLfpiU~HT7A8SIF&Y>Y{;D8>*T#}-&c=*b~NTtUj2E84b)aUc7{a5 z9ZJ8CUJG9a6F`paA7kC?O2~f8O@}SbRb-)i^B|`=9vv{zsoy#FXlRFW5-FqJPo-c5 z#9X(uU%g`ZXNPie9?U{Z!Omy${Xgyec0VYtmGjuL6`Aq>^nZ(#Zem%$BT^#uLN!J1 z3l=T6=bXO_C36$Y5U@h%D^f5tqvzcoy6~~xq)->aX^6Pr zQewuw2)=I}LOWo(&n(Vg<13%fvyqOHCDll!NYs5d(Y(J}Eoyy8f5Yi1pvpEcDO%#2 zB1d%_(v`B~3h(bo0Z-xB#xAXD0kSIa5RZ~Ap(*nOD9O<4{xJTy9#%d+KT-RkNA=sq zE~5wKe=20_CM(zfG^Seh-^7fR0&~a-Vpt6cgr$#>ayV17unzlFV3&O3Y47byJU}Bq z#FmivQ7$z);}Z>fZ?CFDPHP~K%Ma>^nRdUVCkGqq9d|TM)o!ADqoWwrK!0qVkhXC`~2wx{iI1L4Oas;saSz9hKR#XkG;p z=J+Sy{ia{WS!Kt z09qERKBNP}Ggr=1BM?RPp?9zgL%D`0BKe%v)V) zGv!*LD<8ca0M+{@a!BOn_#qOqYf#@{MaQk7l{slIZ{f-)Bq|D;UIv`Lo64Cn&CK4} zc{xPIsEn)i@v~5T)r9gYiEnveK_k*q(m=NmcH!E!!&0wuYYEuRD#v}>{Hc=x)2}NF4 zxbPATbOhU4{_J{IczCULzdlE-kBy`B?~5Hx0f}G0sC!}TFuvoo4@iD0Hm?_Mo6{MR z-btq>1jbfSXTcVU{QxS72b2J*9rH7$_K>qB-WENUA8DR?l$*%X3=%GvSoY#=@KT`K zKYE!ndxw0bgB7*r5!$*oadRG2g1uB#R;&~JW&yZWg2Lj}8;>SN>zCuU~=ebIgI-O%;Lp+ry` zZ;sW^xO}boHU&mthXT;B;7yED1L-_NGdS})fG&{d$Jaw?l*#%x3EAj=wXOm6;hU?Z ziJna%@$4wPuftV~OWDR#FQq{&g-AM9k14JT0e+y1CZYinr%*ekT==$x`wojNjc2C7 zaLae54?NM74AvNE40M$!r7JF4RK3e4V+CRi@{)O@($(O8MT!Y1Xy z4dU%mH^r_^(gS<%zF)TFEysAf5bS?;8TFrA7&W%o6A-O_=~;uw5vXSHkRS*Z(M- z&A>o>dNRzI4aprqAHt)Q90n+OrEEtsh6QDTMJ&raq0S|>>EO*N940hLZnhAOtYu*5a6`&hlYufIbA^tlfVYBkQAV9hQGo53$Amw)n>k2kfz16r>~9@;mz zvKu|3@q7&5xIl1IEm>JfX?-;atxz|t*P1bh7cN7gfwzCn=9j`GC2(f2vOS*8M7Pyp z6Rbujx{)A2Vx2{odIfKfg8<(Z@|9ApMnKEr8zpG3wlbLAP7Ce&aem`WB>@Wez5Zl zo-9Ed461js+s#hji{pwzj|GsF=q8Da;MmTu4X4X;ACgTrl z;+If*YC+Ez4F%2BN7e$hH0#|;br%%yzR{H(ah1ASLa|{(2_lp|VC>hr6?V;LyXal zA^NGO$Kj6*tmo>kS+yQVZ_OvNaW4si%_n7#-EbSDG^D(SL!OJvjtx_f)JpqKk*Wel z?W_GkG1F9mW5^W%nZon|uQt ziOvEs;8rYhi`)m>mG5j-txcN5@)Jpv#Y-fi0H|t2*UosB?&qz&f!cy9aaLW{)N39q z;bMz9Tp<^i4yVI{_vV`YnT790o;oe+l_oCsxp-V{Zu;YVAEDGvbp1uk4`Vkj9ZI}{ zJ<45L8T6G(Nt|pr9o-nqCIBAKMo3^?`+{<%EM^C-uRw(O)7O0+JmbV*+6mUnPZ52C z^(!#+tGgR%AMI7+`F$e4z|B=T_GCnwWPz7rb@x0PQRDgePk6a~|0hDaSOZcoePL^b zgk|x719*o-_HMaEu|8Fbz&YhgvH0=A>!meSS3nimz-Fr%l{DbrDcILCp39UcCcbhB zA11^!;BFk*2-9VJ%nlw)`LZyp3?Ab_`0*;uQME1&s8ouP`KU@;T3{Be0|>}9ymQ%@ z^>tk{ooDfdOjI?)9+LWwcv-vlH`EUwMSmY?GRc-=@Yr1e&rTpDu&FrX#P+O+{?>0g zX}1eNqE0AS3x4fO6kYGYR%yl})vd<+BC|dA;IIGFY-AeAU208YR2Y73Dp@#2U4|Z|u^2 z3b|}-NAEX(+o!-?xh9d4l^i!*28+2Mny$6?cyPAlsSOmaAvZ z$P6LzDZC|)oEl_)R8FU>?C!RWZ3>qJdrtUKo5zWyEL>|@`XXa&VlE<`{3XS@+ZAiB zACKevpT1Shec&nB%YIug2z0gy)54ZR3gQt&^8$%PaK$=gkFqW00;dRd$`0j0|B}o) z+xwfBtY6bxm=M~D9)9a9OHY|-hVQyy3g?q`QkwGQRuQhipf z0=g`v=10o^3|$a32M@mTk*C<2&mWCC9OmLvdN1)3-UQu*!?bxtwpEy7^I=jjLH$m& zE(}~@1MA5x-VY>rU$(AF)UrbV+OG9F3v@1pJrn$VrX&fE0FeX&9`N+cEv4Ku=JDt4 zVVNRR0#ZV2jx+OD?3Ct}N^HfAYRbfGwzYfjlSYiVSUX<`r}Vs5C9ss!TkS!r13*6ZA0%$!U{r)Z5#_4dBb+coe%m%xN{^Q?c=m$tBJ( zjE?7G!Akl1!ODxNfI2+#RUPY|;RA2OA!LaS8a%@9<2gTu#=pf)kU}_t>2dKydbe%`xpeXkr5!iy1A zqn7FMbl?0fbwL-!h&^#=Q7-CI#VEk%ekrzZCqJhjJk|LX0UZhk-1@qWuf6dBMGcu| z1~NN;*@Ft+*Sezpr4R%DvPX%PX+sbE<*n`k^r4aKKx%FhxmsXSELgG{U%U+M5?;*C z%3}~PhK2N~*n(v-gd7%qJ%0l3{@Hx5)j^9Jnfob=yyIX(L{AQ$$RS0@SujZQJZ4vx zEa1yP_TK3nV0UM{BigI7(Ya9;fM4hmeporZqgOe7nNLvj!`LH(L-}g>VgWiq2=0!_ zuO9q36$|#Vgu@}%5zZul?yGHJ5;qJs+i0D#vj%AfH)ZT)5C0h=BdZJL7LUd|6lLa! z4i&u)oEpwaqxxnu8U8daR%Vk@tUa<^-o8v|9slCu0{_uSO|0RYNsa^Wqn}qq9)&XV z+ko5%`ya2uO;TXHUY4w5gK1s(wZ2V+s&$7Jx_RAfYmfd(xJzehKN9Jcvnsxyj)&3Y z#rLut9H=l+H*c^lb7_uV4gFOuPM=55YSi#FF!2>E{1A55E*pSHMuVO; zti3e`>wt9=ihjf3nQ;M1zJWEs?{`ko0or}G$>POcR#8#q`0PhFv(Ic4kN!t_NSgl~x-O>qhk0QU9<{zIR5>eT?S8sc}$j~62H!xvJUFhxWZ%CK-ix|`JtDsLC_P?Gl3t=`*uAU<^k&lm9iMV^ z^$xw2tpLm72bxv>(_s`l91WnN3HxFd!P=C>CuJbsD?qDu*9HKNJY=)+dheTqCwvhS zOhWE=u8FBFHZM_9&TrnN6l*%@mE^K_i))g`dBc1;3aJ-2ic4u zU!DobEF9|tzC_<)|A00!3XC)Iz8I8t7TpO_*|!Mh@8GXujlb`jHUXvRTeseYqSskQ=f^A+D#Il&GNL_TR7It&Pw2Y z$PD!M4>FQ34pLTD`yEudT~w_fu*;>syF0&^aI#6{H(|jc10$G=!SzNpH{N-J2Ki*) z_i($T$}AEM(DC$j@b#%((T9ZM9dWp)(H0sk+aa5RjM zb|*(+qbI@b$8V)O>x)$sB>EFg^D>U3^-_{j+_y(q2DpEAS*~*rl=FJLNrHWOG}aiT zLLDg!mZ_|46=Wv z`DRc%@FX|A5>ff$b%M@HWw0+}YRwC8#T#x)subK*DKwqAf67GE`X6a_T#r_F`fQVS z+s|Km_AA@2%%pHgZ}-hhR%|G|SRhDxNF`vDaAsr3zWEUjRAsJge#yo3tUgUr)nA<0 zw>P?GInbd9M%{4;t~re-#p>#UoaH1G5xG64N5*Rul+5GYoB-B z;Tbx+dk(6zf+i%peJUH&@l>LEkm((Bvth6N{@hI4z}l%z&{e|JA#cJk<)B*Z;4H%* zd}n4*kANQopnQ@*=AbyFfuMy1NNRkhbi#(|1nCd}FC}|fn8HFbyWBqr>x#NOzG{XE zVbm6NW>%KUZc@$6F!5P5hPDi>hj=tUDZubc&~+z;HZDy9{C`iP^}yg4mb0?bnH|%GrUS#}+ z3Yx6c&0v+>FMN=YQ|T2*Wn7)}ejyt%>t< z(Ze-ilIpZn$h2S4QLNlzL?l@pU-Du zbx2`o3Oj$*V_m%z8N;Lt;8lzJ#ux^xHVA*!2kJE{nyqS4&4>&<}af{nQkYuaJ|V(nE}0_(r%N-h;)--x%KZJk>rQUu zR4HINGNy`BV-ii}E~WIYd2*PJq&_@$hAmmrzlk$qaQ%C_>y8;!sNrc+^)Wg@_=ZuN zr1a`rAeUg0ziQ=ZdyIt|!k8l{$U6d zkM% z66AJtKy9uBH;LW3zt;gA2Uxhn<%7aoqH$aX2~YLC8~*Zdpo&)e37Y4Yog++;2~KKU zEgBaOu6R;;`;{@Uj8JqB61Xs&*7rjF@|N+2V$|EIdjFaNg!zlrc`k)r!*Y&-V-P0h z4+#{=$6UilcHu(PBC?eaO+Kl}tY`$<8oCN`EOLuuQAe8r=O=K6 zC(k5{VJvAynA5*-Z}jU+d!kr~uC%YDOEX8`p66{p^;S?Ylz;PA@c}XPq#i|I&}><- zidl6&8(MO@3KF`q>-uRH=v+ z7_t(sEq{aCuM%eVu5nW_{m7g00mjhE?cZy75Mw|taYRkC^-HkI1*Icu&#=#p*^@q9 zBzOIgbQLgm*ov^&N004@J1MRLu1@kHjTn0$*CtI=yQI{Ymx%?p5}P7aT@1TnW&S>H z^!{Y5z#d8dRUG7}Sr3t59sn#;m*)nFj`_M+NMvgeX7axsE)tTyTd_C&ggr4GPEw~Ytb^i;MVN}u+O9v^O)t0Caigy>?*p6t?>)@{N}C?V z%%xwZ@(vZM5E5h~{hdGtp`~+B*ybIK@@VB6(c?kD(bkp9M|9r^Ti0;6Mp9{C7zFjQ zO#>UpHaM(fHU5^3TpmQYbPQhJ6h}vsKo0OHJ_M;0l1|PvEri%Z`Wh3gVrur&D$ngOTSk2VE}0PoWeJelnw4&sgWObGGJ;Mv0pDR_G zvS50s>6q1+u#@`^a6epxU>os0Zy+wjE;G$Ss4 zW((NFH{q(=pX@hXDDxhUHEp5VkJ=rdStA-}jTzcB8x@bnoi9ST4T0Yv%gmk9pn=eNstxM_=g z1R8{?PsR0$`(q7EOue4nBZz2Es-D}{bA(m<{|?P|=wsGOJx3V=Eby>bOqH*#>TgyT ziY9Qt_fi;KNAgf5RV$u|4b$jTw=<6IOn2|dO~18yGnP*kY{+2eXk3HYA8y72#VJ5$ zBR3Ph;t*Ccw8|ZOJ*r`!HJisD`wXuq$N?1RDF|psU7UWiCFG|@!3~qliFidLk&$`; zY637|0Ptn(xpv+5RdqxG;qBXQJy8%#K?P3Bop>5_#?2cbrbb` z&npsIehli9@e2U_@=-IPJV#!16&Obl8Zz)UkIg4;7a&E*2(D{sr%7{fpadCq9@03Z z@TUEmU&c478usl9zi5{(UnmB7G~yifaRBM@*jWIbRQdXY6+F}hE8xK}z2y$FSvkGi zzvf%O*-&+FEEIzm1N~TDb+n+OQPfkcI+bQttSYLb25dwj!-HA0Iq`a&g5<6zCb%RW zEwslMyqgvLelZBpLPQw(O1cv^Y&R7Q36iyrfC7vW)1>-%a@e_g_2d+cbW_Pm>d$vQX(8G`^xq>qzaE9aUJ9_Rq9Yj?+H(#Oi&m?D2Ek zrF*CXhSw;7OHDivdl|h2hK`)jE%h2of^FVx3*PsZW1yD$(wwORXj< zcM~=_T3Jspg$zz#C!}pKzDEF|de?_eDEVUXTZwnxgeMU9f+me|be%w4K7NrcHayY5 zQ$f;LS?)GCzh5gE6)!y3D6wi+ExKrd)J@A=o|>;=Vq5ilH|OeSW>&jLbpDjjJ07}h zcb(bXcjJ@$URkt2Kj}=-jK3xBR8u(BywUc`%$F$liL;+qyQNF>gzrpz&edQEo$fRg z&PdPkjHohOZkm@UNKc4yTqxRePZ?oy|KSd5Z*|`Iwb2S_G#_z)G6Bd(KR*Y`;9(_` zM`lK&Ko?zsLNaq`;E+H1pWpe;zIMv*(FQiHPtImj5OCOMlqv5nW>Q2z_D{c< z`bI?M$)o<%a)mVn#Y(qZJxr}0ZF=g8y$i16rEH*DfQ@fp6VvJT*#--{pDm+s{YQEb z?^V}Tb>CUZhX7_sS-zKA$d|!mE;Y8h8HBzbzGsRQ{7$-t{JKV15`qThq#MhH`!xXc zUf{U?<}4=o9%O(*7rjB^CfmY;?tnmH@@2!R-CzGGg{ z#z$bUMj-dftv_@Dft4#}9vw&YjN|FmByX^#RnT?U*8lSp+^tLIJ&8ScKtWO06-sG=9T?kg0#LV?St0#33-9zi8?E0iEoqinsnk*I zbzixN=(JMr@VkJdRy#}Pv3|+H74U-~%#5E}V{mT_4*z2AO##v#Bq&8pu{?D?$L-W& z?)2AziEKE%`sL(1>P?IGX~cpKp_EhZZI~$MBcOZ=1vz`QJ)Mi)Iboz~%`-C>qI)^_ z`nXJ3zgXp;H=q15i+c>r30T{#_Mfh9Hl09x-s_zjeRKNYB+# zLUxDXNBgx;gGE5dlUhyxnvi~M$ZQt<;0M;JPs5K-_sGO1M4L1(Ngc(wXF71q@J-JR z@6$}Qw#}(2X}xa@4MmI@z%%un;dvzEWpah{(+CH?1ca%4SGB}+Q<%?4)?)L@g zb~Gq(0SnqrYw`&6FfvBM!SMzXY&4|6cBeMA7G8w~PG~=#*tKO>z;$iJCdsi3r05@G z5qP&#?{qP;hqUWgpm={*sk%b=YxZOnb%pE>+|`CxTonLqWdh6cB76mN5`+?E*Za~> z6{J5Z(`dfN?usn;t$RDC`nx^Ya^qL?+3YGK&fxnFzHABg;bR7@YE+Lh8+VMnzFf${ z`L9NBs~7^L-TsrK@{VIx4(rWI4jkoSah~as>ZT}>ZT`S*O%%rwCjL^ zfN8thj;@!f`q}{S2*IB_Vf(I;QJ>|OYq<5O2)#c<)S8biqMdx-l>rf3zLCOxk6q5} zdv@DEFWN-7BG@p~rW`P%&^cjh$X~!h-Brl0CAKo_@TIL@^@mQ|=&v7bO5Rk*A z_C9+Zn$uI3w=I*G(H=oYCwJADq^dTFwX2|IpP_INZz*aV!Oui5wn1bmC%8Yv)Vi7F z%%Mh`&qq}m9{>_0g~ z?xB$`9Wo0yW<^vG*K>_oKBmi5Rf&!#AtMY=am?tjd~3*^78R)VRSU2J+H8T~QAaZ} z>PWh1HW*vHe~kfsBspOoDb?ddbE)ORM-A&*#Oa%oRr)nFN2|P&N6&%+xP%f&&cO0B z!0Z&Ofbp11j!88Sb8y0#kW8*Qa&f_E;4ZEUYiwEX0#Y8u!{s86Z(S3whZY`77d*%Z z^emwyWm8SZ&h3AZw(lzJIn9U4g(4SkzgrCVz2Z9#wtlK6>3m5=v8vz5S}R9rUaf08 z0MB>$cLkP~enj)u_M&9BtZF?&tvHBP2wNX9b2(kd+}&^GetR_{0=Vic&UInS0Do|l z);^PKw{)UwYdBWCL6xSziUS|*DiEU}JU7e$D)0g6G(C^T>e?X1awuP;VKd@POVFNrl}>0)wr<($${+Vo##3o=ksX4$p$L($}y$~3gDNHT_ISv8IA zrZ8smAx^fGZTWX3m*>-vWO@F2TUpWJ*HOOW-%yKNNNXQclIiPOGu>Y66@4yJey`3i zNVD2(0E9jP2gD~T0oo=peCn0~{48xzj^IP;mRUgnV+QD8$1n9Z8>!WD#{7o8V5T+( zoUVyVv#I7d^^>m)_1)m)&w{p_Y=3Wv4>?Bi)y?0lZ5au7hl7C0+$W`|u&vAz|BKkO ziK23NAS(Cd?HqO<1Sh%GJ@vY+iy*0|)tIFAAy$57i@bN+$n@@op|J;YT`YWv7ujh+ z^|s1{;h_NSd%&s%?H4;?u!o7Jm%;oawI8eE%jF~kB5Y>itKoqWJ>8wIo9wNC!)WMT z2&rS0&!X#;uFeOQh9FXgM37>TF@Mrhmz$N+%g{|D{1cKjIe(MAJJSfP2{ zd7mSt@g&5Gc$t;bhI>WcAc~RlF$m_@>GCZDF2!E^T-zn~9Cy#b@>m5b1N1H8b%+50 zN&{RF%*yu}#YB!)5`k5`wJzf`++xNS}r~CG63_a)36TKa$RQ;66KPvkiJ7~ znSUWEcxUP;IAzVs7FoLQZDPEd_x!%O1(oNfc=nk~txuatHY5b0rKqL#WadD9Tb|=U z0kdK$Bn*&^fQfsewzv2LyiO@EugwaS5WP69ifM9m(j>SB+kJ0#Ad|2?v?tX8*9=4k zwNB}PSzK>UODj~p)hMy|gIW9Zx0%-6wj+)b(&$$`lCh-5smYZ+UfNl&)|jWYqSQ|| zH&3Qve%?~X_Fn~TuWBgXBgI%hcMc&J3f@!|QK;?3AZ3moKN4{C85+_Yxd zQp-8#>5n_%#KP|4NLEE^>r0m(5`I^wHLqZlc9|Vn{q@CO$l86&EgVylYHiG#_%0TR zsOZ>yt~9a`#`Q2CN)o$|dL2JMm#Fc67AW?{&-5J8_-GphE5M|-rwL-H4F}1(usHV{ zIDf6yTqh*E|DA=2WaLG3LAR-nxB2ncfOJhCWTFJ;UwKjFt0)&2&qTlE&_>sdWd}v% z?M)Mg?ri0XXx1*z!04ug@Fj8Y)u6rJ0-C9lCc@46@pX-F?iFb#i}3*bn9}M>cYARY zaf0~M^(Am@kqQT)TRd`CfVL0^g6^!qqy2O|v^s0zh4l*=??P6Zp64v8K8xOaEmhA3 zQti`*LZZWl1t+NT7DtR&^PXmXqlJG(`~2uz9zhc$J-SGr(E(#^<|FbUaY5V6CdeDr zwHV-fh9?lqO36ILVnRRpcA~pksZ5SlR{GBMj>?73AcT*aq__%U;5kRln{weh2KM#g zORT;GJU@GkPJ8Rxg2}bM`X8P}slLVwzTQ(Nb=I*H(f;^r38B|()(qRxD_dU8I_d4W z4WsL3Rp@{0#dnb4F#dsLfC&1+Owq4AjMdCdnon3o>?8w47oH1qYVtcE>^xcSNPXpK z6`R=^H)`l?+n6=fii()(fxRYwaJ8FMQ7^M`Q{--X8gPLrH0y{>T{`D7mP zC!HrrtZRo1j$x<<3&AXI|2;f$6tj@UiuD1&rj9@}1(xiFsdo*uUupoM?IS>5zc!KH z{?(Fwtfn&sE4O9kG+cQemZ6tsx~Bv3zgXUbB!M3)>&A4L%ss ziICRB^DkKEH@e~3Sx)eYhgHEuG~Q9njc_{w8u#S>&<-FfQ&Z|MHRR$rquiP`A;5#U zRoar;|22m(8}V7}382t*k}Bpo1CSJ!j1)$!*y?TjJwXhu1oyG~_dXI#sgr(3FHTxY z3UzOoh@{uMJhk@p;6>+P@L1LIE>gt-8O(0I6a8t}tZgry>pm3jpsS)GL`#8dsNjYR zam@3%Tn4=b{cc5o`3jc4<*{}|n{!1IOyKe5E3YeFh+HVH6NP1P>@HUywei2X9R3&_ z+JJ*YoAeuDhki^9FBjBd|n=DWFr>l>jFwwWT;9HS*cFO4} z0N$WSg81eS{IK?5F-hRc?SN?Ua9~kJL_=~9n(LuoVW0Uq#Lm?sqqO3Ctq}tD{qOBu zJ`JO2_1T^107nYmaS$D4>oM1#CVj=w73(+9-oOZB0MBdFIs0>p&3NdQ@}TfW>_svM zNd^|Xeb=2ZGO>^dB>A`Aqn)KiOT(zj3<3>)n8#jMbg-W~fMlX5QMDF>ypQMCJ>*|z zHU8)UJAqkA?~plz2NoS@jb=@d``<~ig@oW6wDUh z!ko|e))idDWdTFjt`#ItG=F#qR-kaOm)iWabO7E+*LEVBp47VJ7|L*=TL%A~7*%Gr z{QOg?S8~ZoSOo7wx%y>J6o>!XW(^?E>(U%(V)qH~7Ev?4@z2Sp0iBkX1ap4-%H_Nn z#CLYf<9szndmZ-WjTSHoMoXU#;%jQ5+XOhZ51L}vl8bAFOiYHmq%}W9lt;C zyJrV2X1{LG(^vY^3C87jfEzDEn6A(`jwFi z5Ca^I8(w9{m!GHSqK9oh`@ku8djSV>YPz9eQ5Fm zRj#qEj3YWX^_-6!-xFm6KpOLInDwK1KeS)mustAXyTOvEbB!f$0tQ)si63Xz;)ZXz zEI(W+y^AHkkQX6j(LHZD$e`=32#2wKS?e3Q03lN1_E9FF)1#1SZGt||rX?w)pQ~V| z5z zCRuPRDl(VA0Sx#Q@HHTi4Tc2=t%%8Nt79n@Jpm-;6t&B{i$!k{)~i>J^no_)Pz1G0 zeXFdKM?4XLk=q|`zPmGGFB;OxSXNWN*PPyLfZyYXIb0g@DeNdBnSqK0T7M2NEI*DE zo=3|=uIL~;6TOj_CGW57EC50jCzPp9+gCu$VEm}j$jU{Z^eLtfcN!T~CPDoGrW>@k z@gwYaf6HD1MgbCB6a;bsf`etg`pHOIje7ZNz)sJRpME#5u0pQE7uQ^Y7h%BjTk2DO z+p?>GW7Sc6}bLYN0_=vcJ5en~NS+9N~cb)VCHs2{zywJR% zsSub+vcgDKn%2_h*OF7Q`#|1-Y39aajP$Om$r-X>j{(4LWLoi`_Z5e{V#`6TW|Te< zu&lgK`!{|2`8VgJ3u>)J~}s!IxZ zwspm*X}Mi#>P_edQx9&?TCJ%J$X?T5K|ND}C_?Yz@fu>Z@HEE<<-I=ZozjydRDz-C*9f>V|C&>7tTl~6s^qgi`rp|O}buBJ($L#3^ z?0q#OSAcm4kjp_n_V*Vb*=*twusqX0<$&NM&8G4^y*s}DoQi}lF)*qUZdsVKq~2TxQET!2P~D=FCT*zasWX&uq{y!RIO86NrnnQnpfGDp>_6x37oq+%6=wK11U<1be zMiyW47pbo#SVH|S1$Xc(ey9oG^LEuB(L{$f<2S$|d-;qM%KZZdSD1N%c$7r}HhXsgUo#G+6x1lnKyazQ13%G_LhwEc z7^Vu;^Vc^L;(rsC*$1|Sz?J?CuLWBTi{~2bvC?%Uf8||qQUvU55}0rjFuwE{ox`Gu zhuF&|kE7wI{()1r<)0vf=&usQkCiXnHa~$uM#l{hTleQTR3|nB3+~`scXuejC+-if z1{Q`wBdpg^6AkC5dHrb=&{5xlabwP$+F(K@(H)x5#p?R*V$gIg^Y1YnvIBeS$W6yB zHKaytWY~Z><(<0kTHXr-;QKEKVFO}zD-R?B2}jF~V^^-gc1~`B_5T^O3Z7gmDfL_y zI_lV++GFszk$g`0hfy7;W5X~Y<;6<6KK($wRRpvOOgXlAo6k8AWVY&nUA30ELcdj1 z1&!8np4Rf(dF|+gE0qO4FmTl!L2+%jKhJ&knDdov+gfk>VDYT?s(uXtzkhOm!a5@# z$bxGg5Bqa`<=ZoLn?jSB?1w<@0?NVDoWsY6;zmib4`g#yOm*_Uo^_7DtY&JEMCF&Y zv+UMP^@P}UcB^X%m-;vPYy9|xW zk^=|aYVjayV7$)Kvpd@r?WCzMr6yJ@8;AfCT~{BRkwxiz9&w;K)%AFuT;E1$$X9G zhdD#2NrMKhgmPd{DjT<9E~1MWX~58;b8P!1Xb}uoPKtYw4|K^+H^&lZU=fv{S}fZ? z4s8+#cI1UrjZ7pTt=lCK>|{u+vYT_RO}ROrJEK1*@1JFzvE0@il`Li-_RCSiVhh&B zgMN9%wdmDOpMjs2C0@t2EmO3ow>%u0_flS2c}u4xro}JcJg2Nfb*-U(R(Wgx`y4Lc zII>R`Fk1;t;B&O4priRr%FII zE?BVGpIqi(iSQdRkU9PP^XR1fzt6#wakBkN!NE(x5{Ga7BZ@;Y9je6P$vM!0Lo4xD z5Qi%9R}zPg<3JLJDsiY1hjG(i9XO1e4kU4?5{D{r7&raZfkP+xR}zPD(}5)Z*Qi8r zm22~Uryy(_om2Wo`u}wetp2lf^5Q>BCsY4fI=S{ArIW+A{#E!xF&#+aP$dpkBKXis z94Nt|F+C8(p-TKU3x_Iks1k=x^6zOljGGQ5ai|i9Dsku}|LVY@lRS{b|C~xtQ|K49 UD$rkg2VF^B`Ns7kMXS*N0+I-&^8f$< literal 0 HcmV?d00001 From 3f9c685bfc6313fa98a7e9c9880984a8fa300680 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:42:20 -0700 Subject: [PATCH 040/119] Add disclaimer about default macos `openssl` binary (#19623) See https://fleetdm.slack.com/archives/C019WG4GH0A/p1718042699503069 --------- Co-authored-by: Jacob Shandling --- articles/windows-mdm-setup.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/articles/windows-mdm-setup.md b/articles/windows-mdm-setup.md index a8baceb6df..1fb36f3bf6 100644 --- a/articles/windows-mdm-setup.md +++ b/articles/windows-mdm-setup.md @@ -18,6 +18,8 @@ How to generate a certificate and key: 2. Create a certificate: `openssl req -x509 -new -nodes -key fleet-mdm-win-wstep.key -sha256 -days 3652 -out fleet-mdm-win-wstep.crt -subj '/CN=Fleet Root CA/C=US/O=Fleet.'`. +> Note: The default `openssl` binary installed on macOS is actually `LibreSSL`, which doesn't support the `--traditional` flag. To successfully generate these files, make sure you're using `OpenSSL` and not `LibreSSL`. You can check what your `openssl` command points to by running `openssl version`. + ### Step 2: Configure Fleet with your certificate and key From fd1500747c6ba42bf2fac98de6b20d2fa8968872 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Mon, 10 Jun 2024 14:49:42 -0700 Subject: [PATCH 041/119] Update macos MDM migration demo policy (#19632) --- it-and-security/teams/workstations-canary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/it-and-security/teams/workstations-canary.yml b/it-and-security/teams/workstations-canary.yml index 55304f6ebf..63b5b07c3d 100644 --- a/it-and-security/teams/workstations-canary.yml +++ b/it-and-security/teams/workstations-canary.yml @@ -121,7 +121,7 @@ policies: platform: darwin calendar_events_enabled: false - name: macOS - MDM migration complete - query: SELECT 1 AS result FROM system_info WHERE local_hostname != 'Titanosauria'; + query: SELECT 1 AS result FROM system_info WHERE computer_name NOT IN ('Titanosauria', 'Drew’s MacBook Pro','fleetwoodmike'); critical: false description: Determines if the device has completed MDM migration to Fleet. resolution: We will migrate your macOS MDM to Fleet. From df16d7656504ebd463c26f78f991cf99c7af2f43 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Mon, 10 Jun 2024 15:15:36 -0700 Subject: [PATCH 042/119] Add fleet_calendar_periodicity to dogfood environment (#19633) From this PR: https://github.com/fleetdm/fleet/pull/19559 --------- Co-authored-by: Robert Fairburn <8029478+rfairburn@users.noreply.github.com> --- infrastructure/dogfood/terraform/aws-tf-module/main.tf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infrastructure/dogfood/terraform/aws-tf-module/main.tf b/infrastructure/dogfood/terraform/aws-tf-module/main.tf index a08aafc158..f8a055fff2 100644 --- a/infrastructure/dogfood/terraform/aws-tf-module/main.tf +++ b/infrastructure/dogfood/terraform/aws-tf-module/main.tf @@ -36,6 +36,10 @@ variable "geolite2_license" {} variable "fleet_sentry_dsn" {} variable "elastic_url" {} variable "elastic_token" {} +variable "fleet_calendar_periodicity" { + default = "30s" + description = "The refresh period for the calendar integration." +} data "aws_caller_identity" "current" {} @@ -55,6 +59,7 @@ locals { ELASTIC_APM_SERVER_URL = var.elastic_url ELASTIC_APM_SECRET_TOKEN = var.elastic_token ELASTIC_APM_SERVICE_NAME = "dogfood" + FLEET_CALENDAR_PERIODICITY = var.fleet_calendar_periodicity } sentry_secrets = { FLEET_SENTRY_DSN = "${aws_secretsmanager_secret.sentry.arn}:FLEET_SENTRY_DSN::" From 917e83e2ffc10ef691d75eb1a449f2745970c87d Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Tue, 11 Jun 2024 12:54:24 +0100 Subject: [PATCH 043/119] =?UTF-8?q?change=20TextCell=20so=20that=20we=20ar?= =?UTF-8?q?e=20rendering=20'0'=20value=20as=20a=20number=20and=20re?= =?UTF-8?q?=E2=80=A6=20(#19441)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit relates to #19433 Makes the rendering of empty text cell values consistent. We also want to render the '0' value as a number instead of the default value `---` with greyed styles. **Before:** ![image](https://github.com/fleetdm/fleet/assets/1153709/7c0ecb99-409d-4698-bb6f-083245fb3919) **After:** ![image](https://github.com/fleetdm/fleet/assets/1153709/d7da74a7-3492-4672-98ea-f810dc0038d7) - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> --- changes/issue-19433-render-0-value-as-number | 1 + .../DataTable/TextCell/TextCell.tests.tsx | 23 ++++++++++++--- .../DataTable/TextCell/TextCell.tsx | 28 ++++++++++++++++--- .../DiskEncryptionTableConfig.tsx | 6 ++-- 4 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 changes/issue-19433-render-0-value-as-number diff --git a/changes/issue-19433-render-0-value-as-number b/changes/issue-19433-render-0-value-as-number new file mode 100644 index 0000000000..ba620c8504 --- /dev/null +++ b/changes/issue-19433-render-0-value-as-number @@ -0,0 +1 @@ +- Makes the rendering of empty text cell values consistent. Also render the '0' value as a number instead of the default value `---`. diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tests.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tests.tsx index c134a44a82..fad278e012 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tests.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tests.tsx @@ -9,13 +9,23 @@ describe("TextCell", () => { expect(screen.getByText("false")).toBeInTheDocument(); }); - it("renders a default value when `value` is empty", () => { - render(); + it("renders a default value when `value` is null, undefined, or an empty string", () => { + const { rerender } = render(); + expect(screen.getByText(DEFAULT_EMPTY_CELL_VALUE)).toBeInTheDocument(); + rerender(); + expect(screen.getByText(DEFAULT_EMPTY_CELL_VALUE)).toBeInTheDocument(); + rerender(); expect(screen.getByText(DEFAULT_EMPTY_CELL_VALUE)).toBeInTheDocument(); }); - it("renders a default value when `value` is empty after formatting", () => { - render( ""} />); + it("renders a default value when `value` is null, undefined, or an empty string after formatting", () => { + const { rerender } = render( + null} /> + ); + expect(screen.getByText(DEFAULT_EMPTY_CELL_VALUE)).toBeInTheDocument(); + rerender( undefined} />); + expect(screen.getByText(DEFAULT_EMPTY_CELL_VALUE)).toBeInTheDocument(); + rerender( ""} />); expect(screen.getByText(DEFAULT_EMPTY_CELL_VALUE)).toBeInTheDocument(); }); @@ -23,4 +33,9 @@ describe("TextCell", () => { render( "bar"} />); expect(screen.getByText("bar")).toBeInTheDocument(); }); + + it("renders the value '0' as a number", () => { + render(); + expect(screen.getByText("0")).toBeInTheDocument(); + }); }); diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx index a699438c85..7c36f8123f 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx @@ -7,6 +7,10 @@ import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; interface ITextCellProps { value?: string | number | boolean | { timeString: string } | null; formatter?: (val: any) => React.ReactNode; // string, number, or null + /** adds a greyed styling to the cell. This will italicise and add a grey + * color to the cell text. + * @default false + */ greyed?: boolean; classes?: string; emptyCellTooltipText?: React.ReactNode; @@ -15,16 +19,30 @@ interface ITextCellProps { const TextCell = ({ value, formatter = (val) => val, // identity function if no formatter is provided - greyed, + greyed = false, classes = "w250", emptyCellTooltipText, -}: ITextCellProps): JSX.Element => { +}: ITextCellProps) => { let val = value; + // we want to render booleans as strings. if (typeof value === "boolean") { val = value.toString(); } - if (!val) { + + const formattedValue = formatter(val); + + // Check if the given value is empty or if the formatted value is empty. + // 'empty' is defined as null, undefined, or an empty string. + const isEmptyValue = + value === null || + value === undefined || + value === "" || + formattedValue === null || + formattedValue === undefined || + formattedValue === ""; + + if (isEmptyValue) { greyed = true; } @@ -50,9 +68,11 @@ const TextCell = ({ return DEFAULT_EMPTY_CELL_VALUE; }; + const cellText = isEmptyValue ? renderEmptyCell() : formattedValue; + return ( - {formatter(val) || renderEmptyCell()} + {cellText} ); }; diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx index 49c447bcb4..a5522d011c 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx @@ -89,7 +89,7 @@ const defaultTableHeaders: IDataColumn[] = [ Cell: ({ cell: { value: aggregateCount } }: ICellProps) => { return (
- <>{val}} /> +
); }, @@ -106,9 +106,7 @@ const defaultTableHeaders: IDataColumn[] = [ disableSortBy: true, accessor: "windowsHosts", Cell: ({ cell: { value: aggregateCount } }: ICellProps) => { - return ( - <>{val}} /> - ); + return ; }, }, { From 30553cecc325479995cf3725683287ba37af4a0f Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Tue, 11 Jun 2024 12:55:25 +0100 Subject: [PATCH 044/119] fix icon misalignments on dashboard cards (#19610) relates to #19555 This fixes various icon misalignments on the dashboard page. **before:** ![image](https://github.com/fleetdm/fleet/assets/1153709/0738c8a3-88c7-481b-8675-fdeb5713de78) **after:** ![image](https://github.com/fleetdm/fleet/assets/1153709/25bc995a-644e-4310-b32d-09d39f28960c) - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Manual QA for all new/changed functionality --- changes/issue-19555-dashboard-icon-fixes | 1 + .../TableContainer/DataTable/InternalLinkCell/_styles.scss | 1 + .../pages/DashboardPage/components/InfoCard/_styles.scss | 6 +----- 3 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 changes/issue-19555-dashboard-icon-fixes diff --git a/changes/issue-19555-dashboard-icon-fixes b/changes/issue-19555-dashboard-icon-fixes new file mode 100644 index 0000000000..a49537a4f0 --- /dev/null +++ b/changes/issue-19555-dashboard-icon-fixes @@ -0,0 +1 @@ +- fix various icon misalignments on the dashboard page diff --git a/frontend/components/TableContainer/DataTable/InternalLinkCell/_styles.scss b/frontend/components/TableContainer/DataTable/InternalLinkCell/_styles.scss index c9dc9c6d90..563c3e2e01 100644 --- a/frontend/components/TableContainer/DataTable/InternalLinkCell/_styles.scss +++ b/frontend/components/TableContainer/DataTable/InternalLinkCell/_styles.scss @@ -5,6 +5,7 @@ color: $core-vibrant-blue; font-weight: $bold; display: inline-flex; + gap: $pad-small; &:hover { cursor: pointer; diff --git a/frontend/pages/DashboardPage/components/InfoCard/_styles.scss b/frontend/pages/DashboardPage/components/InfoCard/_styles.scss index 5b4415547b..6dc9ca5306 100644 --- a/frontend/pages/DashboardPage/components/InfoCard/_styles.scss +++ b/frontend/pages/DashboardPage/components/InfoCard/_styles.scss @@ -57,6 +57,7 @@ color: $core-vibrant-blue; font-weight: $bold; text-decoration: none !important; + gap: $pad-small; &:focus-visible { outline: 1px solid $core-vibrant-blue; @@ -65,9 +66,4 @@ &__action-button-text { text-align: right; } - - .icon { - margin-left: $pad-small; - vertical-align: sub; - } } From de0562a686feee270d4189d6e8bb0b4ed4f57791 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Tue, 11 Jun 2024 12:56:50 +0100 Subject: [PATCH 045/119] UI code cleanup and tests for self service feature (#19487) various code cleanup tasks for the self service UI. Also adds some tests for self service. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- frontend/__mocks__/deviceUserMock.ts | 41 +++++++++ .../DataTable/SoftwareNameCell/_styles.scss | 2 - .../TooltipWrapper/TooltipWrapper.tsx | 10 ++- .../components/TooltipWrapper/_styles.scss | 5 ++ frontend/interfaces/software.ts | 8 +- .../SoftwarePackageCard.tsx | 5 +- .../SoftwarePackageCard/_styles.scss | 3 +- .../SoftwareTable/SoftwareTable.tsx | 3 - .../SelfService/SelfService.tests.tsx | 83 +++++++++++++++++++ .../Software/SelfService/SelfService.tsx | 22 ++--- frontend/test/default-handlers.ts | 5 ++ frontend/test/handlers/device-handler.ts | 12 ++- frontend/test/test-utils.tsx | 20 +++++ frontend/utilities/endpoints.ts | 2 - 14 files changed, 195 insertions(+), 26 deletions(-) create mode 100644 frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx diff --git a/frontend/__mocks__/deviceUserMock.ts b/frontend/__mocks__/deviceUserMock.ts index a176b4ebc0..38017864fd 100644 --- a/frontend/__mocks__/deviceUserMock.ts +++ b/frontend/__mocks__/deviceUserMock.ts @@ -1,4 +1,6 @@ import { IDeviceUser } from "interfaces/host"; +import { IDeviceSoftware } from "interfaces/software"; +import { IGetDeviceSoftwareResponse } from "services/entities/device_user"; const DEFAULT_DEVICE_USER_MOCK: IDeviceUser = { email: "test@test.com", @@ -11,4 +13,43 @@ const createMockDeviceUser = ( return { ...DEFAULT_DEVICE_USER_MOCK, ...overrides }; }; +const DEFAULT_DEVICE_SOFTWARE_MOCK: IDeviceSoftware = { + id: 1, + name: "mock software 1.app", + self_service: false, + source: "apps", + bundle_identifier: "com.app.mock", + status: null, + last_install: null, + installed_versions: null, + package: { + name: "mock software 1", + version: "1.0.0", + }, +}; + +export const createMockDeviceSoftware = ( + overrides?: Partial +) => { + return { ...DEFAULT_DEVICE_SOFTWARE_MOCK, ...overrides }; +}; + +const DEFAULT_DEVICE_SOFTWARE_RESPONSE_MOCK = { + software: [createMockDeviceSoftware()], + count: 0, + meta: { + has_next_results: false, + has_previous_results: false, + }, +}; + +export const createMockDeviceSoftwareResponse = ( + overrides?: Partial +) => { + return { + ...DEFAULT_DEVICE_SOFTWARE_RESPONSE_MOCK, + ...overrides, + }; +}; + export default createMockDeviceUser; diff --git a/frontend/components/TableContainer/DataTable/SoftwareNameCell/_styles.scss b/frontend/components/TableContainer/DataTable/SoftwareNameCell/_styles.scss index 23a3f5a6a0..0b9ede8371 100644 --- a/frontend/components/TableContainer/DataTable/SoftwareNameCell/_styles.scss +++ b/frontend/components/TableContainer/DataTable/SoftwareNameCell/_styles.scss @@ -10,8 +10,6 @@ .software-icon { width: 24px; height: 24px; - border: 1px solid $ui-fleet-black-10; - border-radius: 8px; } &__install-icon { diff --git a/frontend/components/TooltipWrapper/TooltipWrapper.tsx b/frontend/components/TooltipWrapper/TooltipWrapper.tsx index 1fee1e01b1..bb989a0c5d 100644 --- a/frontend/components/TooltipWrapper/TooltipWrapper.tsx +++ b/frontend/components/TooltipWrapper/TooltipWrapper.tsx @@ -23,10 +23,14 @@ interface ITooltipWrapper { tipContent: React.ReactNode; /** If set to `true`, will not show the tooltip. This can be used to dynamically * disable the tooltip from the parent component. - * * @default false */ disableTooltip?: boolean; + /** If set to `true`, will show the arrow on the tooltip. + * This can be used to dynamically hide the arrow from the parent component. + * @default false + */ + showArrow?: boolean; } const baseClass = "component__tooltip-wrapper"; @@ -44,8 +48,10 @@ const TooltipWrapper = ({ tooltipClass, clickable = true, disableTooltip = false, + showArrow = false, }: ITooltipWrapper) => { const wrapperClassNames = classnames(baseClass, className, { + "show-arrow": showArrow, // [`${baseClass}__${wrapperCustomClass}`]: !!wrapperCustomClass, }); @@ -71,7 +77,7 @@ const TooltipWrapper = ({ id={tipId} delayShow={isDelayed ? 500 : undefined} delayHide={isDelayed ? 500 : undefined} - noArrow + noArrow={!showArrow} place={position} opacity={1} disableStyleInjection diff --git a/frontend/components/TooltipWrapper/_styles.scss b/frontend/components/TooltipWrapper/_styles.scss index 95a82a9abd..b9132eaabc 100644 --- a/frontend/components/TooltipWrapper/_styles.scss +++ b/frontend/components/TooltipWrapper/_styles.scss @@ -1,4 +1,9 @@ .component__tooltip-wrapper { + + &.show-arrow { + @include tooltip5-arrow-styles; + } + display: inline-flex; &__element { diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index bb626b1c2a..91e45130bc 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -232,10 +232,12 @@ export interface IHostSoftware { installed_versions: ISoftwareInstallVersion[] | null; } -export interface IDeviceSoftware extends IHostSoftware { - package_available_for_install: never; +export type IDeviceSoftware = Omit< + IHostSoftware, + "package_available_for_install" +> & { package: { name: string; version: string; }; -} +}; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx index 52bb8d0015..7336d16a67 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx @@ -35,8 +35,7 @@ import AdvancedOptionsModal from "../AdvancedOptionsModal"; const baseClass = "software-package-card"; /** TODO: pull this hook and SoftwareName component out. We could use this other places */ - -function useTruncatedElement(ref: any) { +function useTruncatedElement(ref: React.RefObject) { const [isTruncated, setIsTruncated] = useState(false); useLayoutEffect(() => { @@ -64,6 +63,7 @@ const SoftwareName = ({ name }: ISoftwareNameProps) => { position="top" underline={false} disableTooltip={!isTruncated} + showArrow >
{name} @@ -125,6 +125,7 @@ const PackageStatusCount = ({ position="top" tipContent={displayData.tooltip} underline={false} + showArrow >
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/_styles.scss index bc67f06ab7..6897dffe7b 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/_styles.scss @@ -24,7 +24,8 @@ &__title { font-size: $x-small; font-weight: $bold; - @include ellipse-text(290px); + @include ellipse-text; + max-width: 290px; } &__details { diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx index c67000d045..81e4850763 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx @@ -353,9 +353,6 @@ const SoftwareTable = ({ pageSize={perPage} showMarkAllPages={false} isAllPagesSelected={false} - disablePagination={ - !data?.meta.has_next_results && !data?.meta.has_previous_results - } disableNextPage={!data?.meta.has_next_results} searchable={searchable} inputPlaceHolder="Search by name or vulnerabilities (CVEs)" diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx new file mode 100644 index 0000000000..b1de958e24 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { screen } from "@testing-library/react"; + +import { createCustomRenderer, createMockRouter } from "test/test-utils"; +import mockServer from "test/mock-server"; +import { customDeviceSoftwareHandler } from "test/handlers/device-handler"; +import { createMockDeviceSoftware } from "__mocks__/deviceUserMock"; + +import SelfService from "./SelfService"; + +describe("SelfService", () => { + it("should render the self service items correctly", async () => { + mockServer.use( + customDeviceSoftwareHandler({ + software: [ + createMockDeviceSoftware({ name: "test1" }), + createMockDeviceSoftware({ name: "test2" }), + createMockDeviceSoftware({ name: "test3" }), + ], + count: 3, + }) + ); + + const render = createCustomRenderer({ withBackendMock: true }); + + render( + + ); + + // waiting for the device software data to render + await screen.findByText("test1"); + + expect(true).toBe(true); + expect(screen.getByText("test1")).toBeInTheDocument(); + expect(screen.getByText("test2")).toBeInTheDocument(); + expect(screen.getByText("test3")).toBeInTheDocument(); + expect(screen.getByText("3 items")).toBeInTheDocument(); + screen.debug(); + }); + + it("should render the contact link text if contact url is provided", () => { + mockServer.use(customDeviceSoftwareHandler()); + + const render = createCustomRenderer({ withBackendMock: true }); + + const expectedUrl = "http://example.com"; + + render( + + ); + + expect(screen.getByText("reach out to IT")).toBeInTheDocument(); + expect(screen.getByText("reach out to IT").getAttribute("href")).toBe( + expectedUrl + ); + }); +}); diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx index f83f9d3b0d..c7d5f63098 100644 --- a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx +++ b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx @@ -32,6 +32,15 @@ const DEFAULT_SELF_SERVICE_QUERY_PARAMS = { self_service: true, } as const; +interface ISoftwareSelfServiceProps { + contactUrl: string; + deviceToken: string; + isSoftwareEnabled?: boolean; + pathname: string; + queryParams: ReturnType; + router: InjectedRouter; +} + const SoftwareSelfService = ({ contactUrl, deviceToken, @@ -39,15 +48,7 @@ const SoftwareSelfService = ({ pathname, queryParams, router, -}: { - contactUrl: string; // TODO: confirm this has been added to the device API response - deviceToken: string; - isSoftwareEnabled?: boolean; - pathname: string; - queryParams: ReturnType; - router: InjectedRouter; -}) => { - // TOOD: loading state for fetching? +}: ISoftwareSelfServiceProps) => { const { data, isLoading, isError, refetch } = useQuery< IGetDeviceSoftwareResponse, AxiosError, @@ -121,7 +122,8 @@ const SoftwareSelfService = ({
{data.software.map((s) => { - const key = `${s.id}${s.last_install?.install_uuid}`; // concatenating install_uuid so item updates with fresh data on refetch + // concatenating install_uuid so item updates with fresh data on refetch + const key = `${s.id}${s.last_install?.install_uuid}`; return ( { return `/api/latest/fleet${path}`; }; +// These are the default handlers that are used when testing the frontend. They +// are used to mock the responses from the Fleet API when running tests. +// These can be overridden in individual tests using the .use() method on the +// mock server within the desired test. +// More info on .use() here: https://mswjs.io/docs/api/setup-worker/use/ const handlers = [ defaultDeviceHandler, defaultDeviceMappingHandler, diff --git a/frontend/test/handlers/device-handler.ts b/frontend/test/handlers/device-handler.ts index 306c67a01d..8b92a57c78 100644 --- a/frontend/test/handlers/device-handler.ts +++ b/frontend/test/handlers/device-handler.ts @@ -1,11 +1,14 @@ import { rest } from "msw"; -import createMockDeviceUser from "__mocks__/deviceUserMock"; +import createMockDeviceUser, { + createMockDeviceSoftwareResponse, +} from "__mocks__/deviceUserMock"; import createMockHost from "__mocks__/hostMock"; import createMockLicense from "__mocks__/licenseMock"; import createMockMacAdmins from "__mocks__/macAdminsMock"; import { baseUrl } from "test/test-utils"; import { IDeviceUserResponse } from "interfaces/host"; +import { IGetDeviceSoftwareResponse } from "services/entities/device_user"; export const defaultDeviceHandler = rest.get( baseUrl("/device/:token"), @@ -64,3 +67,10 @@ export const defaultMacAdminsHandler = rest.get( ); } ); + +export const customDeviceSoftwareHandler = ( + overrides?: Partial +) => + rest.get(baseUrl("/device/:token/software"), (req, res, context) => { + return res(context.json(createMockDeviceSoftwareResponse(overrides))); + }); diff --git a/frontend/test/test-utils.tsx b/frontend/test/test-utils.tsx index 4111300afc..ce7bde4298 100644 --- a/frontend/test/test-utils.tsx +++ b/frontend/test/test-utils.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { InjectedRouter } from "react-router"; import { render, RenderOptions, RenderResult } from "@testing-library/react"; import type { UserEvent } from "@testing-library/user-event/dist/types/setup/setup"; import userEvent from "@testing-library/user-event"; @@ -151,3 +152,22 @@ export const renderWithSetup = (component: JSX.Element) => { ...render(component), }; }; + +const DEFAULT_MOCK_ROUTER: InjectedRouter = { + push: jest.fn(), + replace: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + go: jest.fn(), + setRouteLeaveHook: jest.fn(), + isActive: jest.fn(), + createHref: jest.fn(), + createPath: jest.fn(), +}; + +export const createMockRouter = (overrides?: Partial) => { + return { + ...DEFAULT_MOCK_ROUTER, + ...overrides, + }; +}; diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index c1dc7057d9..d096fd4a0a 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -1,5 +1,3 @@ -import software from "interfaces/software"; - const API_VERSION = "latest"; export default { From 96c8139c02d13e10cfa0019cd8ce9e5163cb6842 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 11 Jun 2024 08:53:41 -0400 Subject: [PATCH 046/119] Fix a panic when downloading a software installer that exists in the DB but not in the storage (#19527) --- changes/19324-fix-panic-in-download-software | 1 + ee/server/service/software_installers.go | 2 +- server/service/integration_enterprise_test.go | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changes/19324-fix-panic-in-download-software diff --git a/changes/19324-fix-panic-in-download-software b/changes/19324-fix-panic-in-download-software new file mode 100644 index 0000000000..e5cbf57365 --- /dev/null +++ b/changes/19324-fix-panic-in-download-software @@ -0,0 +1 @@ +* Fixed a panic (API returning code 500) when the software installer exists in the database but the installer does not exist in the storage. diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index 59c95de659..989b5ab26e 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -181,7 +181,7 @@ func (svc *Service) getSoftwareInstallerBinary(ctx context.Context, storageID st return nil, ctxerr.Wrap(ctx, err, "checking if installer exists") } if !exists { - return nil, ctxerr.Wrap(ctx, err, "does not exist in software installer store") + return nil, ctxerr.Wrap(ctx, notFoundError{}, "does not exist in software installer store") } // get the installer from the store diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 4009752a0f..dff176ebe7 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -9423,6 +9423,9 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD // check activity s.lastActivityOfTypeMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d, "self_service": true}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0) + + // download the installer, not found anymore + s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/%d/package?alt=media", titleID), nil, http.StatusNotFound, "team_id", fmt.Sprintf("%d", *payload.TeamID)) }) } From dec9bc53e3dbb0554fef7e4953e670a9c46ddd1b Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 11 Jun 2024 08:55:07 -0400 Subject: [PATCH 047/119] Fix code linting issue where a slice was created non-empty and appended-to (#19490) --- changes/19290-fix-make-slice-with-capacity | 1 + server/datastore/mysql/microsoft_mdm.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changes/19290-fix-make-slice-with-capacity diff --git a/changes/19290-fix-make-slice-with-capacity b/changes/19290-fix-make-slice-with-capacity new file mode 100644 index 0000000000..27770b1bfe --- /dev/null +++ b/changes/19290-fix-make-slice-with-capacity @@ -0,0 +1 @@ +* Fixed a code linter issue where a slice was created non-empty and appended-to, instead of empty with the required capacity. diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 15c2b1c142..f8035b1bd2 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -416,7 +416,7 @@ func updateMDMWindowsHostProfileStatusFromResponseDB( WHERE host_uuid = ? AND command_uuid IN (?)` // grab command UUIDs to find matching entries using `getMatchingHostProfilesStmt` - commandUUIDs := make([]string, len(payloads)) + commandUUIDs := make([]string, 0, len(payloads)) // also grab the payloads keyed by the command uuid, so we can easily // grab the corresponding `Detail` and `Status` from the matching // command later on. From 6dd365f266e7954ec08e42e1c4197b12169149fe Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 11 Jun 2024 09:21:57 -0400 Subject: [PATCH 048/119] Fix scripts that block execution of subsequent scripts when timing out on Windows (#19485) --- ...ripts-blocking-other-scripts-after-timeout | 1 + orbit/pkg/scripts/exec_windows.go | 10 ++++++- orbit/pkg/scripts/scripts.go | 4 +++ server/datastore/mysql/activities.go | 27 +++++++++++++++---- server/datastore/mysql/activities_test.go | 12 ++++++++- server/service/integration_core_test.go | 7 ++--- 6 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 orbit/changes/19059-fix-scripts-blocking-other-scripts-after-timeout diff --git a/orbit/changes/19059-fix-scripts-blocking-other-scripts-after-timeout b/orbit/changes/19059-fix-scripts-blocking-other-scripts-after-timeout new file mode 100644 index 0000000000..7e01060b24 --- /dev/null +++ b/orbit/changes/19059-fix-scripts-blocking-other-scripts-after-timeout @@ -0,0 +1 @@ +* Fixed scripts that were blocking execution of other scripts after timing out on Windows. diff --git a/orbit/pkg/scripts/exec_windows.go b/orbit/pkg/scripts/exec_windows.go index f0d58e17ef..fa44467247 100644 --- a/orbit/pkg/scripts/exec_windows.go +++ b/orbit/pkg/scripts/exec_windows.go @@ -6,6 +6,7 @@ import ( "context" "os/exec" "path/filepath" + "time" ) func ExecCmd(ctx context.Context, scriptPath string, env []string) (output []byte, exitCode int, err error) { @@ -16,8 +17,15 @@ func ExecCmd(ctx context.Context, scriptPath string, env []string) (output []byt cmd := exec.CommandContext(ctx, "powershell", "-MTA", "-ExecutionPolicy", "Bypass", "-File", scriptPath) cmd.Env = env cmd.Dir = filepath.Dir(scriptPath) + cmd.WaitDelay = time.Second output, err = cmd.CombinedOutput() - if cmd.ProcessState != nil { + + // we still check if the context was cancelled before setting an exitCode != + // -1, as killing a process on Windows is not straightforward (see the + // WaitDelay documentation) and may have timed out even if exit code is + // reported as 1, so keep it to -1 in that case so that all user messages are + // as expected. + if cmd.ProcessState != nil && ctx.Err() == nil { // The windows exit code is a 32-bit unsigned integer, but the // interpreter treats it like a signed integer. When a process // is killed, it returns 0xFFFFFFFF (interpreted as -1). We diff --git a/orbit/pkg/scripts/scripts.go b/orbit/pkg/scripts/scripts.go index 397e67a916..08207c16ae 100644 --- a/orbit/pkg/scripts/scripts.go +++ b/orbit/pkg/scripts/scripts.go @@ -15,6 +15,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/constant" "github.com/fleetdm/fleet/v4/pkg/scripts" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/rs/zerolog/log" ) // Client defines the methods required for the API requests to the server. The @@ -65,6 +66,7 @@ func (r *Runner) Run(execIDs []string) error { break } + log.Debug().Msgf("running script %v", execID) if err := r.runOne(script); err != nil { errs = append(errs, err) } @@ -120,7 +122,9 @@ func (r *Runner) runOne(script *fleet.HostScriptResult) (finalErr error) { execCmdFn = ExecCmd } start := time.Now() + log.Debug().Msgf("starting script execution of %v", script.ExecutionID) output, exitCode, execErr := execCmdFn(ctx, scriptFile, nil) + log.Debug().Msgf("after script execution of %v", script.ExecutionID) duration := time.Since(start) // report the output or the error diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go index 90764b32a6..f7267c36ba 100644 --- a/server/datastore/mysql/activities.go +++ b/server/datastore/mysql/activities.go @@ -231,14 +231,31 @@ func (ds *Datastore) MarkActivitiesAsStreamed(ctx context.Context, activityIDs [ // software to install, etc.) and provides a unified view of those upcoming // tasks. func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) { + // NOTE: Be sure to update both the count (here) and list statements (below) + // if the query condition is modified. countStmts := []string{ - `SELECT COUNT(*) c FROM host_script_results WHERE host_id = :host_id AND exit_code IS NULL`, - `SELECT COUNT(*) c FROM host_software_installs WHERE host_id = :host_id AND pre_install_query_output IS NULL AND install_script_exit_code IS NULL`, + `SELECT + COUNT(*) c + FROM host_script_results + WHERE host_id = :host_id AND + exit_code IS NULL AND + (sync_request = 0 OR created_at >= DATE_SUB(NOW(), INTERVAL :max_wait_time SECOND))`, + `SELECT + COUNT(*) c + FROM host_software_installs + WHERE host_id = :host_id AND + pre_install_query_output IS NULL AND + install_script_exit_code IS NULL`, } var count uint countStmt := `SELECT SUM(c) FROM ( ` + strings.Join(countStmts, " UNION ALL ") + ` ) AS counts` - countStmt, args, err := sqlx.Named(countStmt, map[string]any{"host_id": hostID}) + + seconds := int(scripts.MaxServerWaitTime.Seconds()) + countStmt, args, err := sqlx.Named(countStmt, map[string]any{ + "host_id": hostID, + "max_wait_time": seconds, + }) if err != nil { return nil, nil, ctxerr.Wrap(ctx, err, "build count query from named args") } @@ -249,7 +266,8 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint return []*fleet.Activity{}, &fleet.PaginationMetadata{}, nil } - // NOTE: Be sure to update both the count and list statements if the list query is modified + // NOTE: Be sure to update both the count (above) and list statements (below) + // if the query condition is modified. listStmts := []string{ // list pending scripts `SELECT @@ -318,7 +336,6 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint `, softwareInstallerHostStatusNamedQuery("hsi", "")), } - seconds := int(scripts.MaxServerWaitTime.Seconds()) listStmt := ` SELECT uuid, diff --git a/server/datastore/mysql/activities_test.go b/server/datastore/mysql/activities_test.go index aad9419485..e91a825227 100644 --- a/server/datastore/mysql/activities_test.go +++ b/server/datastore/mysql/activities_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/fleetdm/fleet/v4/pkg/scripts" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" @@ -414,8 +415,17 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { sw2Meta, err := ds.GetSoftwareInstallerMetadataByID(ctx, sw2) require.NoError(t, err) + // create a sync script request for h1 that has been pending for > MaxWaitTime, will not show up + hsr, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h1.ID, ScriptContents: "sync", UserID: &u.ID, SyncRequest: true}) + require.NoError(t, err) + hSyncExpired := hsr.ExecutionID + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, "UPDATE host_script_results SET created_at = ? WHERE execution_id = ?", time.Now().Add(-(scripts.MaxServerWaitTime + time.Minute)), hSyncExpired) + return err + }) + // create some script requests for h1 - hsr, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h1.ID, ScriptID: &scr1.ID, ScriptContents: scr1.ScriptContents, UserID: &u.ID}) + hsr, err = ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h1.ID, ScriptID: &scr1.ID, ScriptContents: scr1.ScriptContents, UserID: &u.ID}) require.NoError(t, err) h1A := hsr.ExecutionID hsr, err = ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h1.ID, ScriptID: &scr2.ID, ScriptContents: scr2.ScriptContents, UserID: &u.ID}) diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 1f7b1feca3..802705617e 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -11265,8 +11265,9 @@ func (s *integrationTestSuite) TestListHostUpcomingActivities() { endTime = mysql.SetOrderedCreatedAtTimestamps(t, s.ds, endTime, "host_software_installs", "execution_id", h1Foo) mysql.SetOrderedCreatedAtTimestamps(t, s.ds, endTime, "host_script_results", "execution_id", h1C, h1D, h1E) - // modify the timestamp h1A and h1B to simulate an script that has - // been pending for a long time + // modify the timestamp h1A and h1B to simulate an script that has been + // pending for a long time (h1A is a sync request, so it will be ignored for + // upcoming activities) mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { _, err := tx.ExecContext(ctx, "UPDATE host_script_results SET created_at = ? WHERE execution_id IN (?, ?)", time.Now().Add(-24*time.Hour), h1A, h1B) return err @@ -11323,7 +11324,7 @@ func (s *integrationTestSuite) TestListHostUpcomingActivities() { queryArgs := c.queries s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", host1.ID), nil, http.StatusOK, &listResp, queryArgs...) - require.Equal(t, uint(6), listResp.Count) + require.Equal(t, uint(5), listResp.Count) require.Equal(t, len(c.wantExecs), len(listResp.Activities)) require.Equal(t, c.wantMeta, listResp.Meta) From bcfc93ec22f2b46c7d18d2ce0d7221ef61d358a8 Mon Sep 17 00:00:00 2001 From: Marko Lisica <83164494+marko-lisica@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:26:28 +0200 Subject: [PATCH 049/119] Update "Enroll now" copy on AppleAutomaticEnrollmentPage.tsx (#19642) Apple Business Manager changed sign up CTA copy and it's outdated in instructions for automatic enrollment in our UI. --- .../AppleAutomaticEnrollmentPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AppleAutomaticEnrollmentPage/AppleAutomaticEnrollmentPage.tsx b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AppleAutomaticEnrollmentPage/AppleAutomaticEnrollmentPage.tsx index a91acb0771..89760f8cc3 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AppleAutomaticEnrollmentPage/AppleAutomaticEnrollmentPage.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AppleAutomaticEnrollmentPage/AppleAutomaticEnrollmentPage.tsx @@ -189,7 +189,7 @@ const AppleAutomaticEnrollmentPage = ({ />
If your organization doesn’t have an account, select{" "} - Enroll now. + Sign up now. From ecef0d426388b468b68fdb6c6acc1e854fe6dd5a Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:02:50 -0400 Subject: [PATCH 050/119] `fleetd_logs` table (#19489) #18234 --- orbit/changes/18234-fleetd_logs-table | 1 + orbit/cmd/orbit/orbit.go | 15 +- orbit/pkg/table/extension.go | 2 + orbit/pkg/table/fleetd_logs/fleetd_logs.go | 181 +++++++++++++++++++++ schema/osquery_fleet_schema.json | 51 +++++- schema/tables/fleed_logs.yml | 39 +++++ 6 files changed, 283 insertions(+), 6 deletions(-) create mode 100644 orbit/changes/18234-fleetd_logs-table create mode 100644 orbit/pkg/table/fleetd_logs/fleetd_logs.go create mode 100644 schema/tables/fleed_logs.yml diff --git a/orbit/changes/18234-fleetd_logs-table b/orbit/changes/18234-fleetd_logs-table new file mode 100644 index 0000000000..8c9005bd31 --- /dev/null +++ b/orbit/changes/18234-fleetd_logs-table @@ -0,0 +1 @@ +* Add `fleetd_logs` table diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index b2c19fd74f..e3b5216186 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -31,6 +31,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/platform" "github.com/fleetdm/fleet/v4/orbit/pkg/profiles" "github.com/fleetdm/fleet/v4/orbit/pkg/table" + "github.com/fleetdm/fleet/v4/orbit/pkg/table/fleetd_logs" "github.com/fleetdm/fleet/v4/orbit/pkg/table/orbit_info" "github.com/fleetdm/fleet/v4/orbit/pkg/token" "github.com/fleetdm/fleet/v4/orbit/pkg/update" @@ -244,16 +245,24 @@ func main() { } if runtime.GOOS == "windows" { // On Windows, Orbit runs as a "Windows Service", which fails to write to os.Stderr with - // "write /dev/stderr: The handle is invalid" (see #3100). Thus, we log to the logFile only. - log.Logger = log.Output(zerolog.ConsoleWriter{Out: logFile, TimeFormat: time.RFC3339Nano, NoColor: true}) + // "write /dev/stderr: The handle is invalid" (see + // #3100). Thus, we log to the logFile only. + log.Logger = log.Output(zerolog.MultiLevelWriter( + zerolog.ConsoleWriter{Out: logFile, TimeFormat: time.RFC3339Nano, NoColor: true}, + &fleetd_logs.Logger, + )) } else { log.Logger = log.Output(zerolog.MultiLevelWriter( zerolog.ConsoleWriter{Out: logFile, TimeFormat: time.RFC3339Nano, NoColor: true}, zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true}, + &fleetd_logs.Logger, )) } } else { - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true}) + log.Logger = log.Output(zerolog.MultiLevelWriter( + zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true}, + &fleetd_logs.Logger, + )) } zerolog.SetGlobalLevel(zerolog.InfoLevel) diff --git a/orbit/pkg/table/extension.go b/orbit/pkg/table/extension.go index 516bd1a497..22548d14cd 100644 --- a/orbit/pkg/table/extension.go +++ b/orbit/pkg/table/extension.go @@ -11,6 +11,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/table/cryptoinfotable" "github.com/fleetdm/fleet/v4/orbit/pkg/table/dataflattentable" "github.com/fleetdm/fleet/v4/orbit/pkg/table/firefox_preferences" + "github.com/fleetdm/fleet/v4/orbit/pkg/table/fleetd_logs" "github.com/fleetdm/fleet/v4/orbit/pkg/table/sntp_request" "github.com/macadmins/osquery-extension/tables/chromeuserprofiles" "github.com/macadmins/osquery-extension/tables/fileline" @@ -138,6 +139,7 @@ func OrbitDefaultTables() []osquery.OsqueryPlugin { // Orbit extensions. table.NewPlugin("sntp_request", sntp_request.Columns(), sntp_request.GenerateFunc), + fleetd_logs.TablePlugin(), firefox_preferences.TablePlugin(osqueryLogger), cryptoinfotable.TablePlugin(osqueryLogger), diff --git a/orbit/pkg/table/fleetd_logs/fleetd_logs.go b/orbit/pkg/table/fleetd_logs/fleetd_logs.go new file mode 100644 index 0000000000..803f0027db --- /dev/null +++ b/orbit/pkg/table/fleetd_logs/fleetd_logs.go @@ -0,0 +1,181 @@ +package fleetd_logs + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "sync" + "time" + + "github.com/osquery/osquery-go/plugin/table" + "github.com/rs/zerolog" +) + +// No timezone, always return in UTC. Use this format because SQLite3 +// knows how to parse it. +// See https://www.sqlite.org/lang_datefunc.html +const timeFormatString = "2006-01-02 15:04:05.999999999" + +var Logger = logger{} +var MaxEntries uint = 10_000 + +func TablePlugin() *table.Plugin { + columns := []table.ColumnDefinition{ + table.TextColumn("time"), + table.TextColumn("level"), + table.TextColumn("payload"), + table.TextColumn("message"), + table.TextColumn("error"), + } + + return table.NewPlugin("fleetd_logs", columns, generate) +} + +func generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { + output := []map[string]string{} + + for _, entry := range Logger.logs { + row := make(map[string]string, 5) + // It would be nice if we could return NULL instead of an + // empty string when the error is empty + row["time"] = entry.Time + row["level"] = entry.Level.String() + row["payload"] = entry.Payload + row["message"] = entry.Message + row["error"] = entry.Error + output = append(output, row) + } + return output, nil +} + +type Event struct { + Time string + Level zerolog.Level + Payload string + Message string + Error string +} + +type logger struct { + writeMutex sync.Mutex + logs []Event +} + +func (l *logger) Write(event []byte) (int, error) { + msgs, err := processLogEntry(event) + if err != nil { + return 0, fmt.Errorf("fleet_logs.Write: %w", err) + } + + l.writeMutex.Lock() + defer l.writeMutex.Unlock() + + l.logs = append(l.logs, msgs...) + + if MaxEntries > 0 && len(l.logs) > int(MaxEntries) { + l.logs = l.logs[len(l.logs)-int(MaxEntries):] + } + + return len(event), nil +} + +func (l *logger) WriteLevel(level zerolog.Level, event []byte) (int, error) { + msgs, err := processLogEntry(event) + if err != nil { + return 0, fmt.Errorf("fleet_logs.WriteLevel: %w", err) + } + + for idx := range msgs { + msgs[idx].Level = level + } + + l.writeMutex.Lock() + defer l.writeMutex.Unlock() + + l.logs = append(l.logs, msgs...) + + if MaxEntries > 0 && len(l.logs) > int(MaxEntries) { + l.logs = l.logs[len(l.logs)-int(MaxEntries):] + } + + return len(event), nil +} + +func processLogEntry(event []byte) ([]Event, error) { + var evts []map[string]interface{} + dec := json.NewDecoder(bytes.NewReader(event)) + dec.UseNumber() + for { + var evt map[string]interface{} + if err := dec.Decode(&evt); err == io.EOF { + break + } else if err != nil { + return nil, fmt.Errorf("cannot decode: %w", err) + } + evts = append(evts, evt) + } + + var entries []Event + + for _, evt := range evts { + level := zerolog.GlobalLevel() + var err error + evtLevel, ok := evt["level"].(string) + if ok { + level, err = zerolog.ParseLevel(evtLevel) + if err != nil { + return nil, fmt.Errorf("unable to parse log event level: %w", err) + } + delete(evt, "level") + } + + var sqliteTime string + evtTime, ok := evt["time"].(string) + if ok { + goTime, err := time.Parse("2006-01-02T15:04:05-07:00", evtTime) + if err != nil { + return nil, fmt.Errorf("processLogEntry parsing time: %w", err) + } + sqliteTime = goTime.UTC().Format(timeFormatString) + delete(evt, "time") + } else { + sqliteTime = time.Now().UTC().Format(timeFormatString) + } + + evtMessage, ok := evt["message"].(string) + if ok { + delete(evt, "message") + } else { + evtMessage = "" + } + + evtError, ok := evt["error"].(string) + if ok { + delete(evt, "error") + } else { + evtError = "" + } + + payload := []byte{} + if len(evt) > 0 { + payload, err = json.Marshal(evt) + if err != nil { + return nil, fmt.Errorf("unable to marshall log event: %w", err) + } + } + + entry := Event{ + Time: sqliteTime, + Level: level, + Payload: string(payload), + Message: evtMessage, + Error: evtError, + } + + entries = append(entries, entry) + } + + return entries, nil +} diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json index 87ce3656c3..8113d2fbc3 100644 --- a/schema/osquery_fleet_schema.json +++ b/schema/osquery_fleet_schema.json @@ -9730,7 +9730,7 @@ }, { "name": "etc_hosts", - "description": "Line-parsed /etc/hosts.", + "description": "The `hosts` file comprises a local, plain-text configuration for mapping IP addresses to host names. It does not necessarily rely on an external Domain Name System (DNS) for routing. The `etc_hosts` osquery table expresses the data in the `hosts` file.", "url": "https://fleetdm.com/tables/etc_hosts", "platforms": [ "darwin", @@ -9739,8 +9739,8 @@ ], "evented": false, "cacheable": true, - "notes": "", - "examples": "Identify host\"name\"s pointed to IP addresses using the hosts file. This\ntechnique is often abused by malware, but can also indicate services that do\nnot have proper DNS configuration to be reached from workstations.\n\n```\nSELECT * FROM etc_hosts WHERE address!='127.0.0.1' AND address!='::1' AND address!='255.255.255.255';\n```", + "notes": "The `hosts` file is customized by many organizations. As part of a defense-in-depth security posture it's important to track `hosts` modifications. Endpoints with a modified `hosts` configuration connected to enterprise networks can potentially bypass network rules, proxies and firewalls or be routed to malicious sites.\n\nFile paths:\nLinux: /etc/hosts\nmacOS: /private/etc/hosts\nWindows: C:\\Windows\\system32\\drivers\\etc\n\n- [DNS](https://en.wikipedia.org/wiki/Domain_Name_System)\n- The `/etc/hosts` [Guide For Linux](https://thelinuxcode.com/etc-hosts-file-complete-guide-for-linux/)\n- [How to edit the hosts file on Windows](https://www.howtogeek.com/784196/how-to-edit-the-hosts-file-on-windows-10-or-11)", + "examples": "This query detects if the macOS `/private/etc/hosts` file has been modified from its default state:\n\n```\nSELECT * FROM etc_hosts WHERE address != '127.0.0.1' AND address != '::1' AND address != '255.255.255.255';\n```", "columns": [ { "name": "address", @@ -11047,6 +11047,51 @@ "url": "https://fleetdm.com/tables/firmwarepasswd", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/firmwarepasswd.yml" }, + { + "name": "fleetd_logs", + "evented": false, + "platforms": [ + "darwin", + "windows", + "linux" + ], + "description": "Returns the logs from fleetd's current session. Logs are stored in memory, so they are erased when it restarts.", + "examples": "```\nSELECT * FROM fleetd_logs\n```\n\nReturn only log entries with errors attached\n\n```\nSELECT * FROM fleetd_logs WHERE error != \"\"\n```", + "columns": [ + { + "name": "time", + "description": "The time the event was captured, UTC.", + "type": "text", + "required": false + }, + { + "name": "level", + "description": "The log-level of the event. Info, Debug, etc.", + "type": "text", + "required": false + }, + { + "name": "error", + "description": "The error attached to the event", + "type": "text", + "required": false + }, + { + "name": "message", + "description": "The message attached to the event", + "type": "text", + "required": false + }, + { + "name": "payload", + "description": "Any extra data attached to the event, JSON", + "type": "text", + "required": false + } + ], + "url": "https://fleetdm.com/tables/fleetd_logs", + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/fleetd_logs.yml" + }, { "name": "gatekeeper", "description": "macOS Gatekeeper Details.", diff --git a/schema/tables/fleed_logs.yml b/schema/tables/fleed_logs.yml new file mode 100644 index 0000000000..bed48c9b3e --- /dev/null +++ b/schema/tables/fleed_logs.yml @@ -0,0 +1,39 @@ +name: fleetd_logs +evented: false +platforms: + - darwin + - windows + - linux +description: |- + Returns the logs from fleetd's current session. Logs are stored in memory, so they are erased when it restarts. +examples: |- + ``` + SELECT * FROM fleetd_logs + ``` + + Return only log entries with errors attached + + ``` + SELECT * FROM fleetd_logs WHERE error != "" + ``` +columns: + - name: time + description: The time the event was captured, UTC. + type: text + required: false + - name: level + description: The log-level of the event. Info, Debug, etc. + type: text + required: false + - name: error + description: The error attached to the event + type: text + required: false + - name: message + description: The message attached to the event + type: text + required: false + - name: payload + description: Any extra data attached to the event, JSON + type: text + required: false From 48884b0ae36b69dc6ad65a3c3af5daf8bb8ba41a Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:11:40 -0400 Subject: [PATCH 051/119] Fleet UI: Disabled styling fixes (#19614) --- .../PlatformWrapper/PlatformWrapper.tsx | 12 ++++++------ .../AddHostsModal/PlatformWrapper/_styles.scss | 3 +-- .../EnrollSecretRow/EnrollSecretRow.tsx | 2 +- .../forms/UserSettingsForm/UserSettingsForm.jsx | 2 +- .../components/forms/fields/Checkbox/Checkbox.tsx | 8 +++++++- .../components/forms/fields/Checkbox/_styles.scss | 2 +- .../forms/fields/InputField/InputField.jsx | 10 ++++++++-- .../forms/fields/InputField/InputField.stories.tsx | 2 ++ .../components/forms/fields/InputField/_styles.scss | 2 +- .../InputFieldHiddenContent.tsx | 2 +- .../fields/InputFieldHiddenContent/_styles.scss | 2 +- .../APITokenModal/TokenSecretField/SecretField.tsx | 2 +- .../AdvancedOptionsModal/AdvancedOptionsModal.tsx | 2 -- .../IntegrationsPage/cards/Calendars/Calendars.tsx | 2 +- .../EndUserMigrationSection.tsx | 2 +- .../components/UserForm/UserForm.tsx | 10 +++------- .../components/UserForm/_styles.scss | 8 -------- 17 files changed, 36 insertions(+), 37 deletions(-) diff --git a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx index e81d9de244..b519b6427f 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx @@ -360,7 +360,7 @@ const PlatformWrapper = ({
)} {/* TODO: replace with InputFieldHiddenContent component */} Editing your email address requires that SMTP or SES is diff --git a/frontend/components/forms/fields/Checkbox/Checkbox.tsx b/frontend/components/forms/fields/Checkbox/Checkbox.tsx index 9f8b5aabc7..b7455880b8 100644 --- a/frontend/components/forms/fields/Checkbox/Checkbox.tsx +++ b/frontend/components/forms/fields/Checkbox/Checkbox.tsx @@ -11,6 +11,9 @@ const baseClass = "fleet-checkbox"; export interface ICheckboxProps { children?: ReactNode; className?: string; + /** readOnly displays a non-editable field */ + readOnly?: boolean; + /** disabled displays a greyed out non-editable field */ disabled?: boolean; name?: string; onChange?: any; // TODO: meant to be an event; figure out type for this @@ -28,6 +31,7 @@ const Checkbox = (props: ICheckboxProps) => { const { children, className, + readOnly = false, disabled = false, name, onChange = noop, @@ -57,11 +61,13 @@ const Checkbox = (props: ICheckboxProps) => { ); const checkBoxTickClass = classnames(`${baseClass}__tick`, { + [`${baseClass}__tick--read-only`]: readOnly || disabled, [`${baseClass}__tick--disabled`]: disabled, [`${baseClass}__tick--indeterminate`]: indeterminate, }); const checkBoxLabelClass = classnames(checkBoxClass, { + [`${baseClass}__label--read-only`]: readOnly || disabled, [`${baseClass}__label--disabled`]: disabled, }); @@ -78,7 +84,7 @@ const Checkbox = (props: ICheckboxProps) => { { this.input = r; @@ -180,7 +186,7 @@ class InputField extends Component { >
{ return (
{
  • For the OAuth scopes, paste the following value: { page.

    Editing an email address requires that SMTP or SES is configured in @@ -450,7 +450,7 @@ const UserForm = ({ name="sso_enabled" onChange={onCheckboxChange("sso_enabled")} value={formData.sso_enabled} - disabled={!canUseSso} + readOnly={!canUseSso} wrapperClassName={`${baseClass}__invite-admin`} helpText={ canUseSso ? ( @@ -473,11 +473,7 @@ const UserForm = ({ ) } > - - Enable single sign-on - + Enable single sign-on
  • )} diff --git a/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss b/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss index a2a2a41541..386ea703ca 100644 --- a/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss +++ b/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss @@ -3,20 +3,12 @@ .fleet-checkbox { margin-top: 5px; - &__tick--disabled { - @include disabled; - } - &__label { font-size: $x-small; font-weight: $bold; color: $core-fleet-black; } } - - &--disabled { - @include disabled; - } } .sso-disabled { From b45dbdc58efa500ca9acb9d4b655699ca4a324b8 Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:13:23 -0400 Subject: [PATCH 052/119] Update package_bom.yml (#19634) Updates to the package_bom table per #16993 --- schema/tables/package_bom.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/schema/tables/package_bom.yml b/schema/tables/package_bom.yml index 999e583e80..0c3974c98f 100644 --- a/schema/tables/package_bom.yml +++ b/schema/tables/package_bom.yml @@ -1,8 +1,12 @@ name: package_bom +description: The "bill of materials" (`.bom`) file in a macOS installer package (`.pkg`) lists all files installed by the package. The `package_bom` osquery table collects the data from the `.bom` files created in `/private/var/db/receipts` by macOS when a `.pkg` file is executed. examples: |- - List the bill of materials of a package. The receipts directory contains - packages to installed applications. + This query collects the filepath and time of installation for the libVFXCore.dylib (Dynamic Library) file installed as part of Xcode.app: ``` - SELECT * FROM package_bom WHERE path='/private/var/db/receipts/com.yubico.ykman.bom'; + SELECT filepath,modified_time FROM package_bom WHERE path='/private/var/db/receipts/com.apple.pkg.Xcode.bom' AND filepath LIKE '%libVFXCore.dylib'; ``` +notes: |- + Keeping track of files installed by applications is critical for upholding software management best security practices. + + Apple’s [installer package documentation](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution) From d69a4406a5739ea92aa0e6a57d0ed43352d65547 Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:30:43 -0400 Subject: [PATCH 053/119] Update platform_info.yml (#19637) Updates to Update platform_info table per #16993 --- schema/tables/platform_info.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/schema/tables/platform_info.yml b/schema/tables/platform_info.yml index 6075a2344d..dad1cb11d8 100644 --- a/schema/tables/platform_info.yml +++ b/schema/tables/platform_info.yml @@ -1,7 +1,26 @@ name: platform_info +description: The `platform_info` osquery table collects boot platform information from a computer. The `platform_info` table works on Linux, macOS and Windows. examples: |- - See version information about the boot system, such as iBoot on Apple Silicon + Basic query: ``` - SELECT version FROM platform_info; + SELECT extra,firmware_type,vendor FROM platform_info; ``` + + This query results in a listing of the following attributes on a macOS host running a Windows 11 virtual machine in the Parallels.app: + + Mac - + - extra = "Darwin Kernel Version 23.5.0: Wed May 1 20:14:38 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6020" + - firmware_type = "iboot" + - vendor = "Apple Inc." + + Windows - + - extra = "" + - firmware_type = "uefi" + - vendor = "Parallels International GmbH." +notes: |- + Links: + - EFI: https://en.wikipedia.org/wiki/EFI_system_partition + - iboot: https://en.wikipedia.org/wiki/IBoot + - UEFI: https://en.wikipedia.org/wiki/UEFI#Classes + - System booting: https://en.wikipedia.org/wiki/Booting From 6e0ef1f446e7244a3dcc848cee289e04ce5581f4 Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:43:54 -0400 Subject: [PATCH 054/119] Create pipes.yml (#19638) Create pipes table per #16993 --------- Co-authored-by: Eric --- schema/tables/pipes.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 schema/tables/pipes.yml diff --git a/schema/tables/pipes.yml b/schema/tables/pipes.yml new file mode 100644 index 0000000000..66d2c0e999 --- /dev/null +++ b/schema/tables/pipes.yml @@ -0,0 +1,19 @@ +name: pipes +description: |- # (required) string - The description for this table. Note: this field supports Markdown + Named pipes in Windows can be used to provide communication between processes on a computer or between processes on different computers across a network. The `pipes` osquery table lists the named pipes currently running on a Windows computer. +examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown + This query displays all attributes (columns) for the named pipe enabled by opening PowerShell: + + ``` + SELECT * FROM pipes WHERE name LIKE '%powershell'; + ``` +notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown. + Running the following command at a prompt in PowerShell lists the named pipes currently open on a Windows computer: + + ``` + get-childitem \\.\pipe\ + ``` + + Links: + - Microsoft documentation on [named pipes](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes) + - Discover files linked to processes with Windows [Process Explorer](https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer) \ No newline at end of file From 7698bde029beae5621c4528d9e3583bce3281d59 Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:44:07 -0400 Subject: [PATCH 055/119] Update etc_hosts.yml (#19640) added backticks / fixed width font for file paths --- schema/tables/etc_hosts.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/schema/tables/etc_hosts.yml b/schema/tables/etc_hosts.yml index 0808897836..63adce5acb 100644 --- a/schema/tables/etc_hosts.yml +++ b/schema/tables/etc_hosts.yml @@ -10,9 +10,9 @@ notes: |- The `hosts` file is customized by many organizations. As part of a defense-in-depth security posture it's important to track `hosts` modifications. Endpoints with a modified `hosts` configuration connected to enterprise networks can potentially bypass network rules, proxies and firewalls or be routed to malicious sites. File paths to `hosts`: - - Linux: /etc/hosts - - macOS: /private/etc/hosts - - Windows: C:\Windows\system32\drivers\etc + - Linux: `/etc/hosts` + - macOS: `/private/etc/hosts` + - Windows: `C:\Windows\system32\drivers\etc` **More info**: - [DNS](https://en.wikipedia.org/wiki/Domain_Name_System) From c9ebab7cac6b57b876ad743e0e2e2c10ef3edb83 Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:56:22 -0400 Subject: [PATCH 056/119] Update package_install_history.yml (#19635) Update package_install_history per #16993 --- schema/tables/package_install_history.yml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/schema/tables/package_install_history.yml b/schema/tables/package_install_history.yml index 0c34d2b1a9..7073521346 100644 --- a/schema/tables/package_install_history.yml +++ b/schema/tables/package_install_history.yml @@ -1,7 +1,25 @@ name: package_install_history +description: The `package_install_history` table provides a detailed log of all packages installled on macOS. examples: |- - See a list of packages installed in the last week. + Basic query: ``` - SELECT name, version, source, datetime(time,'unixepoch') AS install_time from package_install_history WHERE install_time |-= datetime('now','-7 days'); + SELECT name,package_id,version,source,datetime(time,'unixepoch') AS install_time FROM package_install_history WHERE install_time >= datetime('now','-7 days'); ``` + + This query fetches the following data for a macOS package: + - Name + - Package ID + - Version + - Source + - Install time + + The `WHERE` clause filters the results to show only packages installed in the past 7 days. +notes: |- + + Monitoring the macOS package install history is useful for: + - Regularly checking for newly installed packages and identifying suspicious software + - Verifying that only approved packages are installed + - Creating a Fleet policy to receive alerts for any unauthorized or vulnerable installations + + Apple’s [installer package documentation](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution) From 94c1fac5e63821b6e313330b2f12712f1d047445 Mon Sep 17 00:00:00 2001 From: Dave Herder <27025660+dherder@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:21:28 -0700 Subject: [PATCH 057/119] fix broken trust page link - subprocessors (#19629) --- handbook/business-operations/vendor-questionnaires.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handbook/business-operations/vendor-questionnaires.md b/handbook/business-operations/vendor-questionnaires.md index b171346023..8af1763870 100644 --- a/handbook/business-operations/vendor-questionnaires.md +++ b/handbook/business-operations/vendor-questionnaires.md @@ -67,7 +67,7 @@ Please also see [privacy](https://fleetdm.com/legal/privacy) ## Sub-processors | Question | Answer | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| Does Fleet possess an APEC PRP certification issued by a certification body (or Accountability Agent)? If not, is Fleet able to provide any evidence that the PRP requirements are being met as it relates to the Scoped Services provided to its customers? | Fleet has not undergone APEC PRP certification but has undergone an external security audit that included pen testing. For a complete list of subprocessors, please refer to https://trust.fleetdm.com/subprocessors | +| Does Fleet possess an APEC PRP certification issued by a certification body (or Accountability Agent)? If not, is Fleet able to provide any evidence that the PRP requirements are being met as it relates to the Scoped Services provided to its customers? | Fleet has not undergone APEC PRP certification but has undergone an external security audit that included pen testing. For a complete list of subprocessors, please refer to our [trust page](https://trust.fleetdm.com/subprocessors). | From 44680cbe15669bd5ef2b2d006664175bc3d42290 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 11 Jun 2024 11:37:30 -0500 Subject: [PATCH 058/119] (2024-06-11) Regenerate osquery_fleet_schema.json (#19653) Closes: #19611 Changes: - Regenerated `schema/osquery_fleet_schema.json` --- schema/osquery_fleet_schema.json | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json index 8113d2fbc3..fc665ac993 100644 --- a/schema/osquery_fleet_schema.json +++ b/schema/osquery_fleet_schema.json @@ -9739,7 +9739,7 @@ ], "evented": false, "cacheable": true, - "notes": "The `hosts` file is customized by many organizations. As part of a defense-in-depth security posture it's important to track `hosts` modifications. Endpoints with a modified `hosts` configuration connected to enterprise networks can potentially bypass network rules, proxies and firewalls or be routed to malicious sites.\n\nFile paths:\nLinux: /etc/hosts\nmacOS: /private/etc/hosts\nWindows: C:\\Windows\\system32\\drivers\\etc\n\n- [DNS](https://en.wikipedia.org/wiki/Domain_Name_System)\n- The `/etc/hosts` [Guide For Linux](https://thelinuxcode.com/etc-hosts-file-complete-guide-for-linux/)\n- [How to edit the hosts file on Windows](https://www.howtogeek.com/784196/how-to-edit-the-hosts-file-on-windows-10-or-11)", + "notes": "The `hosts` file is customized by many organizations. As part of a defense-in-depth security posture it's important to track `hosts` modifications. Endpoints with a modified `hosts` configuration connected to enterprise networks can potentially bypass network rules, proxies and firewalls or be routed to malicious sites.\n\nFile paths to `hosts`:\n- Linux: `/etc/hosts`\n- macOS: `/private/etc/hosts`\n- Windows: `C:\\Windows\\system32\\drivers\\etc`\n\n**More info**:\n- [DNS](https://en.wikipedia.org/wiki/Domain_Name_System)\n- The `/etc/hosts` [Guide For Linux](https://thelinuxcode.com/etc-hosts-file-complete-guide-for-linux/)\n- [How to edit the hosts file on Windows](https://www.howtogeek.com/784196/how-to-edit-the-hosts-file-on-windows-10-or-11)", "examples": "This query detects if the macOS `/private/etc/hosts` file has been modified from its default state:\n\n```\nSELECT * FROM etc_hosts WHERE address != '127.0.0.1' AND address != '::1' AND address != '255.255.255.255';\n```", "columns": [ { @@ -18670,15 +18670,15 @@ }, { "name": "package_bom", - "description": "macOS package bill of materials (BOM) file list.", + "description": "The \"bill of materials\" (`.bom`) file in a macOS installer package (`.pkg`) lists all files installed by the package. The `package_bom` osquery table collects the data from the `.bom` files created in `/private/var/db/receipts` by macOS when a `.pkg` file is executed.", "url": "https://fleetdm.com/tables/package_bom", "platforms": [ "darwin" ], "evented": false, "cacheable": false, - "notes": "", - "examples": "List the bill of materials of a package. The receipts directory contains\npackages to installed applications.\n\n```\nSELECT * FROM package_bom WHERE path='/private/var/db/receipts/com.yubico.ykman.bom';\n```", + "notes": "Keeping track of files installed by applications is critical for upholding software management best security practices.\n\nApple’s [installer package documentation](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution)", + "examples": "This query collects the filepath and time of installation for the libVFXCore.dylib (Dynamic Library) file installed as part of Xcode.app:\n\n```\nSELECT filepath,modified_time FROM package_bom WHERE path='/private/var/db/receipts/com.apple.pkg.Xcode.bom' AND filepath LIKE '%libVFXCore.dylib';\n```", "columns": [ { "name": "filepath", @@ -18748,15 +18748,15 @@ }, { "name": "package_install_history", - "description": "macOS package install history.", + "description": "The `package_install_history` table provides a detailed log of all packages installled on macOS.", "url": "https://fleetdm.com/tables/package_install_history", "platforms": [ "darwin" ], "evented": false, "cacheable": false, - "notes": "", - "examples": "See a list of packages installed in the last week.\n\n```\nSELECT name, version, source, datetime(time,'unixepoch') AS install_time from package_install_history WHERE install_time |-= datetime('now','-7 days');\n```", + "notes": "\nMonitoring the macOS package install history is useful for:\n- Regularly checking for newly installed packages and identifying suspicious software\n- Verifying that only approved packages are installed\n- Creating a Fleet policy to receive alerts for any unauthorized or vulnerable installations\n\nApple’s [installer package documentation](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution)", + "examples": "Basic query:\n\n```\nSELECT name,package_id,version,source,datetime(time,'unixepoch') AS install_time FROM package_install_history WHERE install_time >= datetime('now','-7 days');\n```\n\nThis query fetches the following data for a macOS package:\n- Name\n- Package ID\n- Version\n- Source\n- Install time\n\nThe `WHERE` clause filters the results to show only packages installed in the past 7 days.", "columns": [ { "name": "package_id", @@ -19500,15 +19500,15 @@ }, { "name": "pipes", - "description": "Named and Anonymous pipes.", + "description": "Named pipes in Windows can be used to provide communication between processes on a computer or between processes on different computers across a network. The `pipes` osquery table lists the named pipes currently running on a Windows computer.", "url": "https://fleetdm.com/tables/pipes", "platforms": [ "windows" ], "evented": false, "cacheable": false, - "notes": "", - "examples": "```\nselect * from pipes\n```", + "notes": "Running the following command at a prompt in PowerShell lists the named pipes currently open on a Windows computer:\n\n```\nget-childitem \\\\.\\pipe\\\n```\n\nLinks:\n- Microsoft documentation on [named pipes](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes)\n- Discover files linked to processes with Windows [Process Explorer](https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer)", + "examples": "This query displays all attributes (columns) for the named pipe enabled by opening PowerShell:\n\n```\nSELECT * FROM pipes WHERE name LIKE '%powershell';\n```", "columns": [ { "name": "pid", @@ -19556,12 +19556,11 @@ "index": false } ], - "osqueryRepoUrl": "https://github.com/osquery/osquery/blob/master/specs/windows/pipes.table", - "fleetRepoUrl": "https://github.com/fleetdm/fleet/new/main/schema?filename=tables%2Fpipes.yml&value=name%3A%20pipes%0Adescription%3A%20%7C-%20%23%20(required)%20string%20-%20The%20description%20for%20this%20table.%20Note%3A%20this%20field%20supports%20Markdown%0A%09%23%20Add%20description%20here%0Aexamples%3A%20%7C-%20%23%20(optional)%20string%20-%20An%20example%20query%20for%20this%20table.%20Note%3A%20This%20field%20supports%20Markdown%0A%09%23%20Add%20examples%20here%0Anotes%3A%20%7C-%20%23%20(optional)%20string%20-%20Notes%20about%20this%20table.%20Note%3A%20This%20field%20supports%20Markdown.%0A%09%23%20Add%20notes%20here%0Acolumns%3A%20%23%20(required)%0A%09-%20name%3A%20%23%20(required)%20string%20-%20The%20name%20of%20the%20column%0A%09%20%20description%3A%20%23%20(required)%20string%20-%20The%20column's%20description.%20Note%3A%20this%20field%20supports%20Markdown%0A%09%20%20type%3A%20%23%20(required)%20string%20-%20the%20column's%20data%20type%0A%09%20%20required%3A%20%23%20(required)%20boolean%20-%20whether%20or%20not%20this%20column%20is%20required%20to%20query%20this%20table." + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/pipes.yml" }, { "name": "platform_info", - "description": "Information about EFI/UEFI/ROM and platform/boot.", + "description": "The `platform_info` osquery table collects boot platform information from a computer. The `platform_info` table works on Linux, macOS and Windows.", "url": "https://fleetdm.com/tables/platform_info", "platforms": [ "darwin", @@ -19570,8 +19569,8 @@ ], "evented": false, "cacheable": false, - "notes": "", - "examples": "See version information about the boot system, such as iBoot on Apple Silicon\n\n```\nSELECT version FROM platform_info;\n```", + "notes": "Links:\n- EFI: https://en.wikipedia.org/wiki/EFI_system_partition \n- iboot: https://en.wikipedia.org/wiki/IBoot \n- UEFI: https://en.wikipedia.org/wiki/UEFI#Classes \n- System booting: https://en.wikipedia.org/wiki/Booting ", + "examples": "Basic query:\n\n```\nSELECT extra,firmware_type,vendor FROM platform_info;\n```\n\nThis query results in a listing of the following attributes on a macOS host running a Windows 11 virtual machine in the Parallels.app:\n\nMac -\n- extra = \"Darwin Kernel Version 23.5.0: Wed May 1 20:14:38 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6020\"\n- firmware_type = \"iboot\"\n- vendor = \"Apple Inc.\"\n\nWindows -\n- extra = \"\"\n- firmware_type = \"uefi\"\n- vendor = \"Parallels International GmbH.\"", "columns": [ { "name": "vendor", From 33439620bd1f3e8a479d198b20c8b4ef690ce57d Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 11 Jun 2024 15:18:11 -0300 Subject: [PATCH 059/119] Add missing changes file for #19500 (#19655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I added this but forgot to commit it 🤦 --- changes/19500-scripts-cleanup | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/19500-scripts-cleanup diff --git a/changes/19500-scripts-cleanup b/changes/19500-scripts-cleanup new file mode 100644 index 0000000000..f0a365adf7 --- /dev/null +++ b/changes/19500-scripts-cleanup @@ -0,0 +1 @@ +* Fixed a bug that prevented unused script contents to be periodically cleaned up from the database. From 05eb3385619d294b240dae0f23d79e4b8294074c Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 11 Jun 2024 13:20:32 -0500 Subject: [PATCH 060/119] Enable gitops to create teams with no enroll secrets, or clear enroll secrets for an existing team (#19616) Enable gitops to create teams with no enroll secrets, or clear enroll secrets for an existing team #19332 `fleetctl apply` also gains this extra functionality. In `fleetctl apply` secrets will not be change if one of the following: - secrets is missing from yml - They are blank in yml, like: `secrets:` - They are null in yml, like: `secrets: null` They will only be cleared with `fleetctl apply` if the user explicitly sets them to an empty array, like: - `secrets: []` # 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://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- changes/ 19332-clear-secrets-with-gitops | 1 + cmd/fleetctl/gitops_test.go | 41 ++++++++++++------- .../testdata/expectedGetTeamsYaml.yml | 1 + .../macosSetupExpectedTeam1And2Empty.yml | 2 + .../macosSetupExpectedTeam1And2Set.yml | 2 + .../testdata/macosSetupExpectedTeam1Empty.yml | 1 + .../testdata/macosSetupExpectedTeam1Set.yml | 1 + ee/server/service/teams.go | 28 ++++++++----- pkg/spec/gitops.go | 3 +- server/fleet/teams.go | 4 +- server/service/integration_core_test.go | 2 +- server/service/integration_mdm_test.go | 4 +- server/service/teams_test.go | 6 +-- 13 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 changes/ 19332-clear-secrets-with-gitops diff --git a/changes/ 19332-clear-secrets-with-gitops b/changes/ 19332-clear-secrets-with-gitops new file mode 100644 index 0000000000..afeb4f987e --- /dev/null +++ b/changes/ 19332-clear-secrets-with-gitops @@ -0,0 +1 @@ +Enable gitops to create teams with no enroll secrets, or clear enroll secrets for an existing team by setting team_settings.secret to nothing or to an empty array. diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index fb2003c54b..c9e56d636e 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -181,22 +181,29 @@ func TestBasicTeamGitOps(t *testing.T) { CreatedAt: time.Now(), Name: teamName, } + var savedTeam *fleet.Team ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { - if name == teamName { - return team, nil + if name == teamName && savedTeam != nil { + return savedTeam, nil } - return nil, nil + return nil, ¬FoundError{} } ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { if tid == team.ID { - return team, nil + return savedTeam, nil } return nil, nil } + var enrolledTeamSecrets []*fleet.EnrollSecret + ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) { + newTeam.ID = team.ID + savedTeam = newTeam + enrolledTeamSecrets = newTeam.Secrets + return newTeam, nil + } ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) { return true, nil } - var savedTeam *fleet.Team ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) { savedTeam = team return team, nil @@ -205,10 +212,6 @@ func TestBasicTeamGitOps(t *testing.T) { require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus}) return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil } - ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { - declaration.DeclarationUUID = uuid.NewString() - return declaration, nil - } ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { return nil } @@ -216,16 +219,15 @@ func TestBasicTeamGitOps(t *testing.T) { return nil } - var enrolledSecrets []*fleet.EnrollSecret ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error { - enrolledSecrets = secrets + enrolledTeamSecrets = secrets return nil } tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - t.Setenv("TEST_SECRET", secret) + t.Setenv("TEST_SECRET", "") _, err = tmpFile.WriteString( ` @@ -235,7 +237,7 @@ policies: agent_options: name: ${TEST_TEAM_NAME} team_settings: - secrets: [{"secret":"${TEST_SECRET}"}] + secrets: ${TEST_SECRET} `, ) require.NoError(t, err) @@ -255,8 +257,17 @@ team_settings: _ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()}) require.NotNil(t, savedTeam) assert.Equal(t, teamName, savedTeam.Name) - require.Len(t, enrolledSecrets, 1) - assert.Equal(t, secret, enrolledSecrets[0].Secret) + assert.Empty(t, enrolledTeamSecrets) + + // The previous run created the team, so let's rerun with an existing team + _ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()}) + assert.Empty(t, enrolledTeamSecrets) + + // Add a secret + t.Setenv("TEST_SECRET", fmt.Sprintf("[{\"secret\":\"%s\"}]", secret)) + _ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()}) + require.Len(t, enrolledTeamSecrets, 1) + assert.Equal(t, secret, enrolledTeamSecrets[0].Secret) } func TestFullGlobalGitOps(t *testing.T) { diff --git a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml index f1315fcf24..f10577a3af 100644 --- a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml @@ -29,6 +29,7 @@ spec: enable_release_device_manually: false macos_setup_assistant: scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml index 28f815240d..b5a4c03e5c 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml @@ -29,6 +29,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null @@ -63,6 +64,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml index ef911ec34f..a0d15fddd7 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml @@ -29,6 +29,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null @@ -63,6 +64,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml index 19f92edbc0..8a6762468c 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml @@ -29,6 +29,7 @@ spec: windows_settings: custom_settings: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml index 9862a2d66d..2aac4b1481 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml @@ -28,6 +28,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 452bf80f9b..2b8852ff2a 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -773,10 +773,17 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, for _, spec := range specs { var secrets []*fleet.EnrollSecret - for _, secret := range spec.Secrets { - secrets = append(secrets, &fleet.EnrollSecret{ - Secret: secret.Secret, - }) + // When secrets slice is empty, all secrets are removed. + // When secrets slice is nil, existing secrets are kept. + if spec.Secrets != nil { + secrets = make([]*fleet.EnrollSecret, 0, len(*spec.Secrets)) + for _, secret := range *spec.Secrets { + secrets = append( + secrets, &fleet.EnrollSecret{ + Secret: secret.Secret, + }, + ) + } } var create bool @@ -804,7 +811,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, } } } - if len(spec.Secrets) > fleet.MaxEnrollSecretsCount { + if len(secrets) > fleet.MaxEnrollSecretsCount { return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("secrets", "too many secrets"), "validate secrets") } if err := spec.MDM.MacOSUpdates.Validate(); err != nil { @@ -816,8 +823,9 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, if create { - // create a new team enroll secret if none is provided for a new team. - if len(secrets) == 0 { + // create a new team enroll secret if none is provided for a new team, + // unless the user explicitly passed in an empty array + if secrets == nil { secret, err := server.GenerateRandomText(fleet.EnrollSecretDefaultLength) if err != nil { return nil, ctxerr.Wrap(ctx, err, "generate enroll secret string") @@ -1125,7 +1133,7 @@ func (svc *Service) editTeamFromSpec( team.Config.Software = spec.Software } - if len(secrets) > 0 { + if secrets != nil { team.Secrets = secrets } @@ -1179,8 +1187,8 @@ func (svc *Service) editTeamFromSpec( return err } - // only replace enroll secrets if at least one is provided (#6774) - if len(secrets) > 0 { + // If no secrets are provided and user did not explicitly specify an empty list, do not replace secrets. (#6774) + if secrets != nil { if err := svc.ds.ApplyEnrollSecrets(ctx, ptr.Uint(team.ID), secrets); err != nil { return err } diff --git a/pkg/spec/gitops.go b/pkg/spec/gitops.go index 0fbebe70b5..a416c33787 100644 --- a/pkg/spec/gitops.go +++ b/pkg/spec/gitops.go @@ -244,7 +244,8 @@ func parseSecrets(result *GitOps, multiError *multierror.Error) *multierror.Erro return multierror.Append(multiError, errors.New("'team_settings.secrets' is required")) } } - var enrollSecrets []*fleet.EnrollSecret + // When secrets slice is empty, all secrets are removed. + enrollSecrets := make([]*fleet.EnrollSecret, 0) if rawSecrets != nil { secrets, ok := rawSecrets.([]interface{}) if !ok { diff --git a/server/fleet/teams.go b/server/fleet/teams.go index 984d295343..ebdf8a7eb1 100644 --- a/server/fleet/teams.go +++ b/server/fleet/teams.go @@ -419,7 +419,7 @@ type TeamSpec struct { // set to the agent options JSON object. AgentOptions json.RawMessage `json:"agent_options,omitempty"` // marshals as "null" if omitempty is not set HostExpirySettings *HostExpirySettings `json:"host_expiry_settings,omitempty"` - Secrets []EnrollSecret `json:"secrets,omitempty"` + Secrets *[]EnrollSecret `json:"secrets,omitempty"` Features *json.RawMessage `json:"features"` MDM TeamSpecMDM `json:"mdm"` Scripts optjson.Slice[string] `json:"scripts"` @@ -486,7 +486,7 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) { Name: t.Name, AgentOptions: agentOptions, Features: &featuresJSON, - Secrets: secrets, + Secrets: &secrets, MDM: mdmSpec, HostExpirySettings: &t.Config.HostExpirySettings, WebhookSettings: webhookSettings, diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 802705617e..c3d0d8ab77 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -6012,7 +6012,7 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() { // apply team specs var specResp applyTeamSpecsResponse - teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "newteam", Secrets: []fleet.EnrollSecret{{Secret: "ABC"}}}}} + teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "newteam", Secrets: &[]fleet.EnrollSecret{{Secret: "ABC"}}}}} s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusPaymentRequired, &specResp) // modify team agent options diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 4f95a5395d..e61a5e2ad7 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -71,7 +71,7 @@ import ( func TestIntegrationsMDM(t *testing.T) { testingSuite := new(integrationMDMTestSuite) - testingSuite.s = &testingSuite.Suite + testingSuite.withServer.s = &testingSuite.Suite suite.Run(t, testingSuite) } @@ -2316,7 +2316,7 @@ func (s *integrationMDMTestSuite) TestFleetdConfiguration() { // create an enroll secret for the team teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ Name: tm.Name, - Secrets: []fleet.EnrollSecret{{Secret: t.Name() + "team-secret"}}, + Secrets: &[]fleet.EnrollSecret{{Secret: t.Name() + "team-secret"}}, }}} s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) diff --git a/server/service/teams_test.go b/server/service/teams_test.go index c145c34714..9011e8bce7 100644 --- a/server/service/teams_test.go +++ b/server/service/teams_test.go @@ -431,7 +431,7 @@ func TestApplyTeamSpecEnrollSecretForNewTeams(t *testing.T) { return false, nil } _, err := svc.ApplyTeamSpecs( - ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: []fleet.EnrollSecret{enrollSecret}}}, + ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: &[]fleet.EnrollSecret{enrollSecret}}}, fleet.ApplyTeamSpecOptions{ApplySpecOptions: fleet.ApplySpecOptions{DryRun: true}}, ) assert.ErrorContains(t, err, "is already being used") @@ -441,14 +441,14 @@ func TestApplyTeamSpecEnrollSecretForNewTeams(t *testing.T) { return true, nil } _, err = svc.ApplyTeamSpecs( - ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: []fleet.EnrollSecret{enrollSecret}}}, + ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: &[]fleet.EnrollSecret{enrollSecret}}}, fleet.ApplyTeamSpecOptions{ApplySpecOptions: fleet.ApplySpecOptions{DryRun: true}}, ) assert.NoError(t, err) assert.False(t, ds.NewTeamFuncInvoked) _, err = svc.ApplyTeamSpecs( - ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: []fleet.EnrollSecret{enrollSecret}}}, fleet.ApplyTeamSpecOptions{}, + ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: &[]fleet.EnrollSecret{enrollSecret}}}, fleet.ApplyTeamSpecOptions{}, ) require.NoError(t, err) require.True(t, ds.TeamByNameFuncInvoked) From 99f431f8d790cfcd575f4a2ffb22bd3c079bc498 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 11 Jun 2024 14:27:43 -0400 Subject: [PATCH 061/119] Fleet UI: Host details page > policies improvements (#19483) --- changes/conf-6385-host-policy-table-fixes | 1 + .../details/DeviceUserPage/DeviceUserPage.tsx | 1 + .../HostDetailsPage/HostDetailsPage.tsx | 2 + .../details/cards/Policies/HostPolicies.tsx | 51 ++++++++++++++++--- .../HostPoliciesTableConfig.tsx | 32 +++++++++--- .../PoliciesTable/PoliciesTableConfig.tsx | 14 +---- frontend/utilities/helpers.tsx | 16 +++++- 7 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 changes/conf-6385-host-policy-table-fixes diff --git a/changes/conf-6385-host-policy-table-fixes b/changes/conf-6385-host-policy-table-fixes new file mode 100644 index 0000000000..c61124d153 --- /dev/null +++ b/changes/conf-6385-host-policy-table-fixes @@ -0,0 +1 @@ +- Host policy table can be sortable by response and View all host link preserves the team diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index b052371d50..ed2d8ad85c 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -471,6 +471,7 @@ const DeviceUserPage = ({ deviceUser togglePolicyDetailsModal={togglePolicyDetailsModal} hostPlatform={host?.platform || ""} + router={router} /> )} diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 91ddf98ef4..d19575211f 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -903,6 +903,8 @@ const HostDetailsPage = ({ isLoading={isLoadingHost} togglePolicyDetailsModal={togglePolicyDetailsModal} hostPlatform={host.platform} + router={router} + currentTeamId={currentTeam?.id} /> diff --git a/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx b/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx index 53f91b5a9b..e3bb3550eb 100644 --- a/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx +++ b/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx @@ -1,7 +1,11 @@ -import React from "react"; +import React, { useCallback } from "react"; +import { InjectedRouter } from "react-router"; +import { Row } from "react-table"; +import { noop } from "lodash"; import { IHostPolicy } from "interfaces/policy"; -import { SUPPORT_LINK } from "utilities/constants"; +import { PolicyResponse, SUPPORT_LINK } from "utilities/constants"; +import { createHostsByPolicyPath } from "utilities/helpers"; import TableContainer from "components/TableContainer"; import EmptyTable from "components/EmptyTable"; import Card from "components/Card"; @@ -21,6 +25,15 @@ interface IPoliciesProps { deviceUser?: boolean; togglePolicyDetailsModal: (policy: IHostPolicy) => void; hostPlatform: string; + router: InjectedRouter; + currentTeamId?: number; +} + +interface IHostPoliciesRowProps extends Row { + original: { + id: number; + response: "pass" | "fail"; + }; } const Policies = ({ @@ -29,8 +42,13 @@ const Policies = ({ deviceUser, togglePolicyDetailsModal, hostPlatform, + router, + currentTeamId, }: IPoliciesProps): JSX.Element => { - const tableHeaders = generatePolicyTableHeaders(togglePolicyDetailsModal); + const tableHeaders = generatePolicyTableHeaders( + togglePolicyDetailsModal, + currentTeamId + ); if (deviceUser) { // Remove view all hosts link tableHeaders.pop(); @@ -38,6 +56,23 @@ const Policies = ({ const failingResponses: IHostPolicy[] = policies.filter((policy: IHostPolicy) => policy.response === "fail") || []; + const onClickRow = useCallback( + (row: IHostPoliciesRowProps) => { + const { id: policyId, response: policyResponse } = row.original; + + const viewAllHostPath = createHostsByPolicyPath( + policyId, + policyResponse === "pass" + ? PolicyResponse.PASSING + : PolicyResponse.FAILING, + currentTeamId + ); + + router.push(viewAllHostPath); + }, + [router] + ); + const renderHostPolicies = () => { if (hostPlatform === "ios" || hostPlatform === "ipados") { return ( @@ -83,14 +118,16 @@ const Policies = ({ columnConfigs={tableHeaders} data={generatePolicyDataSet(policies)} isLoading={isLoading} - manualSortBy - resultsTitle="policy items" + defaultSortHeader="response" + defaultSortDirection="asc" + resultsTitle="policies" emptyComponent={() => <>} showMarkAllPages={false} isAllPagesSelected={false} - disablePagination disableCount - disableMultiRowSelect + disableMultiRowSelect={!deviceUser} // Removes hover/click state if deviceUser + isClientSidePagination + onClickRow={deviceUser ? noop : onClickRow} /> ); diff --git a/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/HostPoliciesTableConfig.tsx b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/HostPoliciesTableConfig.tsx index 5b2d19c6e7..96455ddad7 100644 --- a/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/HostPoliciesTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/HostPoliciesTableConfig.tsx @@ -1,8 +1,11 @@ import React from "react"; -import StatusIndicatorWithIcon from "components/StatusIndicatorWithIcon"; -import Button from "components/buttons/Button"; + import { IHostPolicy } from "interfaces/policy"; import { PolicyResponse, DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; + +import StatusIndicatorWithIcon from "components/StatusIndicatorWithIcon"; +import Button from "components/buttons/Button"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; import ViewAllHostsLink from "components/ViewAllHostsLink"; import { IndicatorStatus } from "components/StatusIndicatorWithIcon/StatusIndicatorWithIcon"; @@ -42,7 +45,8 @@ interface IDataColumn { // NOTE: cellProps come from react-table // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties const generatePolicyTableHeaders = ( - togglePolicyDetails: (policy: IHostPolicy, teamId?: number) => void + togglePolicyDetails: (policy: IHostPolicy, teamId?: number) => void, + currentTeamId?: number ): IDataColumn[] => { const STATUS_CELL_VALUES: Record = { pass: { @@ -65,12 +69,17 @@ const generatePolicyTableHeaders = ( disableSortBy: true, Cell: (cellProps) => { const { name } = cellProps.row.original; + + const onClickPolicyName = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + togglePolicyDetails(cellProps.row.original); + }; + return (