From 36aeeddf92e8d8c26a31f50b9a618199e906c84c Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Wed, 27 Mar 2024 11:06:23 -0400 Subject: [PATCH 01/83] Fix / stabilize the puppet flow integration test --- server/service/apple_mdm.go | 1 + server/service/integration_mdm_test.go | 109 +++++++++++++------------ 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index de9e26403d..2d72a816b6 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2266,6 +2266,7 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm. // TODO: improve this to not enqueue the job if a host that is // assigned in ABM is manually enrolling for some reason. + fmt.Println(">>>>> TokenUpdate, checking DEP: ", info.DEPAssignedToFleet, info.InstalledFromDEP) if info.DEPAssignedToFleet || info.InstalledFromDEP { svc.logger.Log("info", "queueing post-enroll task for newly enrolled DEP device", "host_uuid", r.ID) if err := worker.QueueAppleMDMJob( diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 39d322cba5..aa346e18f4 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -151,6 +151,12 @@ func (s *integrationMDMTestSuite) SetupSuite() { workr.Register(macosJob, appleMDMJob) s.worker = workr + // clear the jobs queue of any pending jobs generated via DB migrations + mysql.ExecAdhocSQL(s.T(), s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(context.Background(), "DELETE FROM jobs") + return err + }) + var depSchedule *schedule.Schedule var integrationsSchedule *schedule.Schedule var profileSchedule *schedule.Schedule @@ -1354,7 +1360,6 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { // create a host enrolled in fleet mdmHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t) - s.runWorker() // create a host that's not enrolled into MDM nonMDMHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ @@ -1382,6 +1387,8 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { EnrollmentProfile: json.RawMessage(noTeamProf), }, http.StatusOK, &globalAsstResp) + s.runWorker() + // preassign an empty profile, fails s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "empty", HostUUID: nonMDMHost.UUID, Profile: nil}}, http.StatusUnprocessableEntity) @@ -1440,7 +1447,7 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { 0) }) - // and the team has the expected profiles + // and the team has the expected profiles (prof1 and prof2) profs, err := s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID) require.NoError(t, err) require.Len(t, profs, 2) @@ -1455,9 +1462,24 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { require.Equal(t, globalAsstResp.Name, teamAsst.Name) require.JSONEq(t, string(globalAsstResp.Profile), string(teamAsst.Profile)) - // create a team and set profiles to it + // trigger the schedule so profiles are set in their state + s.awaitTriggerProfileSchedule(t) + s.runWorker() + + // the mdm host has the same profiles (i1, i2, plus fleetd config and disk encryption) + s.assertHostConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{ + mdmHost: { + {Identifier: "i1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, + {Identifier: "i2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, + {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, + {Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, + }, + }) + + // create a team and set profiles to it (note that it doesn't have disk encryption enabled) tm2, err := s.ds.NewTeam(context.Background(), &fleet.Team{ - Name: "g1 - g4", + Name: "g1 - g4", + Secrets: []*fleet.EnrollSecret{{Secret: "tm2secret"}}, }) require.NoError(t, err) prof4 := mobileconfigForTest("n4", "i4") @@ -1467,7 +1489,8 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { // create another team with a superset of profiles tm3, err := s.ds.NewTeam(context.Background(), &fleet.Team{ - Name: "team3_" + t.Name(), + Name: "team3_" + t.Name(), + Secrets: []*fleet.EnrollSecret{{Secret: "tm3secret"}}, }) require.NoError(t, err) s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{ @@ -1476,16 +1499,14 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { // and yet another team with the same profiles as tm3 tm4, err := s.ds.NewTeam(context.Background(), &fleet.Team{ - Name: "team4_" + t.Name(), + Name: "team4_" + t.Name(), + Secrets: []*fleet.EnrollSecret{{Secret: "tm4secret"}}, }) require.NoError(t, err) s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{ prof1, prof2, prof4, }}, http.StatusNoContent, "team_id", fmt.Sprint(tm4.ID)) - // trigger the schedule so profiles are set in their state - s.awaitTriggerProfileSchedule(t) - // preassign the MDM host to prof1 and prof4, should match existing team tm2 // // additionally, use external host identifiers with different @@ -1504,49 +1525,38 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { require.Equal(t, tm2.ID, *h.TeamID) // the host's profiles are: - // - the same as the team's and are pending + // - the same as the team's and are pending (prof1 + prof4) // - prof2 + old filevault are pending removal - // - fleetd config being reinstalled (to update the enroll secret) + // - fleetd config being reinstalled (for new enroll secret) s.awaitTriggerProfileSchedule(t) - hostProfs, err := s.ds.GetHostMDMAppleProfiles(ctx, mdmHost.UUID) - require.NoError(t, err) - require.Len(t, hostProfs, 5) - sort.Slice(hostProfs, func(i, j int) bool { - l, r := hostProfs[i], hostProfs[j] - return l.Name < r.Name + // useful for debugging + //mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + // mysql.DumpTable(t, q, "host_mdm_apple_profiles") + // return nil + //}) + s.assertHostConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{ + mdmHost: { + {Identifier: "i1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, + {Identifier: "i2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending}, + {Identifier: "i4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, + {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending}, + {Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending}, + }, }) - require.Equal(t, "Disk encryption", hostProfs[0].Name) - require.NotNil(t, hostProfs[0].Status) - require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[0].Status) - require.Equal(t, fleet.MDMOperationTypeRemove, hostProfs[0].OperationType) - require.Equal(t, "Fleetd configuration", hostProfs[1].Name) - require.NotNil(t, hostProfs[1].Status) - require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[1].Status) - require.Equal(t, fleet.MDMOperationTypeInstall, hostProfs[1].OperationType) - require.Equal(t, "n1", hostProfs[2].Name) - require.NotNil(t, hostProfs[2].Status) - require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[2].Status) - require.Equal(t, fleet.MDMOperationTypeInstall, hostProfs[2].OperationType) - require.Equal(t, "n2", hostProfs[3].Name) - require.NotNil(t, hostProfs[3].Status) - require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[3].Status) - require.Equal(t, fleet.MDMOperationTypeRemove, hostProfs[3].OperationType) - require.Equal(t, "n4", hostProfs[4].Name) - require.NotNil(t, hostProfs[4].Status) - require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[4].Status) - require.Equal(t, fleet.MDMOperationTypeInstall, hostProfs[4].OperationType) // create a new mdm host enrolled in fleet mdmHost2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t) - s.runWorker() + // make it part of team 2 s.Do("POST", "/api/v1/fleet/hosts/transfer", addHostsToTeamRequest{TeamID: &tm2.ID, HostIDs: []uint{mdmHost2.ID}}, http.StatusOK) // simulate having its profiles installed mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { - _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ?`, fleet.OSSettingsVerifying, mdmHost2.UUID) + res, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ?`, fleet.OSSettingsVerifying, mdmHost2.UUID) + n, _ := res.RowsAffected() + require.Equal(t, 3, int(n)) return err }) @@ -1564,23 +1574,14 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { // and its profiles have been left untouched s.awaitTriggerProfileSchedule(t) - hostProfs, err = s.ds.GetHostMDMAppleProfiles(ctx, mdmHost2.UUID) - require.NoError(t, err) - require.Len(t, hostProfs, 3) - sort.Slice(hostProfs, func(i, j int) bool { - l, r := hostProfs[i], hostProfs[j] - return l.Name < r.Name + s.assertHostConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{ + mdmHost2: { + {Identifier: "i1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + {Identifier: "i4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying}, + }, }) - require.Equal(t, "Fleetd configuration", hostProfs[0].Name) - require.NotNil(t, hostProfs[0].Status) - require.Equal(t, fleet.MDMDeliveryVerifying, *hostProfs[0].Status) - require.Equal(t, "n1", hostProfs[1].Name) - require.NotNil(t, hostProfs[1].Status) - require.Equal(t, fleet.MDMDeliveryVerifying, *hostProfs[1].Status) - require.Equal(t, "n4", hostProfs[2].Name) - require.NotNil(t, hostProfs[2].Status) - require.Equal(t, fleet.MDMDeliveryVerifying, *hostProfs[2].Status) } // while s.TestPuppetMatchPreassignProfiles focuses on many edge cases/extra From dc81c2154beeb8ddc49dfd67a33bed6be7723ce5 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Wed, 27 Mar 2024 11:24:06 -0400 Subject: [PATCH 02/83] Add assertion for enable release device manually to puppet flow integration test --- server/service/integration_mdm_test.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index aa346e18f4..f439ce670e 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -1387,6 +1387,12 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { EnrollmentProfile: json.RawMessage(noTeamProf), }, http.StatusOK, &globalAsstResp) + // set the global Enable Release Device manually setting to true, + // will be inherited by teams created via preassign/match. + s.Do("PATCH", "/api/latest/fleet/setup_experience", + json.RawMessage(jsonMustMarshal(t, map[string]any{"enable_release_device_manually": true})), + http.StatusNoContent) + s.runWorker() // preassign an empty profile, fails @@ -1423,6 +1429,8 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { tm1, err := s.ds.Team(ctx, *h.TeamID) require.NoError(t, err) require.Equal(t, "g1", tm1.Name) + require.True(t, tm1.Config.MDM.EnableDiskEncryption) + require.True(t, tm1.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) runWithAdminToken(func() { // it create activities for the new team, the profiles assigned to it, @@ -1454,8 +1462,6 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { // order is guaranteed by profile name require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) - // filevault is enabled by default - require.True(t, tm1.Config.MDM.EnableDiskEncryption) // setup assistant settings are copyied from "no team" teamAsst, err := s.ds.GetMDMAppleSetupAssistant(ctx, &tm1.ID) require.NoError(t, err) @@ -1486,6 +1492,9 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{ prof1, prof4, }}, http.StatusNoContent, "team_id", fmt.Sprint(tm2.ID)) + // tm2 has disk encryption and release device manually disabled + require.False(t, tm2.Config.MDM.EnableDiskEncryption) + require.False(t, tm2.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // create another team with a superset of profiles tm3, err := s.ds.NewTeam(context.Background(), &fleet.Team{ @@ -1523,6 +1532,11 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { require.NoError(t, err) require.NotNil(t, h.TeamID) require.Equal(t, tm2.ID, *h.TeamID) + // tm2 still has disk encryption and release device manually disabled + tm2, err = s.ds.Team(ctx, *h.TeamID) + require.NoError(t, err) + require.False(t, tm2.Config.MDM.EnableDiskEncryption) + require.False(t, tm2.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // the host's profiles are: // - the same as the team's and are pending (prof1 + prof4) From 10b63658740e6f69bf02312a8bae132a99556542 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Wed, 27 Mar 2024 11:44:26 -0400 Subject: [PATCH 03/83] Remove debug println --- server/service/apple_mdm.go | 1 - 1 file changed, 1 deletion(-) diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 2d72a816b6..de9e26403d 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2266,7 +2266,6 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm. // TODO: improve this to not enqueue the job if a host that is // assigned in ABM is manually enrolling for some reason. - fmt.Println(">>>>> TokenUpdate, checking DEP: ", info.DEPAssignedToFleet, info.InstalledFromDEP) if info.DEPAssignedToFleet || info.InstalledFromDEP { svc.logger.Log("info", "queueing post-enroll task for newly enrolled DEP device", "host_uuid", r.ID) if err := worker.QueueAppleMDMJob( From f1b45a386589c6be1a8309a38932608465096427 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Mon, 8 Apr 2024 08:42:42 -0300 Subject: [PATCH 04/83] friday tidy up party (#18106) - add missing tests - remove unused code - remove unnecessary nested branches --- cmd/fleetctl/get_test.go | 4 +- ee/fleetctl/updates.go | 4 +- ee/tools/mdm/certs_test.go | 3 +- server/mdm/apple/util.go | 26 --- .../mdm/internal/commonmdm/commonmdm_test.go | 67 ++++++ server/mdm/microsoft/microsoft_mdm.go | 4 - server/mdm/microsoft/wstep_csr_test.go | 201 ++++++++++++++++++ server/vulnerabilities/utils/rpmvercmp.go | 14 +- 8 files changed, 281 insertions(+), 42 deletions(-) create mode 100644 server/mdm/internal/commonmdm/commonmdm_test.go create mode 100644 server/mdm/microsoft/wstep_csr_test.go diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index b770565a8e..cd54fc3ed9 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -1457,9 +1457,9 @@ func TestGetQuery(t *testing.T) { Platform: "linux", Logging: "differential", }, nil - } else { - return nil, ¬FoundError{} } + + return nil, ¬FoundError{} } expectedYaml := `--- diff --git a/ee/fleetctl/updates.go b/ee/fleetctl/updates.go index 371f09041e..09010e62ee 100644 --- a/ee/fleetctl/updates.go +++ b/ee/fleetctl/updates.go @@ -764,8 +764,8 @@ func (p *passphraseHandler) checkPassphrase(store tuf.LocalStore, role string) e continue } else if len(keys) == 0 { return fmt.Errorf("%s key not found", role) - } else { - return nil } + + return nil } } diff --git a/ee/tools/mdm/certs_test.go b/ee/tools/mdm/certs_test.go index b8f57fef4b..8d42574f65 100644 --- a/ee/tools/mdm/certs_test.go +++ b/ee/tools/mdm/certs_test.go @@ -11,7 +11,8 @@ const ( // These are just example keys generated locally with openssl. They are intentionally published // and should never be used in production. exampleKeyPassphrase = "password" - exampleKeyPEM = `-----BEGIN RSA PRIVATE KEY----- + // #nosec G101 + exampleKeyPEM = `-----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: DES-EDE3-CBC,5F4D77F29A9E2675 diff --git a/server/mdm/apple/util.go b/server/mdm/apple/util.go index bc8b2bc6e4..40b669da93 100644 --- a/server/mdm/apple/util.go +++ b/server/mdm/apple/util.go @@ -7,7 +7,6 @@ import ( "crypto/x509" "encoding/binary" "encoding/pem" - "errors" "fmt" "math" "net/url" @@ -36,18 +35,6 @@ func EncodeCertPEM(cert *x509.Certificate) []byte { return pem.EncodeToMemory(&block) } -func DecodeCertPEM(encoded []byte) (*x509.Certificate, error) { - block, _ := pem.Decode(encoded) - if block == nil { - return nil, errors.New("no PEM-encoded data found") - } - if block.Type != "CERTIFICATE" { - return nil, fmt.Errorf("unexpected block type %s", block.Type) - } - - return x509.ParseCertificate(block.Bytes) -} - func EncodeCertRequestPEM(cert *x509.CertificateRequest) []byte { pemBlock := &pem.Block{ Type: "CERTIFICATE REQUEST", @@ -67,19 +54,6 @@ func EncodePrivateKeyPEM(key *rsa.PrivateKey) []byte { return pem.EncodeToMemory(&block) } -// DecodePrivateKeyPEM decodes PEM-encoded private key data. -func DecodePrivateKeyPEM(encoded []byte) (*rsa.PrivateKey, error) { - block, _ := pem.Decode(encoded) - if block == nil { - return nil, errors.New("no PEM-encoded data found") - } - if block.Type != "RSA PRIVATE KEY" { - return nil, fmt.Errorf("unexpected block type %s", block.Type) - } - - return x509.ParsePKCS1PrivateKey(block.Bytes) -} - // GenerateRandomPin generates a `lenght`-digit PIN number that takes into // account the current time as described in rfc4226 (for one time passwords) // diff --git a/server/mdm/internal/commonmdm/commonmdm_test.go b/server/mdm/internal/commonmdm/commonmdm_test.go new file mode 100644 index 0000000000..8423983163 --- /dev/null +++ b/server/mdm/internal/commonmdm/commonmdm_test.go @@ -0,0 +1,67 @@ +package commonmdm + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestResolveURL(t *testing.T) { + type testCase struct { + serverURL string + relPath string + cleanQuery bool + expected string + expectErr bool + } + + testCases := []testCase{ + { + serverURL: "http://example.com", + relPath: "path/to/resource", + cleanQuery: false, + expected: "http://example.com/path/to/resource", + expectErr: false, + }, + { + serverURL: "http://example.com?query=string", + relPath: "path", + cleanQuery: true, + expected: "http://example.com/path", + expectErr: false, + }, + { + serverURL: "http://example.com/base/", + relPath: "/path", + cleanQuery: false, + expected: "http://example.com/base/path", + expectErr: false, + }, + { + serverURL: "http://example.com", + relPath: "path/to/resource", + cleanQuery: true, + expected: "http://example.com/path/to/resource", + expectErr: false, + }, + { + serverURL: ":invalidurl", + relPath: "path", + cleanQuery: false, + expected: "", + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.serverURL+"_"+tc.relPath, func(t *testing.T) { + result, err := ResolveURL(tc.serverURL, tc.relPath, tc.cleanQuery) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expected, result) + } + }) + } +} diff --git a/server/mdm/microsoft/microsoft_mdm.go b/server/mdm/microsoft/microsoft_mdm.go index 227f311ecc..a8a9254bd8 100644 --- a/server/mdm/microsoft/microsoft_mdm.go +++ b/server/mdm/microsoft/microsoft_mdm.go @@ -81,10 +81,6 @@ func ResolveWindowsMDMEnroll(serverURL string) (string, error) { return commonmdm.ResolveURL(serverURL, MDE2EnrollPath, false) } -func ResolveWindowsMDMAuth(serverURL string) (string, error) { - return commonmdm.ResolveURL(serverURL, MDE2AuthPath, false) -} - func ResolveWindowsMDMManagement(serverURL string) (string, error) { return commonmdm.ResolveURL(serverURL, MDE2ManagementPath, false) } diff --git a/server/mdm/microsoft/wstep_csr_test.go b/server/mdm/microsoft/wstep_csr_test.go new file mode 100644 index 0000000000..f5d5e59800 --- /dev/null +++ b/server/mdm/microsoft/wstep_csr_test.go @@ -0,0 +1,201 @@ +package microsoft_mdm + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "encoding/pem" + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetPublicKeyAlgorithmFromOID(t *testing.T) { + testCases := []struct { + oid asn1.ObjectIdentifier + expected x509.PublicKeyAlgorithm + }{ + {oidPublicKeyRSA, x509.RSA}, + {oidPublicKeyDSA, x509.DSA}, + {oidPublicKeyECDSA, x509.ECDSA}, + {oidPublicKeyEd25519, x509.Ed25519}, + {asn1.ObjectIdentifier{0, 0}, x509.UnknownPublicKeyAlgorithm}, + } + + for _, tc := range testCases { + t.Run(tc.oid.String(), func(t *testing.T) { + result := getPublicKeyAlgorithmFromOID(tc.oid) + require.Equal(t, tc.expected, result) + }) + } +} + +// The following tests were taken from the Go standard library (since the wstep +// code was taken from there as well) +// Copyright 2009 The Go Authors. All rights reserved. + +var pemPrivateKey = testingKey(` +-----BEGIN RSA TESTING KEY----- +MIICXAIBAAKBgQCxoeCUW5KJxNPxMp+KmCxKLc1Zv9Ny+4CFqcUXVUYH69L3mQ7v +IWrJ9GBfcaA7BPQqUlWxWM+OCEQZH1EZNIuqRMNQVuIGCbz5UQ8w6tS0gcgdeGX7 +J7jgCQ4RK3F/PuCM38QBLaHx988qG8NMc6VKErBjctCXFHQt14lerd5KpQIDAQAB +AoGAYrf6Hbk+mT5AI33k2Jt1kcweodBP7UkExkPxeuQzRVe0KVJw0EkcFhywKpr1 +V5eLMrILWcJnpyHE5slWwtFHBG6a5fLaNtsBBtcAIfqTQ0Vfj5c6SzVaJv0Z5rOd +7gQF6isy3t3w9IF3We9wXQKzT6q5ypPGdm6fciKQ8RnzREkCQQDZwppKATqQ41/R +vhSj90fFifrGE6aVKC1hgSpxGQa4oIdsYYHwMzyhBmWW9Xv/R+fPyr8ZwPxp2c12 +33QwOLPLAkEA0NNUb+z4ebVVHyvSwF5jhfJxigim+s49KuzJ1+A2RaSApGyBZiwS +rWvWkB471POAKUYt5ykIWVZ83zcceQiNTwJBAMJUFQZX5GDqWFc/zwGoKkeR49Yi +MTXIvf7Wmv6E++eFcnT461FlGAUHRV+bQQXGsItR/opIG7mGogIkVXa3E1MCQARX +AAA7eoZ9AEHflUeuLn9QJI/r0hyQQLEtrpwv6rDT1GCWaLII5HJ6NUFVf4TTcqxo +6vdM4QGKTJoO+SaCyP0CQFdpcxSAuzpFcKv0IlJ8XzS/cy+mweCMwyJ1PFEc4FX6 +wg/HcAJWY60xZTJDFN+Qfx8ZQvBEin6c2/h+zZi5IVY= +-----END RSA TESTING KEY----- +`) + +var testPrivateKey *rsa.PrivateKey + +func init() { + block, _ := pem.Decode([]byte(pemPrivateKey)) + + var err error + if testPrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { + panic("Failed to parse private key: " + err.Error()) + } +} + +func TestCreateCertificateRequest(t *testing.T) { + random := rand.Reader + + ecdsa256Priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("Failed to generate ECDSA key: %s", err) + } + + ecdsa384Priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + t.Fatalf("Failed to generate ECDSA key: %s", err) + } + + ecdsa521Priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + t.Fatalf("Failed to generate ECDSA key: %s", err) + } + + _, ed25519Priv, err := ed25519.GenerateKey(random) + if err != nil { + t.Fatalf("Failed to generate Ed25519 key: %s", err) + } + + tests := []struct { + name string + priv interface{} + sigAlgo x509.SignatureAlgorithm + }{ + {"RSA", testPrivateKey, x509.SHA1WithRSA}, + {"ECDSA-256", ecdsa256Priv, x509.ECDSAWithSHA1}, + {"ECDSA-384", ecdsa384Priv, x509.ECDSAWithSHA1}, + {"ECDSA-521", ecdsa521Priv, x509.ECDSAWithSHA1}, + {"Ed25519", ed25519Priv, x509.PureEd25519}, + } + + for _, test := range tests { + template := x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: "test.example.com", + Organization: []string{"Σ Acme Co"}, + }, + SignatureAlgorithm: test.sigAlgo, + DNSNames: []string{"test.example.com"}, + EmailAddresses: []string{"gopher@golang.org"}, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1).To4(), net.ParseIP("2001:4860:0:2001::68")}, + } + + derBytes, err := x509.CreateCertificateRequest(random, &template, test.priv) + if err != nil { + t.Errorf("%s: failed to create certificate request: %s", test.name, err) + continue + } + + out, err := ParseCertificateRequestFromWindowsDevice(derBytes) + if err != nil { + t.Errorf("%s: failed to create certificate request: %s", test.name, err) + continue + } + + err = out.CheckSignature() + if err != nil { + t.Errorf("%s: failed to check certificate request signature: %s", test.name, err) + continue + } + + if out.Subject.CommonName != template.Subject.CommonName { + t.Errorf("%s: output subject common name and template subject common name don't match", test.name) + } else if len(out.Subject.Organization) != len(template.Subject.Organization) { + t.Errorf("%s: output subject organisation and template subject organisation don't match", test.name) + } else if len(out.DNSNames) != len(template.DNSNames) { + t.Errorf("%s: output DNS names and template DNS names don't match", test.name) + } else if len(out.EmailAddresses) != len(template.EmailAddresses) { + t.Errorf("%s: output email addresses and template email addresses don't match", test.name) + } else if len(out.IPAddresses) != len(template.IPAddresses) { + t.Errorf("%s: output IP addresses and template IP addresses names don't match", test.name) + } + } +} + +func fromBase64(in string) []byte { + out := make([]byte, base64.StdEncoding.DecodedLen(len(in))) + n, err := base64.StdEncoding.Decode(out, []byte(in)) + if err != nil { + panic("failed to base64 decode") + } + return out[:n] +} + +func TestParseCertificateRequestFromWindowsDevice(t *testing.T) { + for _, csrBase64 := range csrBase64Array { + csrBytes := fromBase64(csrBase64) + csr, err := ParseCertificateRequestFromWindowsDevice(csrBytes) + if err != nil { + t.Fatalf("failed to parse CSR: %s", err) + } + + if len(csr.EmailAddresses) != 1 || csr.EmailAddresses[0] != "gopher@golang.org" { + t.Errorf("incorrect email addresses found: %v", csr.EmailAddresses) + } + + if len(csr.DNSNames) != 1 || csr.DNSNames[0] != "test.example.com" { + t.Errorf("incorrect DNS names found: %v", csr.DNSNames) + } + + if len(csr.Subject.Country) != 1 || csr.Subject.Country[0] != "AU" { + t.Errorf("incorrect Subject name: %v", csr.Subject) + } + } +} + +// These CSR was generated with OpenSSL: +// +// openssl req -out CSR.csr -new -sha256 -nodes -keyout privateKey.key -config openssl.cnf +// +// With openssl.cnf containing the following sections: +// +// [ v3_req ] +// basicConstraints = CA:FALSE +// keyUsage = nonRepudiation, digitalSignature, keyEncipherment +// subjectAltName = email:gopher@golang.org,DNS:test.example.com +// [ req_attributes ] +// challengePassword = ignored challenge +// unstructuredName = ignored unstructured name +var csrBase64Array = [...]string{ + // Just [ v3_req ] + "MIIDHDCCAgQCAQAwfjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLQ29tbW9uIE5hbWUxITAfBgkqhkiG9w0BCQEWEnRlc3RAZW1haWwuYWRkcmVzczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1GY4YFx2ujlZEOJxQVYmsjUnLsd5nFVnNpLE4cV+77sgv9NPNlB8uhn3MXt5leD34rm/2BisCHOifPucYlSrszo2beuKhvwn4+2FxDmWtBEMu/QA16L5IvoOfYZm/gJTsPwKDqvaR0tTU67a9OtxwNTBMI56YKtmwd/o8d3hYv9cg+9ZGAZ/gKONcg/OWYx/XRh6bd0g8DMbCikpWgXKDsvvK1Nk+VtkDO1JxuBaj4Lz/p/MifTfnHoqHxWOWl4EaTs4Ychxsv34/rSj1KD1tJqorIv5Xv2aqv4sjxfbrYzX4kvS5SC1goIovLnhj5UjmQ3Qy8u65eow/LLWw+YFcCAwEAAaBZMFcGCSqGSIb3DQEJDjFKMEgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwLgYDVR0RBCcwJYERZ29waGVyQGdvbGFuZy5vcmeCEHRlc3QuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAB6VPMRrchvNW61Tokyq3ZvO6/NoGIbuwUn54q6l5VZW0Ep5Nq8juhegSSnaJ0jrovmUgKDN9vEo2KxuAtwG6udS6Ami3zP+hRd4k9Q8djJPb78nrjzWiindLK5Fps9U5mMoi1ER8ViveyAOTfnZt/jsKUaRsscY2FzE9t9/o5moE6LTcHUS4Ap1eheR+J72WOnQYn3cifYaemsA9MJuLko+kQ6xseqttbh9zjqd9fiCSh/LNkzos9c+mg2yMADitaZinAh+HZi50ooEbjaT3erNq9O6RqwJlgD00g6MQdoz9bTAryCUhCQfkIaepmQ7BxS0pqWNW3MMwfDwx/Snz6g=", + // Both [ v3_req ] and [ req_attributes ] + "MIIDaTCCAlECAQAwfjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLQ29tbW9uIE5hbWUxITAfBgkqhkiG9w0BCQEWEnRlc3RAZW1haWwuYWRkcmVzczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1GY4YFx2ujlZEOJxQVYmsjUnLsd5nFVnNpLE4cV+77sgv9NPNlB8uhn3MXt5leD34rm/2BisCHOifPucYlSrszo2beuKhvwn4+2FxDmWtBEMu/QA16L5IvoOfYZm/gJTsPwKDqvaR0tTU67a9OtxwNTBMI56YKtmwd/o8d3hYv9cg+9ZGAZ/gKONcg/OWYx/XRh6bd0g8DMbCikpWgXKDsvvK1Nk+VtkDO1JxuBaj4Lz/p/MifTfnHoqHxWOWl4EaTs4Ychxsv34/rSj1KD1tJqorIv5Xv2aqv4sjxfbrYzX4kvS5SC1goIovLnhj5UjmQ3Qy8u65eow/LLWw+YFcCAwEAAaCBpTAgBgkqhkiG9w0BCQcxEwwRaWdub3JlZCBjaGFsbGVuZ2UwKAYJKoZIhvcNAQkCMRsMGWlnbm9yZWQgdW5zdHJ1Y3R1cmVkIG5hbWUwVwYJKoZIhvcNAQkOMUowSDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAuBgNVHREEJzAlgRFnb3BoZXJAZ29sYW5nLm9yZ4IQdGVzdC5leGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAgxe2N5O48EMsYE7o0rZBB0wi3Ov5/yYfnmmVI22Y3sP6VXbLDW0+UWIeSccOhzUCcZ/G4qcrfhhx6gTZTeA01nP7TdTJURvWAH5iFqj9sQ0qnLq6nEcVHij3sG6M5+BxAIVClQBk6lTCzgphc835Fjj6qSLuJ20XHdL5UfUbiJxx299CHgyBRL+hBUIPfz8p+ZgamyAuDLfnj54zzcRVyLlrmMLNPZNll1Q70RxoU6uWvLH8wB8vQe3Q/guSGubLyLRTUQVPh+dw1L4t8MKFWfX/48jwRM4gIRHFHPeAAE9D9YAoqdIvj/iFm/eQ++7DP8MDwOZWsXeB6jjwHuLmkQ==", +} diff --git a/server/vulnerabilities/utils/rpmvercmp.go b/server/vulnerabilities/utils/rpmvercmp.go index 7c5016f1ca..beabd1035d 100644 --- a/server/vulnerabilities/utils/rpmvercmp.go +++ b/server/vulnerabilities/utils/rpmvercmp.go @@ -69,9 +69,9 @@ func (s segment) compare(b segment) int { return 0 } else if *s.number < *b.number { return -1 - } else { - return 1 } + + return 1 // 'a' is a number seg, 'b' is a letter seg, // numbers are always greater than letters } else if s.number != nil && b.number == nil { @@ -81,12 +81,12 @@ func (s segment) compare(b segment) int { return -1 // Both segs are letters, then we just // compare them - } else { - if s.letters == b.letters { - return 0 - } - return strings.Compare(s.letters, b.letters) } + + if s.letters == b.letters { + return 0 + } + return strings.Compare(s.letters, b.letters) } // Returns the next maximal alphabetic or numeric segment, From bac13b4af246d53448412adfb601ac892b966f13 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Mon, 8 Apr 2024 11:10:29 -0300 Subject: [PATCH 05/83] Prevent MDM migration without assigned ADE profile (#17999) for #15929 this prevents us sending the notification to start the MDM migration if the device doesn't have the right JSON profile assigned. --- changes/15929-migration-rate-limit | 1 + server/datastore/mysql/apple_mdm.go | 4 +- server/datastore/mysql/apple_mdm_test.go | 41 +++++++ server/datastore/mysql/hosts.go | 47 ++++---- server/datastore/mysql/hosts_test.go | 23 ++++ server/fleet/hosts.go | 29 +++-- server/fleet/hosts_test.go | 134 +++++++++++++++++++++++ server/service/devices.go | 3 +- server/service/devices_test.go | 126 +++++++++++++++++---- server/service/integration_core_test.go | 7 +- server/service/integration_mdm_test.go | 105 +++++++++++++++++- server/worker/apple_mdm_test.go | 8 +- 12 files changed, 459 insertions(+), 69 deletions(-) create mode 100644 changes/15929-migration-rate-limit diff --git a/changes/15929-migration-rate-limit b/changes/15929-migration-rate-limit new file mode 100644 index 0000000000..e2370a3268 --- /dev/null +++ b/changes/15929-migration-rate-limit @@ -0,0 +1 @@ +* Do not allow an MDM migration to start if the device doesn't have the right ADE JSON profile already assigned. diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 8c9c3cc153..f8b74da34f 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -985,7 +985,9 @@ func upsertHostDEPAssignmentsDB(ctx context.Context, tx sqlx.ExtContext, hosts [ stmt := ` INSERT INTO host_dep_assignments (host_id) VALUES %s - ON DUPLICATE KEY UPDATE added_at = CURRENT_TIMESTAMP, deleted_at = NULL` + ON DUPLICATE KEY UPDATE + added_at = CURRENT_TIMESTAMP, + deleted_at = NULL` args := []interface{}{} values := []string{} diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index c4489bcddc..031b7017ca 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -71,6 +71,7 @@ func TestMDMApple(t *testing.T) { {"ScreenDEPAssignProfileSerialsForCooldown", testScreenDEPAssignProfileSerialsForCooldown}, {"MDMAppleDDMDeclarationsToken", testMDMAppleDDMDeclarationsToken}, {"MDMAppleSetPendingDeclarationsAs", testMDMAppleSetPendingDeclarationsAs}, + {"DEPAssignmentUpdates", testMDMAppleDEPAssignmentUpdates}, } for _, c := range cases { @@ -5311,6 +5312,46 @@ func TestRestorePendingDEPHost(t *testing.T) { }) } +func testMDMAppleDEPAssignmentUpdates(t *testing.T, ds *Datastore) { + ctx := context.Background() + n := t.Name() + h, err := ds.NewHost(ctx, &fleet.Host{ + Hostname: fmt.Sprintf("test-host%s-name", n), + OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%s", n)), + NodeKey: ptr.String(fmt.Sprintf("nodekey-%s", n)), + UUID: fmt.Sprintf("test-uuid-%s", n), + Platform: "darwin", + HardwareSerial: n, + }) + require.NoError(t, err) + + _, err = ds.GetHostDEPAssignment(ctx, h.ID) + require.ErrorIs(t, err, sql.ErrNoRows) + + err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}) + require.NoError(t, err) + + assignment, err := ds.GetHostDEPAssignment(ctx, h.ID) + require.NoError(t, err) + require.Equal(t, h.ID, assignment.HostID) + require.Nil(t, assignment.DeletedAt) + + err = ds.DeleteHostDEPAssignments(ctx, []string{h.HardwareSerial}) + require.NoError(t, err) + + assignment, err = ds.GetHostDEPAssignment(ctx, h.ID) + require.NoError(t, err) + require.Equal(t, h.ID, assignment.HostID) + require.NotNil(t, assignment.DeletedAt) + + err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}) + require.NoError(t, err) + assignment, err = ds.GetHostDEPAssignment(ctx, h.ID) + require.NoError(t, err) + require.Equal(t, h.ID, assignment.HostID) + require.Nil(t, assignment.DeletedAt) +} + func createRawAppleCmd(reqType, cmdUUID string) string { return fmt.Sprintf(` diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 28361f0a54..f8a07b8eda 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -16,8 +16,8 @@ import ( "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" + "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/jmoiron/sqlx" ) @@ -1092,7 +1092,7 @@ func (ds *Datastore) applyHostFilters( if errors.Is(err, sql.ErrNoRows) { return "", nil, ctxerr.Wrap( ctx, &fleet.BadRequestError{ - Message: fmt.Sprintf("team is invalid"), + Message: "team is invalid", InternalErr: err, }, ) @@ -2090,6 +2090,7 @@ type hostWithMDMInfo struct { MDMID *uint `db:"mdm_id"` Name *string `db:"name"` EncryptionKeyAvailable *bool `db:"encryption_key_available"` + DEPProfileAssignStatus *string `db:"dep_profile_assign_status"` } // LoadHostByOrbitNodeKey loads the whole host identified by the node key. @@ -2148,7 +2149,8 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) COALESCE(hdek.decryptable, false) as encryption_key_available, IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet, hd.encrypted as disk_encryption_enabled, - t.name as team_name + t.name as team_name, + hdep.assign_profile_response AS dep_profile_assign_status FROM hosts h LEFT OUTER JOIN @@ -2158,7 +2160,7 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) LEFT OUTER JOIN host_dep_assignments hdep ON - hdep.host_id = h.id + hdep.host_id = h.id AND hdep.deleted_at IS NULL LEFT OUTER JOIN mobile_device_management_solutions mdms ON @@ -2185,13 +2187,14 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) // leave MDMInfo nil unless it has mdm information if hostWithMDM.HostID != nil { host.MDMInfo = &fleet.HostMDM{ - HostID: *hostWithMDM.HostID, - Enrolled: *hostWithMDM.Enrolled, - ServerURL: *hostWithMDM.ServerURL, - InstalledFromDep: *hostWithMDM.InstalledFromDep, - IsServer: *hostWithMDM.IsServer, - MDMID: hostWithMDM.MDMID, - Name: *hostWithMDM.Name, + HostID: *hostWithMDM.HostID, + Enrolled: *hostWithMDM.Enrolled, + ServerURL: *hostWithMDM.ServerURL, + InstalledFromDep: *hostWithMDM.InstalledFromDep, + IsServer: *hostWithMDM.IsServer, + MDMID: hostWithMDM.MDMID, + Name: *hostWithMDM.Name, + DEPProfileAssignStatus: hostWithMDM.DEPProfileAssignStatus, } host.MDM = fleet.MDMHostData{ @@ -2260,7 +2263,8 @@ func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken st hm.mdm_id, COALESCE(hm.is_server, false) AS is_server, COALESCE(mdms.name, ?) AS name, - IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet + IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet, + hdep.assign_profile_response AS dep_profile_assign_status FROM host_device_auth hda INNER JOIN @@ -2272,7 +2276,7 @@ func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken st LEFT OUTER JOIN host_mdm hm ON hm.host_id = h.id LEFT OUTER JOIN - host_dep_assignments hdep ON hdep.host_id = h.id + host_dep_assignments hdep ON hdep.host_id = h.id AND hdep.deleted_at IS NULL LEFT OUTER JOIN mobile_device_management_solutions mdms ON hm.mdm_id = mdms.id WHERE @@ -2286,13 +2290,14 @@ func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken st // leave MDMInfo nil unless it has mdm information if hostWithMDM.HostID != nil { host.MDMInfo = &fleet.HostMDM{ - HostID: *hostWithMDM.HostID, - Enrolled: *hostWithMDM.Enrolled, - ServerURL: *hostWithMDM.ServerURL, - InstalledFromDep: *hostWithMDM.InstalledFromDep, - IsServer: *hostWithMDM.IsServer, - MDMID: hostWithMDM.MDMID, - Name: *hostWithMDM.Name, + HostID: *hostWithMDM.HostID, + Enrolled: *hostWithMDM.Enrolled, + ServerURL: *hostWithMDM.ServerURL, + InstalledFromDep: *hostWithMDM.InstalledFromDep, + IsServer: *hostWithMDM.IsServer, + MDMID: hostWithMDM.MDMID, + Name: *hostWithMDM.Name, + DEPProfileAssignStatus: hostWithMDM.DEPProfileAssignStatus, } } return &host, nil diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 32e5507887..106cd57fff 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -7463,6 +7463,7 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) { require.Equal(t, hSimple.ID, loadSimple.MDMInfo.HostID) require.True(t, loadSimple.IsOsqueryEnrolled()) require.False(t, loadSimple.MDMInfo.IsPendingDEPFleetEnrollment()) + require.False(t, loadSimple.IsEligibleForDEPMigration()) // create a host that will be pending enrollment in Fleet MDM hFleet := createOrbitHost("fleet") @@ -7477,6 +7478,8 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) { require.True(t, loadFleet.IsOsqueryEnrolled()) require.True(t, loadFleet.MDMInfo.IsPendingDEPFleetEnrollment()) require.False(t, loadFleet.MDMInfo.IsServer) + require.Empty(t, loadFleet.MDMInfo.DEPProfileAssignStatus) + require.False(t, loadFleet.IsEligibleForDEPMigration()) // force its is_server mdm field to NULL, should be same as false ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { @@ -7487,6 +7490,7 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Equal(t, hFleet.ID, loadFleet.ID) require.False(t, loadFleet.MDMInfo.IsServer) + require.False(t, loadFleet.IsEligibleForDEPMigration()) // fill in disk encryption information require.NoError(t, ds.SetOrUpdateHostDisksEncryption(context.Background(), hFleet.ID, true)) @@ -7500,6 +7504,25 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) { require.NoError(t, err) require.NotNil(t, loadFleet.DiskEncryptionEnabled) require.True(t, *loadFleet.DiskEncryptionEnabled) + require.False(t, loadFleet.IsEligibleForDEPMigration()) + require.Empty(t, loadFleet.MDMInfo.DEPProfileAssignStatus) + + // simulate the device being assigned to Fleet in ABM + err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*hFleet}) + require.NoError(t, err) + loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey) + require.NoError(t, err) + require.Empty(t, loadFleet.MDMInfo.DEPProfileAssignStatus) + + // simulate a failed JSON profile assignment + err = updateHostDEPAssignProfileResponses( + ctx, ds.writer(ctx), ds.logger, + "foo", []string{hFleet.HardwareSerial}, string(fleet.DEPAssignProfileResponseFailed), + ) + require.NoError(t, err) + loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey) + require.NoError(t, err) + require.EqualValues(t, *loadFleet.MDMInfo.DEPProfileAssignStatus, fleet.DEPAssignProfileResponseFailed) } func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expectedKey string, expectedDecryptable *bool) { diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index f81cfbb5b4..1a7344729e 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -382,6 +382,8 @@ type MDMHostData struct { // DEPProfileError is a boolean representing whether Fleet received a "FAILED" response when // attempting to assign a DEP profile for the host. // See https://developer.apple.com/documentation/devicemanagement/assignprofileresponse + // + // It is not filled in by all host-returning datastore methods. DEPProfileError bool `json:"dep_profile_error" db:"dep_profile_error" csv:"mdm.dep_profile_error"` // ServerURL is the server_url stored in the host_mdm table, loaded by // JOIN in datastore @@ -650,6 +652,7 @@ func (h *Host) IsDEPAssignedToFleet() bool { func (h *Host) IsEligibleForDEPMigration() bool { return h.IsOsqueryEnrolled() && h.IsDEPAssignedToFleet() && + h.MDMInfo.HasJSONProfileAssigned() && h.MDMInfo.IsEnrolledInThirdPartyMDM() } @@ -899,13 +902,14 @@ type HostMunkiInfo struct { // used by a host. Note that it uses a different JSON representation than its // struct - it implements a custom JSON marshaler. type HostMDM struct { - HostID uint `db:"host_id" json:"-" csv:"-"` - Enrolled bool `db:"enrolled" json:"-" csv:"-"` - ServerURL string `db:"server_url" json:"-" csv:"-"` - InstalledFromDep bool `db:"installed_from_dep" json:"-" csv:"-"` - IsServer bool `db:"is_server" json:"-" csv:"-"` - MDMID *uint `db:"mdm_id" json:"-" csv:"-"` - Name string `db:"name" json:"-" csv:"-"` + HostID uint `db:"host_id" json:"-" csv:"-"` + Enrolled bool `db:"enrolled" json:"-" csv:"-"` + ServerURL string `db:"server_url" json:"-" csv:"-"` + InstalledFromDep bool `db:"installed_from_dep" json:"-" csv:"-"` + IsServer bool `db:"is_server" json:"-" csv:"-"` + MDMID *uint `db:"mdm_id" json:"-" csv:"-"` + Name string `db:"name" json:"-" csv:"-"` + DEPProfileAssignStatus *string `db:"dep_profile_assign_status" json:"-" csv:"-"` } // IsPendingDEPFleetEnrollment returns true if the host's MDM information @@ -929,6 +933,17 @@ func (h *HostMDM) IsEnrolledInThirdPartyMDM() bool { return h.Enrolled && h.Name != WellKnownMDMFleet } +// HasJSONProfileAssigned returns true if Fleet has assigned an ADE/DEP JSON +// profile to the host, and it'll be enrolled into Fleet the next time the host +// performs automatic enrollment. +func (h *HostMDM) HasJSONProfileAssigned() bool { + // TODO: get rid of h != nil with a better solution once we stablish + // the pattern for dealing with a nil HostMDM + return h != nil && + h.DEPProfileAssignStatus != nil && + *h.DEPProfileAssignStatus == string(DEPAssignProfileResponseSuccess) +} + // IsDEPCapable returns true if and only if the host's MDM information // indicates that the device is capable of doing DEP/AEP enrollments. func (h *HostMDM) IsDEPCapable() bool { diff --git a/server/fleet/hosts_test.go b/server/fleet/hosts_test.go index 18c4fc42b6..83c2e2519c 100644 --- a/server/fleet/hosts_test.go +++ b/server/fleet/hosts_test.go @@ -311,3 +311,137 @@ func TestIsEligibleForBitLockerEncryption(t *testing.T) { hostThatNeedsEnforcement.MDMInfo.Enrolled = true require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) } + +func TestIsEligibleForDEPMigration(t *testing.T) { + testCases := []struct { + name string + osqueryHostID *string + depAssignedToFleet *bool + depProfileResponse DEPAssignProfileResponseStatus + enrolledInThirdPartyMDM bool + expected bool + }{ + { + name: "Eligible for DEP migration", + osqueryHostID: ptr.String("some-id"), + depAssignedToFleet: ptr.Bool(true), + depProfileResponse: DEPAssignProfileResponseSuccess, + enrolledInThirdPartyMDM: true, + expected: true, + }, + { + name: "Not eligible - osqueryHostID nil", + osqueryHostID: nil, + depAssignedToFleet: ptr.Bool(true), + depProfileResponse: DEPAssignProfileResponseSuccess, + enrolledInThirdPartyMDM: true, + expected: false, + }, + { + name: "Not eligible - not DEP assigned to Fleet", + osqueryHostID: ptr.String("some-id"), + depAssignedToFleet: ptr.Bool(false), + depProfileResponse: DEPAssignProfileResponseSuccess, + enrolledInThirdPartyMDM: true, + expected: false, + }, + { + name: "Not eligible - not enrolled in third-party MDM", + osqueryHostID: ptr.String("some-id"), + depAssignedToFleet: ptr.Bool(true), + depProfileResponse: DEPAssignProfileResponseSuccess, + enrolledInThirdPartyMDM: false, + expected: false, + }, + { + name: "Not eligible - not DEP assigned and DEP profile failed", + osqueryHostID: ptr.String("some-id"), + depAssignedToFleet: ptr.Bool(false), + depProfileResponse: DEPAssignProfileResponseNotAccessible, + enrolledInThirdPartyMDM: true, + expected: false, + }, + { + name: "Not eligible - DEP assigned and DEP profile failed", + osqueryHostID: ptr.String("some-id"), + depAssignedToFleet: ptr.Bool(true), + depProfileResponse: DEPAssignProfileResponseFailed, + enrolledInThirdPartyMDM: true, + expected: false, + }, + { + name: "Not eligible - DEP assigned but not response yet", + osqueryHostID: ptr.String("some-id"), + depAssignedToFleet: ptr.Bool(true), + depProfileResponse: "", + enrolledInThirdPartyMDM: true, + expected: false, + }, + { + name: "Not eligible - DEP assigned but not accessible", + osqueryHostID: ptr.String("some-id"), + depAssignedToFleet: ptr.Bool(true), + depProfileResponse: DEPAssignProfileResponseNotAccessible, + enrolledInThirdPartyMDM: true, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + host := &Host{ + OsqueryHostID: tc.osqueryHostID, + DEPAssignedToFleet: tc.depAssignedToFleet, + MDMInfo: &HostMDM{ + Enrolled: tc.enrolledInThirdPartyMDM, + Name: "Some MDM", + DEPProfileAssignStatus: ptr.String(string(tc.depProfileResponse)), + }, + } + + require.Equal(t, tc.expected, host.IsEligibleForDEPMigration()) + }) + } +} + +func TestHasJSONProfileAssigned(t *testing.T) { + testCases := []struct { + name string + hostMDM *HostMDM + expected bool + }{ + { + name: "nil HostMDM", + hostMDM: nil, + expected: false, + }, + { + name: "nil DEPProfileAssignStatus", + hostMDM: &HostMDM{ + DEPProfileAssignStatus: nil, + }, + expected: false, + }, + { + name: "DEPProfileAssignStatus not successful", + hostMDM: &HostMDM{ + DEPProfileAssignStatus: new(string), + }, + expected: false, + }, + { + name: "DEPProfileAssignStatus successful", + hostMDM: &HostMDM{ + DEPProfileAssignStatus: ptr.String(string(DEPAssignProfileResponseSuccess)), + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.hostMDM.HasJSONProfileAssigned() + require.Equal(t, tc.expected, result) + }) + } +} diff --git a/server/service/devices.go b/server/service/devices.go index a96b3bb155..ea8069efc3 100644 --- a/server/service/devices.go +++ b/server/service/devices.go @@ -11,7 +11,6 @@ import ( "time" "github.com/fleetdm/fleet/v4/server/contexts/authz" - authzctx "github.com/fleetdm/fleet/v4/server/contexts/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" hostctx "github.com/fleetdm/fleet/v4/server/contexts/host" "github.com/fleetdm/fleet/v4/server/contexts/logging" @@ -176,7 +175,7 @@ func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.S } func (svc *Service) GetHostDEPAssignment(ctx context.Context, host *fleet.Host) (*fleet.HostDEPAssignment, error) { - alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) + alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) if !alreadyAuthd { if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil { return nil, err diff --git a/server/service/devices_test.go b/server/service/devices_test.go index e937695996..3fc3ba127c 100644 --- a/server/service/devices_test.go +++ b/server/service/devices_test.go @@ -114,10 +114,11 @@ func TestGetFleetDesktopSummary(t *testing.T) { OsqueryHostID: ptr.String("test"), DEPAssignedToFleet: &c.depAssigned, MDMInfo: &fleet.HostMDM{ - IsServer: false, - InstalledFromDep: true, - Enrolled: true, - Name: fleet.WellKnownMDMIntune, + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMIntune, + DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)), }}) sum, err := svc.GetFleetDesktopSummary(ctx) require.NoError(t, err) @@ -206,10 +207,11 @@ func TestGetFleetDesktopSummary(t *testing.T) { OsqueryHostID: ptr.String("test"), DEPAssignedToFleet: &c.depAssigned, MDMInfo: &fleet.HostMDM{ - IsServer: false, - InstalledFromDep: true, - Enrolled: false, - Name: fleet.WellKnownMDMFleet, + IsServer: false, + InstalledFromDep: true, + Enrolled: false, + Name: fleet.WellKnownMDMFleet, + DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)), }}) sum, err := svc.GetFleetDesktopSummary(ctx) require.NoError(t, err) @@ -262,10 +264,11 @@ func TestGetFleetDesktopSummary(t *testing.T) { OsqueryHostID: ptr.String("test"), DEPAssignedToFleet: ptr.Bool(false), MDMInfo: &fleet.HostMDM{ - IsServer: false, - InstalledFromDep: false, - Enrolled: true, - Name: fleet.WellKnownMDMIntune, + IsServer: false, + InstalledFromDep: false, + Enrolled: true, + Name: fleet.WellKnownMDMIntune, + DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)), }}, err: nil, out: fleet.DesktopNotifications{ @@ -279,10 +282,11 @@ func TestGetFleetDesktopSummary(t *testing.T) { DEPAssignedToFleet: ptr.Bool(true), OsqueryHostID: ptr.String("test"), MDMInfo: &fleet.HostMDM{ - IsServer: false, - InstalledFromDep: true, - Enrolled: false, - Name: fleet.WellKnownMDMFleet, + IsServer: false, + InstalledFromDep: true, + Enrolled: false, + Name: fleet.WellKnownMDMFleet, + DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)), }}, err: nil, out: fleet.DesktopNotifications{ @@ -296,10 +300,83 @@ func TestGetFleetDesktopSummary(t *testing.T) { DEPAssignedToFleet: ptr.Bool(true), OsqueryHostID: ptr.String("test"), MDMInfo: &fleet.HostMDM{ - IsServer: false, - InstalledFromDep: true, - Enrolled: true, - Name: fleet.WellKnownMDMFleet, + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMFleet, + DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)), + }}, + err: nil, + out: fleet.DesktopNotifications{ + NeedsMDMMigration: false, + RenewEnrollmentProfile: false, + }, + }, + { + name: "failed ADE assignment status", + host: &fleet.Host{ + DEPAssignedToFleet: ptr.Bool(true), + OsqueryHostID: ptr.String("test"), + MDMInfo: &fleet.HostMDM{ + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMIntune, + DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseFailed)), + }}, + err: nil, + out: fleet.DesktopNotifications{ + NeedsMDMMigration: false, + RenewEnrollmentProfile: false, + }, + }, + { + name: "not accessible ADE assignment status", + host: &fleet.Host{ + DEPAssignedToFleet: ptr.Bool(true), + OsqueryHostID: ptr.String("test"), + MDMInfo: &fleet.HostMDM{ + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMIntune, + DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseNotAccessible)), + }}, + err: nil, + out: fleet.DesktopNotifications{ + NeedsMDMMigration: false, + RenewEnrollmentProfile: false, + }, + }, + { + name: "empty ADE assignment status", + host: &fleet.Host{ + DEPAssignedToFleet: ptr.Bool(true), + OsqueryHostID: ptr.String("test"), + MDMInfo: &fleet.HostMDM{ + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMIntune, + DEPProfileAssignStatus: ptr.String(""), + }}, + err: nil, + out: fleet.DesktopNotifications{ + NeedsMDMMigration: false, + RenewEnrollmentProfile: false, + }, + }, + { + name: "nil ADE assignment status", + host: &fleet.Host{ + DEPAssignedToFleet: ptr.Bool(true), + OsqueryHostID: ptr.String("test"), + MDMInfo: &fleet.HostMDM{ + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMIntune, + DEPProfileAssignStatus: nil, }}, err: nil, out: fleet.DesktopNotifications{ @@ -313,10 +390,11 @@ func TestGetFleetDesktopSummary(t *testing.T) { DEPAssignedToFleet: ptr.Bool(true), OsqueryHostID: ptr.String("test"), MDMInfo: &fleet.HostMDM{ - IsServer: false, - InstalledFromDep: true, - Enrolled: true, - Name: fleet.WellKnownMDMIntune, + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMIntune, + DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)), }}, err: nil, out: fleet.DesktopNotifications{ diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 87640f3ed1..3c1b00b3d4 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -8253,11 +8253,8 @@ func (s *integrationTestSuite) TestOrbitConfigNotifications() { require.False(t, resp.Notifications.RenewEnrollmentProfile) // simulate ABM assignment - mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { - insertAppConfigQuery := `INSERT INTO host_dep_assignments (host_id) VALUES (?)` - _, err = q.ExecContext(context.Background(), insertAppConfigQuery, hFleetMDM.ID) - return err - }) + err = s.ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*hFleetMDM}) + require.NoError(t, err) err = s.ds.SetOrUpdateMDMData(context.Background(), hSimpleMDM.ID, false, true, "https://simplemdm.com", false, fleet.WellKnownMDMSimpleMDM, "") require.NoError(t, err) resp = orbitGetConfigResponse{} diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 9c071f6375..c160444b1a 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -5463,6 +5463,20 @@ func (s *integrationMDMTestSuite) TestMigrateMDMDeviceWebhook() { }, }) require.NoError(t, err) + + case "/profile/devices": + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + var prof profileAssignmentReq + require.NoError(t, json.Unmarshal(b, &prof)) + var resp godep.ProfileResponse + resp.ProfileUUID = prof.ProfileUUID + resp.Devices = map[string]string{ + prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess), + } + encoder := json.NewEncoder(w) + err = encoder.Encode(resp) + require.NoError(t, err) } })) s.runDEPSchedule() @@ -5578,6 +5592,19 @@ func (s *integrationMDMTestSuite) TestMigrateMDMDeviceWebhookErrors() { }, }) require.NoError(t, err) + case "/profile/devices": + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + var prof profileAssignmentReq + require.NoError(t, json.Unmarshal(b, &prof)) + var resp godep.ProfileResponse + resp.ProfileUUID = prof.ProfileUUID + resp.Devices = map[string]string{ + prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess), + } + encoder := json.NewEncoder(w) + err = encoder.Encode(resp) + require.NoError(t, err) } })) s.runDEPSchedule() @@ -7134,6 +7161,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() { require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) // simulate that the device is assigned to Fleet in ABM + profileAssignmentStatusResponse := fleet.DEPAssignProfileResponseSuccess s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) switch r.URL.Path { @@ -7156,9 +7184,33 @@ func (s *integrationMDMTestSuite) TestMDMMigration() { }, }) require.NoError(t, err) + case "/profile/devices": + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + var prof profileAssignmentReq + require.NoError(t, json.Unmarshal(b, &prof)) + var resp godep.ProfileResponse + resp.ProfileUUID = prof.ProfileUUID + resp.Devices = map[string]string{ + prof.Devices[0]: string(profileAssignmentStatusResponse), + } + encoder := json.NewEncoder(w) + err = encoder.Encode(resp) + require.NoError(t, err) } })) - s.runDEPSchedule() + + cleanAssignmentStatus := func() { + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + stmt := `UPDATE host_dep_assignments + SET assign_profile_response = NULL, + response_updated_at = NULL, + profile_uuid = NULL + WHERE host_id = ?` + _, err := q.ExecContext(context.Background(), stmt, host.ID) + return err + }) + } // simulate that the device is enrolled in a third-party MDM and DEP capable err := s.ds.SetOrUpdateMDMData( @@ -7173,24 +7225,70 @@ func (s *integrationMDMTestSuite) TestMDMMigration() { ) require.NoError(t, err) + // simulate a response before we have the chance to assign the profile getDesktopResp = fleetDesktopResponse{} res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK) require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp)) require.NoError(t, res.Body.Close()) require.NoError(t, getDesktopResp.Err) require.Zero(t, *getDesktopResp.FailingPolicies) - require.True(t, getDesktopResp.Notifications.NeedsMDMMigration) + require.False(t, getDesktopResp.Notifications.NeedsMDMMigration) require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile) require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL) require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground) require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL) require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName) require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode) - orbitConfigResp = orbitGetConfigResponse{} s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp) + require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration) + require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) + cleanAssignmentStatus() + + // simulate a "FAILED" JSON profile assignment + profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseFailed + s.runDEPSchedule() + getDesktopResp = fleetDesktopResponse{} + res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK) + require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp)) + require.NoError(t, res.Body.Close()) + require.False(t, getDesktopResp.Notifications.NeedsMDMMigration) + require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) + orbitConfigResp = orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp) + require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration) + require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) + require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial})) + cleanAssignmentStatus() + + // simulate a "NOT_ACCESSIBLE" JSON profile assignment + profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseNotAccessible + s.runDEPSchedule() + getDesktopResp = fleetDesktopResponse{} + res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK) + require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp)) + require.NoError(t, res.Body.Close()) + require.False(t, getDesktopResp.Notifications.NeedsMDMMigration) + require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp) + require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration) + require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) + require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial})) + cleanAssignmentStatus() + + // simulate a "SUCCESS" JSON profile assignment + profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseSuccess + s.runDEPSchedule() + getDesktopResp = fleetDesktopResponse{} + res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK) + require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp)) + require.NoError(t, res.Body.Close()) + require.True(t, getDesktopResp.Notifications.NeedsMDMMigration) + require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp) require.True(t, orbitConfigResp.Notifications.NeedsMDMMigration) require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile) + cleanAssignmentStatus() // simulate that the device needs to be enrolled in fleet, DEP capable err = s.ds.SetOrUpdateMDMData( @@ -7260,6 +7358,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() { host := createOrbitEnrolledHost(t, "darwin", "h", s.ds) createDeviceTokenForHost(t, s.ds, host.ID, token) checkMigrationResponses(host, token) + require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial})) tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team-1"}) require.NoError(t, err) diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go index 822d67dfd8..0f597102b2 100644 --- a/server/worker/apple_mdm_test.go +++ b/server/worker/apple_mdm_test.go @@ -75,12 +75,8 @@ func TestAppleMDM(t *testing.T) { return err }) if depAssignedToFleet { - mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { - _, err := q.ExecContext(ctx, ` - INSERT INTO host_dep_assignments (host_id) VALUES (?) ON DUPLICATE KEY UPDATE host_id = host_id, deleted_at = NULL - `, h.ID) - return err - }) + err := ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}) + require.NoError(t, err) } err = ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "http://example.com", depAssignedToFleet, fleet.WellKnownMDMFleet, "") require.NoError(t, err) From 4fec1c9efb3f38a88b14b77b8baaec4229d8beaa Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 8 Apr 2024 10:20:15 -0500 Subject: [PATCH 06/83] Website: browser compatibility fixes 2024-04-05 (#18109) Closes: #18089 Closes: #18107 Changes: - Updated styles for the buttons on the /register and /login pages to fix alignment issues on Opera v64. - Updated the style of the "start now" header button to fix an alignment issue on Opera v64. - updated the animation the logo carousel uses to fix weird behavior on Safari 13 --- .../assets/styles/components/logo-carousel.component.less | 4 ++-- website/assets/styles/layout.less | 2 ++ website/assets/styles/pages/entrance/login.less | 3 +-- website/assets/styles/pages/entrance/signup.less | 3 +-- website/views/layouts/layout.ejs | 6 ------ 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/website/assets/styles/components/logo-carousel.component.less b/website/assets/styles/components/logo-carousel.component.less index 2e057bfc05..b16e8ace53 100644 --- a/website/assets/styles/components/logo-carousel.component.less +++ b/website/assets/styles/components/logo-carousel.component.less @@ -57,10 +57,10 @@ } @keyframes scroll-horizontal { 0% { - transform: translateX(50%); + transform: translateX(25%); } 100% { - transform: translateX(-50%); + transform: translateX(-75%); } } } diff --git a/website/assets/styles/layout.less b/website/assets/styles/layout.less index 210cc4914b..a03435ff6c 100644 --- a/website/assets/styles/layout.less +++ b/website/assets/styles/layout.less @@ -74,6 +74,7 @@ html, body { position: relative; font-size: 14px; font-weight: 700; + line-height: 1; padding: 16px; box-shadow: 1px 1px 2px rgba(25, 33, 71, 0.24); border-radius: 4px; @@ -231,6 +232,7 @@ html, body { position: relative; font-size: 14px; font-weight: 700; + line-height: 1; padding: 16px; box-shadow: 1px 1px 2px rgba(25, 33, 71, 0.24); border-radius: 4px; diff --git a/website/assets/styles/pages/entrance/login.less b/website/assets/styles/pages/entrance/login.less index f49be521f6..42cadfb9be 100644 --- a/website/assets/styles/pages/entrance/login.less +++ b/website/assets/styles/pages/entrance/login.less @@ -62,7 +62,6 @@ border-radius: 8px; padding-top: 16px; padding-bottom: 16px; - height: 48px; display: flex; align-items: center; span { @@ -70,7 +69,7 @@ margin-left: auto; margin-right: auto; font-size: 16px; - line-height: 20px; + line-height: 16px; text-align: center; font-weight: 700; } diff --git a/website/assets/styles/pages/entrance/signup.less b/website/assets/styles/pages/entrance/signup.less index 6c9a3b2c78..058d58681c 100644 --- a/website/assets/styles/pages/entrance/signup.less +++ b/website/assets/styles/pages/entrance/signup.less @@ -66,7 +66,6 @@ border-radius: 8px; padding-top: 16px; padding-bottom: 16px; - height: 48px; display: flex; align-items: center; span { @@ -74,7 +73,7 @@ margin-left: auto; margin-right: auto; font-size: 16px; - line-height: 20px; + line-height: 16px; text-align: center; font-weight: 700; } diff --git a/website/views/layouts/layout.ejs b/website/views/layouts/layout.ejs index dd6720d902..8e2c6ac3cc 100644 --- a/website/views/layouts/layout.ejs +++ b/website/views/layouts/layout.ejs @@ -525,12 +525,6 @@ - - - - - - From b9ef0290b973ac64a3f75fd7f0a4eb921931f264 Mon Sep 17 00:00:00 2001 From: George Karr Date: Mon, 8 Apr 2024 10:49:47 -0500 Subject: [PATCH 07/83] Fixed invalid bash (empty if) / ignore directories we don't need to change versions (#18116) --- tools/release/publish_release.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/release/publish_release.sh b/tools/release/publish_release.sh index 6cc615af17..f056d08ad5 100755 --- a/tools/release/publish_release.sh +++ b/tools/release/publish_release.sh @@ -190,7 +190,7 @@ build_changelog() { prompt=$'I am creating a changelog for an open source project from a list of commit messages. Please format it for me using the following rules:\n1. Correct spelling and punctuation.\n2. Sentence casing.\n3. Past tense.\n4. Each list item is designated with an asterisk.\n5. Output in markdown format.' if [[ "$main_release" == "true" ]]; then # Place to make a main targeted prompt - #prompt=$'I am creating a changelog for an open source project from a list of commit messages. Please format it for me using the following rules:\n1. Correct spelling and punctuation.\n2. Sentence casing.\n3. Past tense.\n4. Each list item is designated with an asterisk.\n5. Output in markdown format.' + prompt=$'I am creating a changelog for an open source project from a list of commit messages. Please format it for me using the following rules:\n1. Correct spelling and punctuation.\n2. Sentence casing.\n3. Past tense.\n4. Each list item is designated with an asterisk.\n5. Output in markdown format.' fi content=$(cat new_changelog | sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g') @@ -263,7 +263,7 @@ changelog_and_versions() { cp /tmp/CHANGELOG.md . git add CHANGELOG.md escaped_start_version=$(echo "$start_milestone" | sed 's/\./\\./g') - version_files=`ack -l --ignore-file=is:CHANGELOG.md "$escaped_start_version"` + version_files=`ack -l --ignore-dir=tools/release --ignore-dir=articles --ignore-file=is:CHANGELOG.md "$escaped_start_version"` unameOut="$(uname -s)" case "${unameOut}" in Linux*) echo "$version_files" | xargs sed -i "s/$escaped_start_version/$target_milestone/g";; From d89af249557fa67d2a545999547c565dc0212f9b Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 8 Apr 2024 10:51:15 -0500 Subject: [PATCH 08/83] Website: Update /start questionnaire to update leads (#18120) Changes: - Updated the `save-questionnaire-progress` to send a request to Zapier when a user completes the "What are you using Fleet for" step of the /start questionnaire. --- .../controllers/save-questionnaire-progress.js | 16 ++++++++++++++++ website/assets/js/pages/start.page.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/website/api/controllers/save-questionnaire-progress.js b/website/api/controllers/save-questionnaire-progress.js index 8ef697de53..3f53545906 100644 --- a/website/api/controllers/save-questionnaire-progress.js +++ b/website/api/controllers/save-questionnaire-progress.js @@ -59,6 +59,22 @@ module.exports = { await User.updateOne({id: this.req.me.id}).set({ primaryBuyingSituation }); + // Send a POST request to Zapier + await sails.helpers.http.post.with({ + url: 'https://hooks.zapier.com/hooks/catch/3627242/3pl7yt1/', + data: { + primaryBuyingSituation, + emailAddress: this.req.me.emailAddress, + webhookSecret: sails.config.custom.zapierSandboxWebhookSecret, + } + }) + .timeout(5000) + .tolerate(['non200Response', 'requestFailed'], (err)=>{ + // Note that Zapier responds with a 2xx status code even if something goes wrong, so just because this message is not logged doesn't mean everything is hunky dory. More info: https://github.com/fleetdm/fleet/pull/6380#issuecomment-1204395762 + sails.log.warn(`When a user completed the 'What are you using Fleet for' questionnaire step, a lead/contact could not be updated in the CRM for this email address: ${this.req.me.emailAddress}. Raw error: ${err}`); + return; + }); + // Set the primary buying situation in the user's session. this.req.session.primaryBuyingSituation = primaryBuyingSituation; } diff --git a/website/assets/js/pages/start.page.js b/website/assets/js/pages/start.page.js index 9b7173da0b..47dfe0bc98 100644 --- a/website/assets/js/pages/start.page.js +++ b/website/assets/js/pages/start.page.js @@ -75,7 +75,7 @@ parasails.registerPage('start', { } // If this user has not completed the 'what are you using fleet for' step, and has a primaryBuyingSituation set by an ad. prefill the formData for this step. if(this.primaryBuyingSituation && _.isEmpty(this.formData['what-are-you-using-fleet-for'])){ - this.formData['what-are-you-using-fleet-for'].primaryBuyingSituation = _.clone(this.primaryBuyingSituation); + this.formData['what-are-you-using-fleet-for'] = {primaryBuyingSituation: this.primaryBuyingSituation}; } }, mounted: async function() { From 02f4d5c134a4f8d201d316139f15d86af65cb80a Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Mon, 8 Apr 2024 13:19:56 -0300 Subject: [PATCH 09/83] run make generate-docs for DDM activities (#17888) --- docs/Using Fleet/Audit-logs.md | 59 +++++++++++++++++++ docs/Using Fleet/Understanding-host-vitals.md | 32 ++++++---- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/docs/Using Fleet/Audit-logs.md b/docs/Using Fleet/Audit-logs.md index 2367b31d9f..700f2e869a 100644 --- a/docs/Using Fleet/Audit-logs.md +++ b/docs/Using Fleet/Audit-logs.md @@ -1049,6 +1049,65 @@ This activity contains the following fields: } ``` +## created_declaration_profile + +Generated when a user adds a new macOS declaration to a team (or no team). + +This activity contains the following fields: +- "profile_name": Name of the declaration. +- "identifier": Identifier of the declaration. +- "team_id": The ID of the team that the declaration applies to, `null` if it applies to devices that are not in a team. +- "team_name": The name of the team that the declaration applies to, `null` if it applies to devices that are not in a team. + +#### Example + +```json +{ + "profile_name": "Passcode requirements", + "profile_identifier": "com.my.declaration", + "team_id": 123, + "team_name": "Workstations" +} +``` + +## deleted_declaration_profile + +Generated when a user removes a macOS declaration from a team (or no team). + +This activity contains the following fields: +- "profile_name": Name of the declaration. +- "identifier": Identifier of the declaration. +- "team_id": The ID of the team that the declaration applies to, `null` if it applies to devices that are not in a team. +- "team_name": The name of the team that the declaration applies to, `null` if it applies to devices that are not in a team. + +#### Example + +```json +{ + "profile_name": "Passcode requirements", + "profile_identifier": "com.my.declaration", + "team_id": 123, + "team_name": "Workstations" +} +``` + +## edited_declaration_profile + +Generated when a user edits the macOS declarations of a team (or no team) via the fleetctl CLI. + +This activity contains the following fields: +- "team_id": The ID of the team that the declarations apply to, `null` if they apply to devices that are not in a team. +- "team_name": The name of the team that the declarations apply to, `null` if they apply to devices that are not in a team. + +#### Example + +```json +{ + "team_id": 123, + "team_name": "Workstations" +} +``` + diff --git a/docs/Using Fleet/Understanding-host-vitals.md b/docs/Using Fleet/Understanding-host-vitals.md index 47dca9f92c..2fe0abb033 100644 --- a/docs/Using Fleet/Understanding-host-vitals.md +++ b/docs/Using Fleet/Understanding-host-vitals.md @@ -199,7 +199,7 @@ WITH registry_keys AS ( -- coalesce to 'unknown' and keep that state in the list -- in order to account for hosts that might not have this -- key, and servers - WHERE COALESCE(e.state, '0') IN ('0', '1', '2') + WHERE COALESCE(e.state, '0') IN ('0', '1', '2', '3') LIMIT 1; ``` @@ -373,12 +373,20 @@ SELECT * FROM os_version LIMIT 1 - Query: ```sql -SELECT os.name, r.data as display_version, k.version +WITH display_version_table AS ( + SELECT data as display_version + FROM registry + WHERE path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion' + ) + SELECT + os.name, + COALESCE(d.display_version, '') AS display_version, + k.version FROM - registry r, os_version os, kernel_info k - WHERE r.path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion' + LEFT JOIN + display_version_table d ``` ## os_windows @@ -387,19 +395,23 @@ SELECT os.name, r.data as display_version, k.version - Query: ```sql -SELECT +WITH display_version_table AS ( + SELECT data as display_version + FROM registry + WHERE path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion' + ) + SELECT os.name, os.platform, os.arch, k.version as kernel_version, os.version, - r.data as display_version + COALESCE(d.display_version, '') AS display_version FROM os_version os, - kernel_info k, - registry r - WHERE - r.path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion' + kernel_info k + LEFT JOIN + display_version_table d ``` ## osquery_flags From 36366f90718691a3e49de2ef98e6c325077e37c7 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 8 Apr 2024 12:28:46 -0500 Subject: [PATCH 10/83] Website: Update parallax city component (#18124) Changes: - Updated function names used by the parallax-city component. --- .../js/components/parallax-city.component.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/website/assets/js/components/parallax-city.component.js b/website/assets/js/components/parallax-city.component.js index 6f3de558ef..423aa7d735 100644 --- a/website/assets/js/components/parallax-city.component.js +++ b/website/assets/js/components/parallax-city.component.js @@ -61,14 +61,14 @@ parasails.registerComponent('parallaxCity', { } // Check for hardware/graphics acceleration. if(bowser.chrome || bowser.opera) { - this.enableAnimation = this.isHardwareAccelerationEnabledOnChromiumBrowsers(); + this.enableAnimation = this._isHardwareAccelerationEnabledOnChromiumBrowsers(); } else if(bowser.firefox){ - this.enableAnimation = this.isHardwareAccelerationEnabledOnFirefox(); + this.enableAnimation = this._isHardwareAccelerationEnabledOnFirefox(); } }, mounted: async function(){ - if(!this.enableAnimation) { - this.setupParallaxAnimation(); + if(this.enableAnimation) { + this._setupParallaxAnimation(); } }, beforeDestroy: function() { @@ -79,13 +79,13 @@ parasails.registerComponent('parallaxCity', { // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ methods: { - getElementPositions: function() { + _getElementPositions: function() { this.elementHeight = this.parallaxCityElement.clientHeight; this.distanceFromTopOfPage = this.parallaxCityElement.offsetTop; this.distanceFromBottomOfPage = document.body.scrollHeight - this.distanceFromTopOfPage - (this.elementHeight * .5); this.elementBottomPosition = this.elementHeight + this.distanceFromTopOfPage; }, - setupParallaxAnimation: function() { + _setupParallaxAnimation: function() { // Store a reference to the parent container, we'll use this to determine the elements position relative to the user's viewport. this.parallaxCityElement = $('div[purpose="parallax-city-container"]')[0]; // Build an array of parallax layers, and set the initial bottom position of each layer to be negative the layer's scroll amount. @@ -95,15 +95,15 @@ parasails.registerComponent('parallaxCity', { this.parallaxLayers.push({element: layer, scrollAmount}); } // Determine the parallax image's position on the page/user's viewport. - this.getElementPositions(); + this._getElementPositions(); // If the bottom of the element is within the user's viewport, update the positions of the layers. if(this.parallaxCityElement.getBoundingClientRect().bottom > this.parallaxCityElement.offsetTop) { this.scrollParallaxLayers(); } // Add a scroll event listener - $(window).scroll(this.throttleParallaxScroll); + $(window).scroll(this._throttleParallaxScroll); // Add a resize event listener. - $(window).resize(this.getElementPositions); + $(window).resize(this._getElementPositions); }, scrollParallaxLayers: function() { if(!this.parallaxLayersAreCurrentlyAnimating) { @@ -122,13 +122,13 @@ parasails.registerComponent('parallaxCity', { } } }, - throttleParallaxScroll: function() { + _throttleParallaxScroll: function() { this.scrollParallaxLayers(); setTimeout(()=>{ this.parallaxLayersAreCurrentlyAnimating = false; }, 100); }, - isHardwareAccelerationEnabledOnChromiumBrowsers: function() { + _isHardwareAccelerationEnabledOnChromiumBrowsers: function() { let isHardwareAccelerationEnabled = true; // For Chromium based browsers, we'll check the vendor of the user's graphics card. // See https://gist.github.com/cvan/042b2448fcecefafbb6a91469484cdf8 for more info about this method. @@ -148,7 +148,7 @@ parasails.registerComponent('parallaxCity', { } return isHardwareAccelerationEnabled; }, - isHardwareAccelerationEnabledOnFirefox: function() { + _isHardwareAccelerationEnabledOnFirefox: function() { // For Firefox, the method we use for chrome does not always work. // Instead, we'll run two tests, one with forced software rendering, and one without to see if the results are the same. // See https://stackoverflow.com/a/77170999 for more info about this method. From 05ccf9ee23179bdc6a527d829d3a03744c155c17 Mon Sep 17 00:00:00 2001 From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:34:55 -0500 Subject: [PATCH 11/83] Fix issues related to Fleet builtin labels and reserved MDM profile names (#18043) --- changes/18003-windows-mdm-reserved-profiles | 1 + changes/18034-builtin-labels | 1 + frontend/interfaces/platform.ts | 4 +- frontend/pages/LabelPage/LabelPage.tsx | 8 + .../hosts/ManageHostsPage/ManageHostsPage.tsx | 7 +- .../HostsFilterBlock/HostsFilterBlock.tsx | 5 +- .../LabelFilterSelect/LabelFilterSelect.tsx | 18 +- .../components/LabelFilterSelect/constants.ts | 7 - frontend/utilities/constants.tsx | 58 +++++- frontend/utilities/helpers.tsx | 7 +- server/datastore/mysql/labels.go | 5 + server/datastore/mysql/microsoft_mdm.go | 6 + server/fleet/labels.go | 26 +++ server/mdm/mdm.go | 6 + server/service/integration_core_test.go | 79 +++++++- server/service/integration_mdm_test.go | 180 ++++++++++++++++++ server/service/labels.go | 41 ++++ server/service/testing_client.go | 2 +- server/test/new_objects.go | 68 +++++++ 19 files changed, 501 insertions(+), 28 deletions(-) create mode 100644 changes/18003-windows-mdm-reserved-profiles create mode 100644 changes/18034-builtin-labels diff --git a/changes/18003-windows-mdm-reserved-profiles b/changes/18003-windows-mdm-reserved-profiles new file mode 100644 index 0000000000..559a08d646 --- /dev/null +++ b/changes/18003-windows-mdm-reserved-profiles @@ -0,0 +1 @@ +- Fixed issue where applying Windows MDM profiles using `fleetctl apply` would cause Fleet to overwrite the reserved profile used to manage Windows OS updates. \ No newline at end of file diff --git a/changes/18034-builtin-labels b/changes/18034-builtin-labels new file mode 100644 index 0000000000..4f351a9fdf --- /dev/null +++ b/changes/18034-builtin-labels @@ -0,0 +1 @@ +- Updated label endpoints and UI to prevent creating, updating, or deleting built-in labels. diff --git a/frontend/interfaces/platform.ts b/frontend/interfaces/platform.ts index 6add6961f6..fe74bb70ab 100644 --- a/frontend/interfaces/platform.ts +++ b/frontend/interfaces/platform.ts @@ -73,5 +73,7 @@ export const HOST_LINUX_PLATFORMS = [ * the possible Linux-like platform values. */ export const isLinuxLike = (platform: string) => { - return HOST_LINUX_PLATFORMS.includes(platform as any); + return HOST_LINUX_PLATFORMS.includes( + platform as typeof HOST_LINUX_PLATFORMS[number] + ); }; diff --git a/frontend/pages/LabelPage/LabelPage.tsx b/frontend/pages/LabelPage/LabelPage.tsx index 40756c1b63..8257f32972 100644 --- a/frontend/pages/LabelPage/LabelPage.tsx +++ b/frontend/pages/LabelPage/LabelPage.tsx @@ -108,6 +108,10 @@ const LabelPage = ({ setLabelValidator({ name: "A label with this name already exists", }); + } else if (updateError.data.errors[0].reason.includes("built-in")) { + setLabelValidator({ + name: "A built-in label with this name already exists", + }); } else if ( updateError.data.errors[0].reason.includes( "Data too long for column 'name'" @@ -150,6 +154,10 @@ const LabelPage = ({ setLabelValidator({ name: "A label with this name already exists", }); + } else if (updateError.data.errors[0].reason.includes("built-in")) { + setLabelValidator({ + name: "A built-in label with this name already exists", + }); } else if ( updateError.data.errors[0].reason.includes( "Data too long for column 'name'" diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index c6a1c62ecf..0bd5f3cb7e 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -45,6 +45,7 @@ import { IEnrollSecret, IEnrollSecretsResponse, } from "interfaces/enroll_secret"; +import { getErrorReason } from "interfaces/errors"; import { ILabel } from "interfaces/label"; import { IOperatingSystemVersion } from "interfaces/operating_system"; import { IPolicy, IStoredPolicyResponse } from "interfaces/policy"; @@ -1023,7 +1024,11 @@ const ManageHostsPage = ({ renderFlash("success", "Successfully deleted label."); } catch (error) { console.error(error); - renderFlash("error", "Could not delete label. Please try again."); + if (getErrorReason(error).includes("built-in")) { + renderFlash("error", "Built-in labels can’t be modified or deleted."); + } else { + renderFlash("error", "Could not delete label. Please try again."); + } } finally { setIsUpdatingLabel(false); } diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx index 6a7e3da6fd..b122344957 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx @@ -22,6 +22,7 @@ import { import { PLATFORM_LABEL_DISPLAY_NAMES, + isPlatformLabelNameFromAPI, PolicyResponse, } from "utilities/constants"; @@ -134,7 +135,9 @@ const HostsFilterBlock = ({ if (selectedLabel) { const { description, display_text, label_type } = selectedLabel; const pillLabel = - PLATFORM_LABEL_DISPLAY_NAMES[display_text] ?? display_text; + (isPlatformLabelNameFromAPI(display_text) && + PLATFORM_LABEL_DISPLAY_NAMES[display_text]) || + display_text; return ( <> diff --git a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx index 9f5e5bbb8c..355e87d665 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx @@ -3,11 +3,15 @@ import Select, { GroupBase, SelectInstance, components } from "react-select-5"; import classnames from "classnames"; import { ILabel } from "interfaces/label"; -import { PLATFORM_LABEL_DISPLAY_NAMES } from "utilities/constants"; +import { + hasPlatformTypeIcon, + isPlatformLabelNameFromAPI, + PLATFORM_LABEL_DISPLAY_NAMES, + PLATFORM_TYPE_ICONS, +} from "utilities/constants"; import Icon from "components/Icon"; import CustomLabelGroupHeading from "../CustomLabelGroupHeading"; -import { PLATFORM_TYPE_ICONS } from "./constants"; import { createDropdownOptions, IEmptyOption, IGroupOption } from "./helpers"; import CustomDropdownIndicator from "../CustomDropdownIndicator"; @@ -39,23 +43,25 @@ const formatOptionLabel = (data: ILabel | IEmptyOption) => { const isLabel = "display_text" in data; const isPlatform = isLabel && data.type === "platform"; - let labelText = isLabel ? data.display_text : data.label; + let displayText = isLabel ? data.display_text : data.label; // the display names for platform options are slightly different then the display_text // property, so we get the correct display name here if (isLabel && isPlatform) { - labelText = PLATFORM_LABEL_DISPLAY_NAMES[data.display_text]; + if (isPlatformLabelNameFromAPI(data.display_text)) { + displayText = PLATFORM_LABEL_DISPLAY_NAMES[data.display_text]; + } } return (
- {isPlatform && ( + {isLabel && hasPlatformTypeIcon(data.display_text) && ( )} - {labelText} + {displayText}
); }; diff --git a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/constants.ts b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/constants.ts index 697b5b7880..3fd33b126e 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/constants.ts +++ b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/constants.ts @@ -8,11 +8,4 @@ export const EMPTY_OPTION = { isDisabled: true, }; -export const PLATFORM_TYPE_ICONS: Record = { - "All Linux": "linux", - macOS: "darwin", - "MS Windows": "windows", - chrome: "chrome", -}; - export const FILTERED_LINUX = ["Red Hat Linux", "CentOS Linux", "Ubuntu Linux"]; diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx index 186dd39184..e16a487145 100644 --- a/frontend/utilities/constants.tsx +++ b/frontend/utilities/constants.tsx @@ -4,6 +4,7 @@ import paths from "router/paths"; import { ISchedulableQuery } from "interfaces/schedulable_query"; import React from "react"; import { IDropdownOption } from "interfaces/dropdownOption"; +import { IconNames } from "components/icons"; const { origin } = global.window.location; export const BASE_URL = `${origin}${URL_PREFIX}/api`; @@ -184,6 +185,25 @@ export const DEFAULT_CAMPAIGN_STATE = { campaign: { ...DEFAULT_CAMPAIGN }, }; +const PLATFORM_LABEL_NAMES_FROM_API = [ + "All Hosts", + "All Linux", + "CentOS Linux", + "macOS", + "MS Windows", + "Red Hat Linux", + "Ubuntu Linux", + "chrome", +] as const; + +type PlatformLabelNameFromAPI = typeof PLATFORM_LABEL_NAMES_FROM_API[number]; + +export const isPlatformLabelNameFromAPI = ( + s: string +): s is PlatformLabelNameFromAPI => { + return PLATFORM_LABEL_NAMES_FROM_API.includes(s as PlatformLabelNameFromAPI); +}; + export const PLATFORM_DISPLAY_NAMES: Record = { darwin: "macOS", macOS: "macOS", @@ -193,10 +213,13 @@ export const PLATFORM_DISPLAY_NAMES: Record = { Linux: "Linux", chrome: "ChromeOS", ChromeOS: "ChromeOS", -}; +} as const; // as returned by the TARGETS API; based on display_text -export const PLATFORM_LABEL_DISPLAY_NAMES: Record = { +export const PLATFORM_LABEL_DISPLAY_NAMES: Record< + PlatformLabelNameFromAPI, + string +> = { "All Hosts": "All hosts", "All Linux": "Linux", "CentOS Linux": "CentOS Linux", @@ -205,7 +228,7 @@ export const PLATFORM_LABEL_DISPLAY_NAMES: Record = { "Red Hat Linux": "Red Hat Linux", "Ubuntu Linux": "Ubuntu Linux", chrome: "ChromeOS", -}; +} as const; export const PLATFORM_LABEL_DISPLAY_ORDER = [ "macOS", @@ -214,9 +237,12 @@ export const PLATFORM_LABEL_DISPLAY_ORDER = [ "Red Hat Linux", "Ubuntu Linux", "MS Windows", -]; +] as const; -export const PLATFORM_LABEL_DISPLAY_TYPES: Record = { +export const PLATFORM_LABEL_DISPLAY_TYPES: Record< + PlatformLabelNameFromAPI, + string +> = { "All Hosts": "all", "All Linux": "platform", "CentOS Linux": "platform", @@ -225,6 +251,28 @@ export const PLATFORM_LABEL_DISPLAY_TYPES: Record = { "Red Hat Linux": "platform", "Ubuntu Linux": "platform", chrome: "platform", +} as const; + +export const PLATFORM_TYPE_ICONS: Record< + Extract< + PlatformLabelNameFromAPI, + "All Linux" | "macOS" | "MS Windows" | "chrome" + >, + IconNames +> = { + "All Linux": "linux", + macOS: "darwin", + "MS Windows": "windows", + chrome: "chrome", +} as const; + +export const hasPlatformTypeIcon = ( + s: string +): s is Extract< + PlatformLabelNameFromAPI, + "All Linux" | "macOS" | "MS Windows" | "chrome" +> => { + return !!PLATFORM_TYPE_ICONS[s as keyof typeof PLATFORM_TYPE_ICONS]; }; interface IPlatformDropdownOptions { diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx index 14bfe56640..e18ee526ec 100644 --- a/frontend/utilities/helpers.tsx +++ b/frontend/utilities/helpers.tsx @@ -51,6 +51,7 @@ import { DEFAULT_GRAVATAR_LINK_DARK_FALLBACK, INITIAL_FLEET_DATE, PLATFORM_LABEL_DISPLAY_TYPES, + isPlatformLabelNameFromAPI, } from "utilities/constants"; import { IScheduledQueryStats } from "interfaces/scheduled_query_stats"; import { IDropdownOption } from "interfaces/dropdownOption"; @@ -220,10 +221,14 @@ export const formatFloatAsPercentage = (float?: number): string => { const formatLabelResponse = (response: any): ILabel[] => { const labels = response.labels.map((label: ILabel) => { + let labelType = "custom"; + if (isPlatformLabelNameFromAPI(label.display_text)) { + labelType = PLATFORM_LABEL_DISPLAY_TYPES[label.display_text]; + } return { ...label, slug: labelSlug(label), - type: PLATFORM_LABEL_DISPLAY_TYPES[label.display_text] || "custom", + type: labelType, target_type: "labels", }; }); diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go index c2075b3a5f..8507a07625 100644 --- a/server/datastore/mysql/labels.go +++ b/server/datastore/mysql/labels.go @@ -15,6 +15,11 @@ import ( func (ds *Datastore) ApplyLabelSpecs(ctx context.Context, specs []*fleet.LabelSpec) (err error) { err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + // TODO: do we want to allow on duplicate updating label_type or + // label_membership_type or should those always be immutable? + // are we ok depending solely on the caller to ensure that these fields + // are not changed? + sql := ` INSERT INTO labels ( name, diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 3380815902..2da15a65e3 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -1693,6 +1693,12 @@ ON DUPLICATE KEY UPDATE keepNames = append(keepNames, p.Name) } } + for n := range mdm.FleetReservedProfileNames() { + if _, ok := incomingProfs[n]; !ok { + // always keep reserved profiles even if they're not incoming + keepNames = append(keepNames, n) + } + } var ( stmt string diff --git a/server/fleet/labels.go b/server/fleet/labels.go index c754c80767..5925bf2c01 100644 --- a/server/fleet/labels.go +++ b/server/fleet/labels.go @@ -131,3 +131,29 @@ type LabelSpec struct { LabelMembershipType LabelMembershipType `json:"label_membership_type" db:"label_membership_type"` Hosts []string `json:"hosts,omitempty"` } + +const ( + BuiltinLabelNameAllHosts = "All Hosts" + BuiltinLabelNameMacOS = "macOS" + BuiltinLabelNameUbuntuLinux = "Ubuntu Linux" + BuiltinLabelNameCentOSLinux = "CentOS Linux" + BuiltinLabelNameWindows = "MS Windows" + BuiltinLabelNameRedHatLinux = "Red Hat Linux" + BuiltinLabelNameAllLinux = "All Linux" + BuiltinLabelNameChrome = "chrome" +) + +// ReservedLabelNames returns a map of label name strings +// that are reserved by Fleet. +func ReservedLabelNames() map[string]struct{} { + return map[string]struct{}{ + BuiltinLabelNameAllHosts: {}, + BuiltinLabelNameMacOS: {}, + BuiltinLabelNameUbuntuLinux: {}, + BuiltinLabelNameCentOSLinux: {}, + BuiltinLabelNameWindows: {}, + BuiltinLabelNameRedHatLinux: {}, + BuiltinLabelNameAllLinux: {}, + BuiltinLabelNameChrome: {}, + } +} diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go index e2888fbc50..9adc0c1988 100644 --- a/server/mdm/mdm.go +++ b/server/mdm/mdm.go @@ -108,3 +108,9 @@ func FleetReservedProfileNames() map[string]struct{} { func ListFleetReservedWindowsProfileNames() []string { return []string{FleetWindowsOSUpdatesProfileName} } + +// ListFleetReservedMacOSProfileNames returns a list of PayloadDisplayName strings +// that are reserved by Fleet for macOS. +func ListFleetReservedMacOSProfileNames() []string { + return []string{FleetFileVaultProfileName, FleetdConfigProfileName} +} diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 3c1b00b3d4..43be6eff3c 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -3581,19 +3581,25 @@ func (s *integrationTestSuite) TestLabels() { t := s.T() // list labels, has the built-in ones + builtinsMap := fleet.ReservedLabelNames() var listResp listLabelsResponse s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp) assert.True(t, len(listResp.Labels) > 0) for _, lbl := range listResp.Labels { + _, ok := builtinsMap[lbl.Name] + assert.True(t, ok) assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType) } builtInsCount := len(listResp.Labels) + require.Equal(t, builtInsCount, len(builtinsMap)) // labels summary has the built-in ones var summaryResp getLabelsSummaryResponse s.DoJSON("GET", "/api/latest/fleet/labels/summary", nil, http.StatusOK, &summaryResp) assert.Len(t, summaryResp.Labels, builtInsCount) for _, lbl := range summaryResp.Labels { + _, ok := builtinsMap[lbl.Name] + assert.True(t, ok) assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType) } @@ -3601,6 +3607,11 @@ func (s *integrationTestSuite) TestLabels() { var createResp createLabelResponse s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Query: ptr.String("select 1")}, http.StatusUnprocessableEntity, &createResp) + // create invalid label, conflicts with builtin name + for n := range builtinsMap { + s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String(n), Query: ptr.String("select 1")}, http.StatusUnprocessableEntity, &createResp) + } + // create a valid label s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String(t.Name()), Query: ptr.String("select 1")}, http.StatusOK, &createResp) assert.NotZero(t, createResp.Label.ID) @@ -3621,6 +3632,11 @@ func (s *integrationTestSuite) TestLabels() { assert.Equal(t, lbl1.ID, modResp.Label.ID) assert.NotEqual(t, lbl1.Name, modResp.Label.Name) + // attempt to modify a label to a reserved name + for n := range builtinsMap { + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/labels/%d", lbl1.ID), &fleet.ModifyLabelPayload{Name: ptr.String(n)}, http.StatusUnprocessableEntity, &modResp) + } + // modify a non-existing label s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/labels/%d", lbl1.ID+1), &fleet.ModifyLabelPayload{Name: ptr.String("zzz")}, http.StatusNotFound, &modResp) @@ -3633,7 +3649,7 @@ func (s *integrationTestSuite) TestLabels() { assert.Len(t, summaryResp.Labels, builtInsCount+1) // next page is empty - s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "per_page", "2", "page", "1", "query", t.Name()) + s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "per_page", strconv.Itoa(builtInsCount+1), "page", "1", "query", t.Name()) assert.Len(t, listResp.Labels, 0) // create another label @@ -3734,15 +3750,22 @@ func (s *integrationTestSuite) TestLabels() { // list labels, only the built-ins remain s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "per_page", strconv.Itoa(builtInsCount+1)) assert.Len(t, listResp.Labels, builtInsCount) + idsByName := make(map[string]uint, len(listResp.Labels)) for _, lbl := range listResp.Labels { + _, ok := builtinsMap[lbl.Name] + assert.True(t, ok) assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType) + idsByName[lbl.Name] = lbl.ID } // labels summary, only the built-ins remains s.DoJSON("GET", "/api/latest/fleet/labels/summary", nil, http.StatusOK, &summaryResp) assert.Len(t, summaryResp.Labels, builtInsCount) for _, lbl := range summaryResp.Labels { + _, ok := builtinsMap[lbl.Name] + assert.True(t, ok) assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType) + assert.Equal(t, idsByName[lbl.Name], lbl.ID) } // host summary matches built-ins count @@ -3750,7 +3773,22 @@ func (s *integrationTestSuite) TestLabels() { s.DoJSON("GET", "/api/latest/fleet/host_summary", nil, http.StatusOK, &hostSummaryResp) assert.Len(t, hostSummaryResp.BuiltinLabels, builtInsCount) for _, lbl := range hostSummaryResp.BuiltinLabels { + _, ok := builtinsMap[lbl.Name] + assert.True(t, ok) assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType) + assert.Equal(t, idsByName[lbl.Name], lbl.ID) + } + + require.Len(t, idsByName, len(builtinsMap)) + for name := range builtinsMap { + id, ok := idsByName[name] + require.True(t, ok) + + // attempt to delete by name + s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/labels/%s", url.PathEscape(name)), nil, http.StatusUnprocessableEntity, &delResp) + + // attempt to delete by id + s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/labels/id/%d", id), nil, http.StatusUnprocessableEntity, &delIDResp) } } @@ -3923,6 +3961,33 @@ func (s *integrationTestSuite) TestLabelSpecs() { }, }, http.StatusInternalServerError, &applyResp) + // apply an invalid label spec - builtin label type + s.DoJSON("POST", "/api/latest/fleet/spec/labels", applyLabelSpecsRequest{ + Specs: []*fleet.LabelSpec{ + { + Name: name, + Query: "select 1", + Platform: "linux", + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + LabelType: fleet.LabelTypeBuiltIn, + }, + }, + }, http.StatusUnprocessableEntity, &applyResp) + + // apply an invalid label spec - builtin label name + for n := range fleet.ReservedLabelNames() { + s.DoJSON("POST", "/api/latest/fleet/spec/labels", applyLabelSpecsRequest{ + Specs: []*fleet.LabelSpec{ + { + Name: n, + Query: "select 1", + Platform: "linux", + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + }, + }, + }, http.StatusUnprocessableEntity, &applyResp) + } + // apply a valid label spec s.DoJSON("POST", "/api/latest/fleet/spec/labels", applyLabelSpecsRequest{ Specs: []*fleet.LabelSpec{ @@ -6477,16 +6542,20 @@ func (s *integrationTestSuite) TestSearchTargets() { hosts := s.createHosts(t) - lblMap, err := s.ds.LabelIDsByName(context.Background(), []string{"All Hosts"}) + var builtinNames []string + for name := range fleet.ReservedLabelNames() { + builtinNames = append(builtinNames, name) + } + lblMap, err := s.ds.LabelIDsByName(context.Background(), builtinNames) require.NoError(t, err) - require.Len(t, lblMap, 1) + require.Len(t, lblMap, len(builtinNames)) // no search criteria var searchResp searchTargetsResponse s.DoJSON("POST", "/api/latest/fleet/targets", searchTargetsRequest{}, http.StatusOK, &searchResp) require.Equal(t, uint(0), searchResp.TargetsCount) require.Len(t, searchResp.Targets.Hosts, len(hosts)) // the HostTargets.HostIDs are actually host IDs to *omit* from the search - require.Len(t, searchResp.Targets.Labels, 1) + require.Len(t, searchResp.Targets.Labels, len(lblMap)) require.Len(t, searchResp.Targets.Teams, 0) var lblIDs []uint @@ -6505,7 +6574,7 @@ func (s *integrationTestSuite) TestSearchTargets() { s.DoJSON("POST", "/api/latest/fleet/targets", searchTargetsRequest{Selected: fleet.HostTargets{HostIDs: []uint{hosts[1].ID}}}, http.StatusOK, &searchResp) require.Equal(t, uint(1), searchResp.TargetsCount) require.Len(t, searchResp.Targets.Hosts, len(hosts)-1) // one omitted host id - require.Len(t, searchResp.Targets.Labels, 1) // labels have not been omitted + require.Len(t, searchResp.Targets.Labels, len(lblMap)) // labels have not been omitted require.Len(t, searchResp.Targets.Teams, 0) searchResp = searchTargetsResponse{} diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index c160444b1a..1c428b66bc 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -6272,6 +6272,30 @@ func (s *integrationMDMTestSuite) assertConfigProfilesByIdentifier(teamID *uint, return profile } +func (s *integrationMDMTestSuite) assertMacOSConfigProfilesByName(teamID *uint, profileName string, exists bool) { + t := s.T() + if teamID == nil { + teamID = ptr.Uint(0) + } + var cfgProfs []*fleet.MDMAppleConfigProfile + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + return sqlx.SelectContext(context.Background(), q, &cfgProfs, `SELECT name FROM mdm_apple_configuration_profiles WHERE team_id = ?`, teamID) + }) + + label := "exist" + if !exists { + label = "not exist" + } + require.Condition(t, func() bool { + for _, p := range cfgProfs { + if p.Name == profileName { + return exists // success if we want it to exist, failure if we don't + } + } + return !exists + }, "a config profile must %s with name: %s", label, profileName) +} + func (s *integrationMDMTestSuite) assertWindowsConfigProfilesByName(teamID *uint, profileName string, exists bool) { t := s.T() if teamID == nil { @@ -12271,3 +12295,159 @@ func (s *integrationMDMTestSuite) TestIsServerBitlockerStatus() { require.NotNil(t, hr.Host.MDM.OSSettings.DiskEncryption.Status) require.Equal(t, fleet.DiskEncryptionEnforcing, *hr.Host.MDM.OSSettings.DiskEncryption.Status) } + +func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { + t := s.T() + ctx := context.Background() + + checkMacProfs := func(teamID *uint, names ...string) { + var count int + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var tid uint + if teamID != nil { + tid = *teamID + } + return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_apple_configuration_profiles WHERE team_id = ?`, tid) + }) + require.Equal(t, len(names), count) + for _, n := range names { + s.assertMacOSConfigProfilesByName(teamID, n, true) + } + } + + checkWinProfs := func(teamID *uint, names ...string) { + var count int + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var tid uint + if teamID != nil { + tid = *teamID + } + return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_windows_configuration_profiles WHERE team_id = ?`, tid) + }) + for _, n := range names { + s.assertWindowsConfigProfilesByName(teamID, n, true) + } + } + + acResp := appConfigResponse{} + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + require.True(t, acResp.MDM.EnabledAndConfigured) + require.True(t, acResp.MDM.WindowsEnabledAndConfigured) + + // ensures that the fleetd profile is created + secrets, err := s.ds.GetEnrollSecrets(ctx, nil) + require.NoError(t, err) + if len(secrets) == 0 { + require.NoError(t, s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})) + } + require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)) + + // turn on disk encryption and os updates + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "enable_disk_encryption": true, + "windows_updates": { + "deadline_days": 3, + "grace_period_days": 1 + }, + "macos_updates": { + "deadline": "2023-12-31", + "minimum_version": "13.3.7" + } + } + }`), http.StatusOK, &acResp) + checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...) + checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...) + + // batch set only windows profiles doesn't remove the reserved names + newWinProfile := syncml.ForTestWithData(map[string]string{"l1": "d1"}) + var testProfiles []fleet.MDMProfileBatchPayload + testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{ + Name: "n1", + Contents: newWinProfile, + }) + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent) + checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...) + checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) + + // batch set windows and mac profiles doesn't remove the reserved names + newMacProfile := mcBytesForTest("n2", "i2", uuid.NewString()) + testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{ + Name: "n2", + Contents: newMacProfile, + }) + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent) + checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) + + // batch set only mac profiles doesn't remove the reserved names + testProfiles = []fleet.MDMProfileBatchPayload{{ + Name: "n2", + Contents: newMacProfile, + }} + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent) + checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...) + + // create a team + var tmResp teamResponse + s.DoJSON("POST", "/api/v1/fleet/teams", map[string]string{"Name": t.Name()}, http.StatusOK, &tmResp) + + // edit team mdm config to turn on disk encryption and os updates + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tmResp.Team.ID), modifyTeamRequest{ + TeamPayload: fleet.TeamPayload{ + Name: ptr.String(t.Name()), + MDM: &fleet.TeamPayloadMDM{ + EnableDiskEncryption: optjson.SetBool(true), + WindowsUpdates: &fleet.WindowsUpdates{ + DeadlineDays: optjson.SetInt(4), + GracePeriodDays: optjson.SetInt(1), + }, + MacOSUpdates: &fleet.MacOSUpdates{ + Deadline: optjson.SetString("2023-12-31"), + MinimumVersion: optjson.SetString("13.3.8"), + }, + }, + }, + }, http.StatusOK, &teamResponse{}) + + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d", tmResp.Team.ID), nil, http.StatusOK, &tmResp) + require.True(t, tmResp.Team.Config.MDM.EnableDiskEncryption) + require.Equal(t, 4, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value) + require.Equal(t, 1, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value) + require.Equal(t, "2023-12-31", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value) + require.Equal(t, "13.3.8", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value) + + require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)) + + checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...) + checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...) + + // batch set only windows profiles doesn't remove the reserved names + var testTeamProfiles []fleet.MDMProfileBatchPayload + testTeamProfiles = append(testTeamProfiles, fleet.MDMProfileBatchPayload{ + Name: "n1", + Contents: newWinProfile, + }) + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) + checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...) + checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) + + // batch set windows and mac profiles doesn't remove the reserved names + testTeamProfiles = append(testTeamProfiles, fleet.MDMProfileBatchPayload{ + Name: "n2", + Contents: newMacProfile, + }) + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) + checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) + + // batch set only mac profiles doesn't remove the reserved names + testTeamProfiles = []fleet.MDMProfileBatchPayload{{ + Name: "n2", + Contents: newMacProfile, + }} + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) + checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...) +} diff --git a/server/service/labels.go b/server/service/labels.go index 799b17fae5..6b15ec0787 100644 --- a/server/service/labels.go +++ b/server/service/labels.go @@ -2,6 +2,8 @@ package service import ( "context" + "fmt" + "net/http" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/contexts/viewer" @@ -64,6 +66,12 @@ func (svc *Service) NewLabel(ctx context.Context, p fleet.LabelPayload) (*fleet. label.Description = *p.Description } + for name := range fleet.ReservedLabelNames() { + if label.Name == name { + return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("cannot add label '%s' because it conflicts with the name of a built-in label", name)) + } + } + label, err := svc.ds.NewLabel(ctx, label) if err != nil { return nil, err @@ -111,7 +119,16 @@ func (svc *Service) ModifyLabel(ctx context.Context, id uint, payload fleet.Modi if err != nil { return nil, err } + if label.LabelType == fleet.LabelTypeBuiltIn { + return nil, fleet.NewInvalidArgumentError("label_type", fmt.Sprintf("cannot modify built-in label '%s'", label.Name)) + } if payload.Name != nil { + // Check if the new name is a reserved label name + for name := range fleet.ReservedLabelNames() { + if *payload.Name == name { + return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("cannot rename label to '%s' because it conflicts with the name of a built-in label", name)) + } + } label.Name = *payload.Name } if payload.Description != nil { @@ -319,6 +336,13 @@ func (svc *Service) DeleteLabel(ctx context.Context, name string) error { return err } + // check if the label is a built-in label + for n := range fleet.ReservedLabelNames() { + if n == name { + return fleet.NewInvalidArgumentError("name", fmt.Sprintf("cannot delete built-in label '%s'", name)) + } + } + return svc.ds.DeleteLabel(ctx, name) } @@ -354,6 +378,15 @@ func (svc *Service) DeleteLabelByID(ctx context.Context, id uint) error { if err != nil { return err } + if label.LabelType == fleet.LabelTypeBuiltIn { + return fleet.NewInvalidArgumentError("label_type", fmt.Sprintf("cannot delete built-in label '%s'", label.Name)) + } + for name := range fleet.ReservedLabelNames() { + if label.Name == name { + return fleet.NewInvalidArgumentError("name", fmt.Sprintf("cannot delete built-in label '%s'", label.Name)) + } + } + return svc.ds.DeleteLabel(ctx, label.Name) } @@ -393,6 +426,14 @@ func (svc *Service) ApplyLabelSpecs(ctx context.Context, specs []*fleet.LabelSpe // Hosts list doesn't need to contain anything, but it should at least not be nil. return ctxerr.Errorf(ctx, "label %s is declared as manual but contains no `hosts key`", spec.Name) } + if spec.LabelType == fleet.LabelTypeBuiltIn { + return fleet.NewUserMessageError(ctxerr.Errorf(ctx, "cannot modify built-in label '%s'", spec.Name), http.StatusUnprocessableEntity) + } + for name := range fleet.ReservedLabelNames() { + if spec.Name == name { + return fleet.NewUserMessageError(ctxerr.Errorf(ctx, "cannot modify built-in label '%s'", name), http.StatusUnprocessableEntity) + } + } } return svc.ds.ApplyLabelSpecs(ctx, specs) } diff --git a/server/service/testing_client.go b/server/service/testing_client.go index 5b67ada9ee..493d35bccb 100644 --- a/server/service/testing_client.go +++ b/server/service/testing_client.go @@ -40,7 +40,7 @@ type withDS struct { func (ts *withDS) SetupSuite(dbName string) { t := ts.s.T() ts.ds = mysql.CreateNamedMySQLDS(t, dbName) - test.AddAllHostsLabel(t, ts.ds) + test.AddBuiltinLabels(t, ts.ds) // setup the required fields on AppConfig appConf, err := ts.ds.AppConfig(context.Background()) diff --git a/server/test/new_objects.go b/server/test/new_objects.go index 8db4f56858..29963e0773 100644 --- a/server/test/new_objects.go +++ b/server/test/new_objects.go @@ -106,6 +106,74 @@ func AddAllHostsLabel(t *testing.T, ds fleet.Datastore) { require.NoError(t, err) } +func AddBuiltinLabels(t *testing.T, ds fleet.Datastore) { + builtins := []*fleet.Label{ + { + Name: "All Hosts", + Query: "select 1", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeManual, + }, + { + Name: "macOS", + Query: "select 1 from os_version where platform = 'darwin';", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + }, + { + Name: "Ubuntu Linux", + Query: "select 1 from os_version where platform = 'ubuntu';", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + }, + { + Name: "CentOS Linux", + Query: "select 1 from os_version where platform = 'centos' or name like '%centos%';", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + }, + { + Name: "MS Windows", + Query: "select 1 from os_version where platform = 'windows';", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + }, + { + Name: "Red Hat Linux", + Query: "SELECT 1 FROM os_version WHERE name LIKE '%red hat%'", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + }, + { + Name: "All Linux", + Query: "SELECT 1 FROM osquery_info WHERE build_platform LIKE '%ubuntu%' OR build_distro LIKE '%centos%';", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + }, + { + Name: "chrome", + Query: "select 1 from os_version where platform = 'chrome';", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + }, + } + + names := fleet.ReservedLabelNames() + require.Equal(t, len(builtins), len(names)) + storedByName := map[string]*fleet.Label{} + for _, b := range builtins { + stored, err := ds.NewLabel(context.Background(), b) + require.NoError(t, err) + storedByName[stored.Name] = stored + } + require.Len(t, storedByName, len(builtins)) + + for name := range names { + _, ok := storedByName[name] + require.True(t, ok, "expected label %s to be created", name) + } +} + // NewHostOption is an Option for the NewHost function. type NewHostOption func(*fleet.Host) From 455cb861e1b7ede9386ac3d0f06e78b4f78b8ed5 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 8 Apr 2024 15:00:39 -0500 Subject: [PATCH 12/83] Website: update IDs and `currentSection` values of landing pages (#18131) Closes: #18072 Changes: - Updated the IDs of landing pages to prevent auto-generated IDs of Markdown headings having the same ID as pages. - Added a `currentSection` value to the /endpoint-ops and /vulnerability-management pages. --- website/assets/js/pages/device-management.page.js | 2 +- website/assets/js/pages/endpoint-ops.page.js | 2 +- website/assets/js/pages/vulnerability-management.page.js | 2 +- website/assets/styles/pages/device-management.less | 2 +- website/assets/styles/pages/endpoint-ops.less | 2 +- website/assets/styles/pages/vulnerability-management.less | 2 +- website/config/routes.js | 2 ++ website/views/pages/device-management.ejs | 2 +- website/views/pages/endpoint-ops.ejs | 2 +- website/views/pages/vulnerability-management.ejs | 2 +- 10 files changed, 11 insertions(+), 9 deletions(-) diff --git a/website/assets/js/pages/device-management.page.js b/website/assets/js/pages/device-management.page.js index 76633cef05..be249f1a8f 100644 --- a/website/assets/js/pages/device-management.page.js +++ b/website/assets/js/pages/device-management.page.js @@ -1,4 +1,4 @@ -parasails.registerPage('device-management', { +parasails.registerPage('device-management-page', { // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ diff --git a/website/assets/js/pages/endpoint-ops.page.js b/website/assets/js/pages/endpoint-ops.page.js index 2a97714019..f79d51d830 100644 --- a/website/assets/js/pages/endpoint-ops.page.js +++ b/website/assets/js/pages/endpoint-ops.page.js @@ -1,4 +1,4 @@ -parasails.registerPage('endpoint-ops', { +parasails.registerPage('endpoint-ops-page', { // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ diff --git a/website/assets/js/pages/vulnerability-management.page.js b/website/assets/js/pages/vulnerability-management.page.js index a4e9cf678a..29c8bfc12f 100644 --- a/website/assets/js/pages/vulnerability-management.page.js +++ b/website/assets/js/pages/vulnerability-management.page.js @@ -1,4 +1,4 @@ -parasails.registerPage('vulnerability-management', { +parasails.registerPage('vulnerability-management-page', { // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ diff --git a/website/assets/styles/pages/device-management.less b/website/assets/styles/pages/device-management.less index 6f85892181..d3a9cb6339 100644 --- a/website/assets/styles/pages/device-management.less +++ b/website/assets/styles/pages/device-management.less @@ -1,4 +1,4 @@ -#device-management { +#device-management-page { @heading-lineheight: 120%; @text-lineheight: 150%; diff --git a/website/assets/styles/pages/endpoint-ops.less b/website/assets/styles/pages/endpoint-ops.less index c39657dad9..09aa410aa4 100644 --- a/website/assets/styles/pages/endpoint-ops.less +++ b/website/assets/styles/pages/endpoint-ops.less @@ -1,4 +1,4 @@ -#endpoint-ops { +#endpoint-ops-page { @heading-line-height: 120%; @text-line-height: 150%; diff --git a/website/assets/styles/pages/vulnerability-management.less b/website/assets/styles/pages/vulnerability-management.less index c0757a512e..e38708df49 100644 --- a/website/assets/styles/pages/vulnerability-management.less +++ b/website/assets/styles/pages/vulnerability-management.less @@ -1,4 +1,4 @@ -#vulnerability-management { +#vulnerability-management-page { background: linear-gradient(180deg, #E8F1F6 0%, #FFF 8.76%); h1 { font-size: 56px; diff --git a/website/config/routes.js b/website/config/routes.js index d1d490ffc7..c4d871a6cf 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -248,6 +248,7 @@ module.exports.routes = { locals: { pageTitleForMeta: 'Endpoint ops | Fleet', pageDescriptionForMeta: 'Pulse check anything, build reports, and ship data to any platform with Fleet.', + currentSection: 'platform', } }, @@ -256,6 +257,7 @@ module.exports.routes = { locals: { pageTitleForMeta: 'Vulnerability management | Fleet', pageDescriptionForMeta: 'Report CVEs, software inventory, security posture, and other risks down to the chipset of any endpoint with Fleet.', + currentSection: 'platform', } }, diff --git a/website/views/pages/device-management.ejs b/website/views/pages/device-management.ejs index b0529e7bb1..a7f6c96cf7 100644 --- a/website/views/pages/device-management.ejs +++ b/website/views/pages/device-management.ejs @@ -1,4 +1,4 @@ -
+
diff --git a/website/views/pages/endpoint-ops.ejs b/website/views/pages/endpoint-ops.ejs index 875048fb54..9576f3d175 100644 --- a/website/views/pages/endpoint-ops.ejs +++ b/website/views/pages/endpoint-ops.ejs @@ -1,4 +1,4 @@ -
+
diff --git a/website/views/pages/vulnerability-management.ejs b/website/views/pages/vulnerability-management.ejs index b60dee57ec..bab556462f 100644 --- a/website/views/pages/vulnerability-management.ejs +++ b/website/views/pages/vulnerability-management.ejs @@ -1,4 +1,4 @@ -
+
From b348243634870976c6d73a19d6724ecabecf5bae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 03:51:45 -0300 Subject: [PATCH 13/83] Update versions of fleetd components in Fleet's TUF [automated] (#18139) Automated change from [GitHub action](https://github.com/fleetdm/fleet/actions/workflows/fleetd-tuf.yml). Co-authored-by: lucasmrod --- orbit/TUF.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/orbit/TUF.md b/orbit/TUF.md index fc164d0d15..4e4c48fec4 100644 --- a/orbit/TUF.md +++ b/orbit/TUF.md @@ -17,8 +17,8 @@ Following are the currently deployed versions of fleetd components on the `stabl | Component\OS | macOS | Linux | Windows | |--------------|--------|--------|---------| -| orbit | 1.22.0 | 1.22.0 | 1.22.0 | -| desktop | 1.22.0 | 1.22.0 | 1.22.0 | +| orbit | 1.23.0 | 1.23.0 | 1.23.0 | +| desktop | 1.23.0 | 1.23.0 | 1.23.0 | | osqueryd | 5.12.1 | 5.12.1 | 5.12.1 | | nudge | - | - | - | | swiftDialog | - | - | - | From 3e31fbc37318966dca7cca055077bc376327ee61 Mon Sep 17 00:00:00 2001 From: Jason Lewis <57552211+Patagonia121@users.noreply.github.com> Date: Tue, 9 Apr 2024 06:37:50 -0700 Subject: [PATCH 14/83] Update open-positions.yml (#18125) fixed error in "MYSql" and changed to proper format: "MySQL" # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/` or `orbit/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. - [ ] 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)). --- handbook/company/open-positions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handbook/company/open-positions.yml b/handbook/company/open-positions.yml index fcc3a2e43b..c10b8858be 100644 --- a/handbook/company/open-positions.yml +++ b/handbook/company/open-positions.yml @@ -87,6 +87,6 @@ - ✍️ Familiarity with shell scripting, Python, Powershell, and using Terminal to execute commands or run scripts, and other line of business applications. - 🟣 Openness: Speak freely. Interrupt and be interrupted. Give pointed and respectful feedback, even when you disagree. - 🔴 Empathy: You should demonstrate empathy by keenly understanding and addressing customer concerns with genuine compassion. - - ➕ Bonus: Familiarity with osquery, MYSql, GitOps workflows, Terraform, Tines/Torq and open source projects. Experience working with IT, SRE, CPE, or SecOps teams. + - ➕ Bonus: Familiarity with osquery, MySQL, GitOps workflows, Terraform, Tines/Torq and open source projects. Experience working with IT, SRE, CPE, or SecOps teams. From 08e72db93a12c70ae12c17553eefda3e9f340efd Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Tue, 9 Apr 2024 08:38:26 -0500 Subject: [PATCH 15/83] Update README.md (#18136) --- handbook/customer-success/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/handbook/customer-success/README.md b/handbook/customer-success/README.md index 9f8a266164..2f96814de7 100644 --- a/handbook/customer-success/README.md +++ b/handbook/customer-success/README.md @@ -18,6 +18,7 @@ This handbook page details processes specific to working [with](#contact-us) and ## Responsibilities The customer success department is directly responsible for ensuring that customers and community members of Fleet achieve their desired outcomes with Fleet products and services. + ### Assign a customer codename Occasionally, we will need to track public issues for customers who wish to remain anonymous on our public issue tracker. To do this, we choose an appropriate minor planet name from this [Wikipedia page](https://en.wikipedia.org/wiki/List_of_named_minor_planets_(alphabetical)) and create a label which we attach to the issue and any future issues for this customer. @@ -26,7 +27,7 @@ Locate the relevant issue or create it if it doesn't already exist (to avoid dup - Make sure the issue has a "customer request" label or "customer-codename" label. - Occasionally, we will need to track public issues for customers that wish to remain anonymous on our public issue tracker. To do this, we choose an appropriate minor planet name from this [Wikipedia page](https://en.wikipedia.org/wiki/List_of_named_minor_planets_(alphabetical)) and create a label which we attach to the issue and any future issues for this customer. - "+" prefixed labels (e.g., "+more info please") indicate we are waiting on an answer from an external community member who does not work at Fleet or that no further action is needed from the Fleet team until an external community member, who doesn't work at Fleet, replies with a comment. At this point, our bot will automatically remove the +-prefixed label. -- 1. Required details that will help speed up time to resolution: +1. Required details that will help speed up time to resolution: - Fleet server version - Agent version - Osquery or fleetd? From 65ab7a896d47e0635e6e06c97c4833b5a9f89e4f Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:47:47 -0400 Subject: [PATCH 16/83] Remove duplicate DEP profile example (#18114) - Remove duplicate - Add redirect so the example profile can move later w/o breaking links - Update links --- docs/Using Fleet/MDM-macOS-setup-experience.md | 2 +- tools/mdm/apple/dep_sample_profile.json | 18 ------------------ tools/mdm/apple/glossary-and-protocols.md | 3 ++- website/config/routes.js | 1 + 4 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 tools/mdm/apple/dep_sample_profile.json diff --git a/docs/Using Fleet/MDM-macOS-setup-experience.md b/docs/Using Fleet/MDM-macOS-setup-experience.md index 8ed4d055fb..d440667761 100644 --- a/docs/Using Fleet/MDM-macOS-setup-experience.md +++ b/docs/Using Fleet/MDM-macOS-setup-experience.md @@ -273,7 +273,7 @@ To customize the macOS Setup Assistant, we will do the following steps: ### Step 1: create an automatic enrollment profile -1. Download Fleet's example automatic enrollment profile by navigating to the example [here on GitHub](https://github.com/fleetdm/fleet/blob/main/it-and-security/lib/automatic-enrollment.dep.json) and clicking the download icon. +1. Download Fleet's example automatic enrollment profile by navigating to the example [here](fleetdm.com/example-dep-profile) and clicking the download icon. 2. Open the automatic enrollment profile and replace the `profile_name` key with your organization's name. diff --git a/tools/mdm/apple/dep_sample_profile.json b/tools/mdm/apple/dep_sample_profile.json deleted file mode 100644 index af76650f54..0000000000 --- a/tools/mdm/apple/dep_sample_profile.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "profile_name": "Fleet Device Management Inc.", - "allow_pairing": true, - "auto_advance_setup": false, - "department": "it@fleetdm.com", - "is_supervised": false, - "is_multi_user": false, - "is_mandatory": false, - "is_mdm_removable": true, - "language": "en", - "org_magic": "1", - "region": "US", - "support_phone_number": "+1 408 555 1010", - "support_email_address": "support@fleetdm.com", - "anchor_certs": [], - "supervising_host_certs": [], - "skip_setup_items": ["Accessibility", "Appearance", "AppleID", "AppStore", "Biometric", "Diagnostics", "FileVault", "iCloudDiagnostics", "iCloudStorage", "Location", "Payment", "Privacy", "Restore", "ScreenTime", "Siri", "TermsOfAddress", "TOS", "UnlockWithWatch"] -} diff --git a/tools/mdm/apple/glossary-and-protocols.md b/tools/mdm/apple/glossary-and-protocols.md index b4324a05a7..c0dcf8278b 100644 --- a/tools/mdm/apple/glossary-and-protocols.md +++ b/tools/mdm/apple/glossary-and-protocols.md @@ -80,7 +80,8 @@ For [DEP enrollment](#dep-device-enrollment-program) the enrollment profile is d This (JSON) profile is used to configure a device in Apple Business Manager. It contains all the necessary information that a device needs to automatically enroll to an MDM server during device setup. -Sample: [dep_sample_profile.json](https://github.com/fleetdm/nanodep/blob/main/docs/dep-profile.example.json). +[Example](https://fleetdm.com/example-dep-profile) + See all fields [here](https://developer.apple.com/documentation/devicemanagement/profile). ### Commands diff --git a/website/config/routes.js b/website/config/routes.js index c4d871a6cf..4646289e21 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -466,6 +466,7 @@ module.exports.routes = { 'GET /try-fleet/waitlist': '/try-fleet', 'GET /mdm': '/device-management',// « alias for radio ad 'GET /endpoint-operations': '/endpoint-ops',// « just in case we type it the wrong way + 'GET /example-dep-profile': 'https://github.com/fleetdm/fleet/blob/main/it-and-security/lib/automatic-enrollment.dep.json', // Fleet UI // ============================================================================================================= From f827a4727e9f16f1dc850b43513b1547f3b2a0d2 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 9 Apr 2024 11:15:22 -0300 Subject: [PATCH 17/83] use license.IsPremium to check for premium licenses (#18141) a license can be considered premium for trials, this replaces occurrences that used comparison operators to check for premium licences. --- changes/license-comparison | 1 + server/service/appconfig.go | 2 +- server/service/apple_mdm.go | 5 +---- 3 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 changes/license-comparison diff --git a/changes/license-comparison b/changes/license-comparison new file mode 100644 index 0000000000..e17ede70fc --- /dev/null +++ b/changes/license-comparison @@ -0,0 +1 @@ +* Fixed license checks to allow migration and restoring DEP devices during trial diff --git a/server/service/appconfig.go b/server/service/appconfig.go index b2457d772d..d54e0882c1 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -821,7 +821,7 @@ func (svc *Service) validateMDM( // TODO: Should we validate MDM configured on here too? if mdm.MacOSMigration.Enable { - if license.Tier != fleet.TierPremium { + if !license.IsPremium() { invalid.Append("macos_migration.enable", ErrMissingLicense.Error()) return nil } diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index ddbd5e1e4a..2d6e847f6a 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -3053,10 +3053,7 @@ func (svc *Service) maybeRestorePendingDEPHost(ctx context.Context, host *fleet. return nil } - license, ok := license.FromContext(ctx) - if !ok { - return ctxerr.New(ctx, "maybe restore pending DEP host: missing license") - } else if license.Tier != fleet.TierPremium { + if !license.IsPremium(ctx) { // only premium tier supports DEP so nothing more to do return nil } From 36a75a0b64d2ae6df315a18e749e625a28745f39 Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:27:35 -0500 Subject: [PATCH 18/83] Update README.md (#18143) --- handbook/business-operations/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/handbook/business-operations/README.md b/handbook/business-operations/README.md index 4fc8c9bc5a..112388e5ab 100644 --- a/handbook/business-operations/README.md +++ b/handbook/business-operations/README.md @@ -129,6 +129,7 @@ Certain new team members, especially in go-to-market (GTM) roles, will need paid This reporting is performed to update the status of open or upcoming customer actions regarding the financial health of the opportunity. To complete the report: - Go to this [report folder](https://fleetdm.lightning.force.com/lightning/r/Folder/00lUG000000DstpYAC/view?queryScope=userFolders) in SFDC. The three reports will provide the data used in the report. - Copy the template below and paste it into the [#g-sales slack channel](https://fleetdm.slack.com/archives/C030A767HQV) and complete all "todos" using the data from Salesforce before sending. + ``` Weekly revenue report - [@`todo: CRO` and @`todo: CEO`] - Number accounts with outstanding balances = `todo` From af7f0fa89e4cf730864a4b4dcade9f7db8d01307 Mon Sep 17 00:00:00 2001 From: George Karr Date: Tue, 9 Apr 2024 11:41:41 -0500 Subject: [PATCH 19/83] Adding changes for patch 4.48.1 (#18129) --- CHANGELOG.md | 8 ++++++++ charts/fleet/Chart.yaml | 2 +- charts/fleet/values.yaml | 2 +- infrastructure/dogfood/terraform/aws/variables.tf | 2 +- infrastructure/dogfood/terraform/gcp/variables.tf | 2 +- terraform/byo-vpc/README.md | 2 +- terraform/byo-vpc/byo-db/byo-ecs/variables.tf | 2 +- terraform/byo-vpc/byo-db/variables.tf | 2 +- terraform/byo-vpc/example/main.tf | 2 +- terraform/byo-vpc/variables.tf | 2 +- terraform/example/main.tf | 4 ++-- terraform/variables.tf | 2 +- tools/fleetctl-npm/package.json | 2 +- 13 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d18839ae8..bad9917bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Fleet 4.48.1 (Apr 08, 2024) + +### Bug fixes + +- Made block_id mismatch errors more informative as 400s instead of 500s +- Fixed a bug where values were not being rendered in host-specific query reports +- Fixed potential server panic when events are created with calendar integration, but then global calendar integration is disabled + ## Fleet 4.48.0 (Apr 03, 2024) ### Endpoint operations diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index 792846d9bb..ca14828253 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.48.0 +appVersion: v4.48.1 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index f553a50326..13eaabda25 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.48.0 # Version of Fleet to deploy +imageTag: v4.48.1 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf index cb0d1593de..8b800b268f 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.48.0" + default = "fleetdm/fleet:v4.48.1" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index afe4e46243..3a01f75209 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.48.0" + default = "fleet:v4.48.1" } diff --git a/terraform/byo-vpc/README.md b/terraform/byo-vpc/README.md index 0cc44fdec0..2ef3b21c3c 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.48.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)
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
},
"redis": {
"address": null,
"use_tls": true
},
"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.48.1")
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)
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
},
"redis": {
"address": null,
"use_tls": true
},
"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.02.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.02.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/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf index 20b8249bff..d62b9deb61 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.48.0") + image = optional(string, "fleetdm/fleet:v4.48.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index 88cb0c03fb..d8dec47a9b 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.48.0") + image = optional(string, "fleetdm/fleet:v4.48.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf index 28311ecee1..ed30ae8608 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.48.0" + fleet_image = "fleetdm/fleet:v4.48.1" domain_name = "example.com" } diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index 4ad249b0b0..cbf4b49df9 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.48.0") + image = optional(string, "fleetdm/fleet:v4.48.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/example/main.tf b/terraform/example/main.tf index 04b2691900..c503048375 100644 --- a/terraform/example/main.tf +++ b/terraform/example/main.tf @@ -59,8 +59,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.48.0" - image = "fleetdm/fleet:v4.48.0" # override default to deploy the image you desire + # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.48.1" + image = "fleetdm/fleet:v4.48.1" # override default to deploy the image you desire # See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling # memory and cpu. autoscaling = { diff --git a/terraform/variables.tf b/terraform/variables.tf index a5cb956185..347d339ee1 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.48.0") + image = optional(string, "fleetdm/fleet:v4.48.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index 80a5a278cb..f14ddce042 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.48.0", + "version": "v4.48.1", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" From 98a41914ab8586b9e4f23e780f490ee0f5615272 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 9 Apr 2024 11:58:05 -0500 Subject: [PATCH 20/83] Website: upgrade dependencies (#18145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to: #18048 Changes: Upgraded website dependencies: - `@sailshq/lodash`: `^3.10.3` » `^3.10.6` - `jsonwebtoken`: `9.0.0` » `9.0.2` - `sails`: `^1.5.8` » `^1.5.9` - `sails-hook-apianalytics`: `^2.0.5` » `^2.0.6` - `sails-hook-orm`: `^4.0.2` » `^4.0.3` - `sails-postgresql`: `^5.0.0` » `^5.0.1` - `sails-hook-grunt`: `^4.0.0` » `^5.0.0` --- website/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/website/package.json b/website/package.json index e050e3271f..9c3601ae69 100644 --- a/website/package.json +++ b/website/package.json @@ -6,16 +6,16 @@ "keywords": [], "dependencies": { "@sailshq/connect-redis": "^6.1.3", - "@sailshq/lodash": "^3.10.3", + "@sailshq/lodash": "^3.10.5", "@sailshq/socket.io-redis": "^6.1.2", - "jsonwebtoken": "9.0.0", + "jsonwebtoken": "9.0.2", "moment": "2.29.4", - "sails": "^1.5.7", - "sails-hook-apianalytics": "^2.0.5", + "sails": "^1.5.9", + "sails-hook-apianalytics": "^2.0.6", "sails-hook-organics": "^2.2.2", - "sails-hook-orm": "^4.0.2", + "sails-hook-orm": "^4.0.3", "sails-hook-sockets": "^3.0.0", - "sails-postgresql": "^5.0.0" + "sails-postgresql": "^5.0.1" }, "devDependencies": { "eslint": "5.16.0", @@ -23,7 +23,7 @@ "htmlhint": "0.11.0", "lesshint": "6.3.6", "marked": "4.0.10", - "sails-hook-grunt": "^4.0.0", + "sails-hook-grunt": "^5.0.0", "yaml": "1.10.2" }, "scripts": { From c263923eab77ef17fb97eabcab0ef706810d5750 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 9 Apr 2024 12:25:14 -0400 Subject: [PATCH 21/83] Explicitly set collation for character columns --- .github/pull_request_template.md | 3 ++- .../tables/20240327115617_CreateTableNanoDDMRequests.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index feb3159edb..573d3b004a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,10 +9,11 @@ If some of the following don't apply, delete the relevant line. - [ ] 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 +- [ ] 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. diff --git a/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go b/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go index 2aa6de300b..e5595d7842 100644 --- a/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go +++ b/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go @@ -14,9 +14,9 @@ func Up_20240327115617(tx *sql.Tx) error { CREATE TABLE mdm_apple_declarative_requests ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - enrollment_id VARCHAR(255) NOT NULL, + enrollment_id VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL, -- Should be one of "tokens", "declaration-items", "status", or "declaration/…/…" where the ellipses reference a declaration on the server - message_type VARCHAR(255) NOT NULL, + message_type VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL, -- json payload raw_json TEXT, PRIMARY KEY (id), From 5d4bc856b9c8de2addd9f0469ea3f897695092c3 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 9 Apr 2024 12:26:38 -0400 Subject: [PATCH 22/83] Add changes file --- changes/18142-fix-migration-issue-related-to-collation | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/18142-fix-migration-issue-related-to-collation diff --git a/changes/18142-fix-migration-issue-related-to-collation b/changes/18142-fix-migration-issue-related-to-collation new file mode 100644 index 0000000000..cf48ada6d8 --- /dev/null +++ b/changes/18142-fix-migration-issue-related-to-collation @@ -0,0 +1 @@ +* Fixed an issue with the `20240327115617_CreateTableNanoDDMRequests` database migration where it could fail if the database did not default to the `utf8mb4_unicode_ci` collation. From 1b35ffd0efcfbb6394d2cf2c73d2ca5475e5ef39 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Tue, 9 Apr 2024 15:15:06 -0300 Subject: [PATCH 23/83] Release fleetd 1.23.0 (#18133) --- .github/workflows/generate-desktop-targets.yml | 2 +- orbit/CHANGELOG.md | 6 ++++++ orbit/changes/16594-orbit-enroll-backoff | 1 - orbit/changes/dataflatten-tables | 1 - 4 files changed, 7 insertions(+), 3 deletions(-) delete mode 100644 orbit/changes/16594-orbit-enroll-backoff delete mode 100644 orbit/changes/dataflatten-tables diff --git a/.github/workflows/generate-desktop-targets.yml b/.github/workflows/generate-desktop-targets.yml index fef2d6075b..ce24e24173 100644 --- a/.github/workflows/generate-desktop-targets.yml +++ b/.github/workflows/generate-desktop-targets.yml @@ -24,7 +24,7 @@ defaults: shell: bash env: - FLEET_DESKTOP_VERSION: 1.22.0 + FLEET_DESKTOP_VERSION: 1.23.0 permissions: contents: read diff --git a/orbit/CHANGELOG.md b/orbit/CHANGELOG.md index b62a34e6d5..90b43dc314 100644 --- a/orbit/CHANGELOG.md +++ b/orbit/CHANGELOG.md @@ -1,3 +1,9 @@ +## Orbit 1.23.0 (Apr 08, 2024) + +* Add `parse_json`, `parse_jsonl`, `parse_xml`, and `parse_ini` tables. + +* Add exponential backoff to orbit enroll retries. + ## Orbit 1.22.0 (Feb 26, 2024) * Reduce error logs when orbit cannot connect to Fleet. diff --git a/orbit/changes/16594-orbit-enroll-backoff b/orbit/changes/16594-orbit-enroll-backoff deleted file mode 100644 index 01ef599803..0000000000 --- a/orbit/changes/16594-orbit-enroll-backoff +++ /dev/null @@ -1 +0,0 @@ -* Add exponential backoff to orbit enroll retries. diff --git a/orbit/changes/dataflatten-tables b/orbit/changes/dataflatten-tables deleted file mode 100644 index d2ec646ff4..0000000000 --- a/orbit/changes/dataflatten-tables +++ /dev/null @@ -1 +0,0 @@ -- Add `parse_json`, `parse_jsonl`, `parse_xml`, and `parse_ini` tables. From d3214fab510bde511ad34299aeecbdbe390b9049 Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Tue, 9 Apr 2024 15:01:44 -0500 Subject: [PATCH 24/83] Update primary-tagline.partial.ejs (#18140) --- website/views/partials/primary-tagline.partial.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/views/partials/primary-tagline.partial.ejs b/website/views/partials/primary-tagline.partial.ejs index debe33a902..32b47d58e3 100644 --- a/website/views/partials/primary-tagline.partial.ejs +++ b/website/views/partials/primary-tagline.partial.ejs @@ -1 +1 @@ -<%= primaryBuyingSituation === 'mdm' ? 'Your last MDM migration' : 'Focus on data, not vendors' %> +<%= primaryBuyingSituation === 'mdm' ? 'Your last MDM migration' : primaryBuyingSituation ? 'Focus on data, not vendors' : 'Untangle your endpoints' %> From 6fac9aaf7ee475ccfd5e0bb601827a97f3ea1048 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 9 Apr 2024 15:27:55 -0500 Subject: [PATCH 25/83] Website: update layout and personalization in website navigation (#18155) Changes: - Updated the link to the queries page in the website header navigation menu and docs navigation menu to say "Device health checks" for users who have a primaryBuyingSituation set to `eo-it` or `mdm`. - Updated links in the docs side nav to be relative - Replaced the snitcher script tag with an updated version from snitcher's website. --- website/views/layouts/layout.ejs | 14 +++++++------- website/views/pages/docs/basic-documentation.ejs | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/website/views/layouts/layout.ejs b/website/views/layouts/layout.ejs index 8e2c6ac3cc..a40637b66c 100644 --- a/website/views/layouts/layout.ejs +++ b/website/views/layouts/layout.ejs @@ -104,11 +104,11 @@ <% /* Snitcher analytics code */ %> <% /* HubSpot Embed Code */ %> @@ -179,7 +179,7 @@
Docs REST API - Built-in queries + <%= ['eo-it', 'mdm'].includes(primaryBuyingSituation) ? 'Device health checks' : 'Built-in queries' %> Data tables SUPPORT
@@ -239,7 +239,7 @@ - Data tables - Built-in queries + Data tables + <%= ['eo-it', 'mdm'].includes(primaryBuyingSituation) ? 'Device health checks' : 'Built-in queries' %> Releases - Support + Support
From 630b71875e17d377247a92751f0357141421984c Mon Sep 17 00:00:00 2001 From: Award Malisi <45960385+Unearthlyglow@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:35:47 -0500 Subject: [PATCH 26/83] Added Award to humans (#18156) --- website/api/controllers/webhooks/receive-from-github.js | 1 + 1 file changed, 1 insertion(+) diff --git a/website/api/controllers/webhooks/receive-from-github.js b/website/api/controllers/webhooks/receive-from-github.js index 375e685b0e..fff9b3155a 100644 --- a/website/api/controllers/webhooks/receive-from-github.js +++ b/website/api/controllers/webhooks/receive-from-github.js @@ -89,6 +89,7 @@ module.exports = { 'nonpunctual', 'hughestaylor', 'dantecatalfamo', + 'unearthlyglow', ]; let GREEN_LABEL_COLOR = 'C2E0C6';// « Used in multiple places below. (FUTURE: Use the "+" prefix for this instead of color. 2022-05-05) From 3367b7e03682e3fa3790c2419ec5ce430a58380c Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 9 Apr 2024 16:33:44 -0500 Subject: [PATCH 27/83] Added orbit_version, fleet_desktop_version, and scripts_enabled to host details. (#18123) #17361 #17148 In GET fleet/hosts/:id response, added the following fields: - orbit_version - `orbit_version == null` means this agent is not an orbit agent - fleet_desktop_version - `fleet_desktop_version == null` means this agent is not an orbit agent or it is an older version which is not collecting the desktop version - `fleet_desktop_version == ""` means this agent is an orbit agent but does not have fleet desktop - scripts_enabled - `scripts_enabled == null` means this agent is not an orbit agent or it is an older version which is not collecting scripts_enabled In orbit_info table, added the following fields: - desktop_version - scripts_enabled Updated docs for orbit_info PR: https://github.com/fleetdm/fleet/pull/18135 Updated API docs: https://github.com/fleetdm/fleet/pull/17814 MDM lock/unlock/wipe error messages are not part of this PR. They will be in a separate PR. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - [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/17362-orbit-and-desktop-version | 1 + .../expectedHostDetailResponseJson.json | 3 + .../expectedHostDetailResponseYaml.yml | 3 + .../testdata/expectedListHostsJson.json | 6 ++ .../testdata/expectedListHostsMDM.json | 6 ++ .../testdata/expectedListHostsYaml.yml | 3 + cmd/osquery-perf/agent.go | 31 ++++++++++ .../17362-desktop-version-and-scripts-enabled | 1 + orbit/cmd/desktop/desktop.go | 7 +++ orbit/cmd/orbit/orbit.go | 21 ++++++- orbit/pkg/table/orbit_info/orbit_info.go | 24 +++++++- orbit/pkg/update/notifications.go | 18 ++++-- orbit/pkg/update/runner_test.go | 4 ++ server/datastore/mysql/hosts.go | 16 ++++-- server/datastore/mysql/hosts_test.go | 28 +++++++++- .../20240408085837_NewOrbitInfoFields.go | 27 +++++++++ .../20240408085837_NewOrbitInfoFields_test.go | 56 +++++++++++++++++++ server/datastore/mysql/schema.sql | 6 +- server/datastore/mysql/statistics_test.go | 7 ++- server/fleet/datastore.go | 5 +- server/fleet/errors.go | 1 + server/fleet/hosts.go | 3 + server/mock/datastore_mock.go | 7 ++- server/service/integration_core_test.go | 18 +++--- server/service/integration_enterprise_test.go | 21 +++++++ server/service/osquery_test.go | 11 +++- server/service/osquery_utils/queries.go | 14 ++++- server/service/scripts.go | 7 +++ tools/tuf/test/README.md | 1 + 29 files changed, 324 insertions(+), 32 deletions(-) create mode 100644 changes/17362-orbit-and-desktop-version create mode 100644 orbit/changes/17362-desktop-version-and-scripts-enabled create mode 100644 server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields.go create mode 100644 server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields_test.go diff --git a/changes/17362-orbit-and-desktop-version b/changes/17362-orbit-and-desktop-version new file mode 100644 index 0000000000..c681b7644e --- /dev/null +++ b/changes/17362-orbit-and-desktop-version @@ -0,0 +1 @@ +In GET fleet/hosts/:id response, added orbit_version, fleet_desktop_version, and scripts_enabled fields. diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json index 0a05fb8b08..46f48e23d6 100644 --- a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json @@ -18,6 +18,9 @@ "uuid": "", "platform": "", "osquery_version": "", + "orbit_version": null, + "fleet_desktop_version": null, + "scripts_enabled": null, "os_version": "", "build": "", "platform_like": "", diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml index 5b1f81d4a1..52057fe4b6 100644 --- a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml @@ -15,6 +15,7 @@ spec: display_text: test_host display_name: test_host distributed_interval: 0 + fleet_desktop_version: null gigs_disk_space_available: 0 gigs_total_disk_space: 0 hardware_model: "" @@ -40,6 +41,7 @@ spec: pending_action: "" server_url: null memory: 0 + orbit_version: null os_version: "" osquery_version: "" pack_stats: null @@ -83,6 +85,7 @@ spec: primary_mac: "" refetch_requested: false refetch_critical_queries_until: null + scripts_enabled: null seen_time: "0001-01-01T00:00:00Z" software_updated_at: "0001-01-01T00:00:00Z" status: offline diff --git a/cmd/fleetctl/testdata/expectedListHostsJson.json b/cmd/fleetctl/testdata/expectedListHostsJson.json index 2d80597d4b..6280165fdf 100644 --- a/cmd/fleetctl/testdata/expectedListHostsJson.json +++ b/cmd/fleetctl/testdata/expectedListHostsJson.json @@ -18,6 +18,9 @@ "uuid": "", "platform": "", "osquery_version": "", + "orbit_version": null, + "fleet_desktop_version": null, + "scripts_enabled": null, "os_version": "", "build": "", "platform_like": "", @@ -89,6 +92,9 @@ "uuid": "", "platform": "", "osquery_version": "", + "orbit_version": null, + "fleet_desktop_version": null, + "scripts_enabled": null, "os_version": "", "build": "", "platform_like": "", diff --git a/cmd/fleetctl/testdata/expectedListHostsMDM.json b/cmd/fleetctl/testdata/expectedListHostsMDM.json index fcd180320c..de36e08df9 100644 --- a/cmd/fleetctl/testdata/expectedListHostsMDM.json +++ b/cmd/fleetctl/testdata/expectedListHostsMDM.json @@ -19,6 +19,9 @@ "uuid": "", "platform": "", "osquery_version": "", + "orbit_version": null, + "fleet_desktop_version": null, + "scripts_enabled": null, "os_version": "", "build": "", "platform_like": "", @@ -90,6 +93,9 @@ "uuid": "", "platform": "", "osquery_version": "", + "orbit_version": null, + "fleet_desktop_version": null, + "scripts_enabled": null, "os_version": "", "build": "", "platform_like": "", diff --git a/cmd/fleetctl/testdata/expectedListHostsYaml.yml b/cmd/fleetctl/testdata/expectedListHostsYaml.yml index ac733c2b83..da049feb2e 100644 --- a/cmd/fleetctl/testdata/expectedListHostsYaml.yml +++ b/cmd/fleetctl/testdata/expectedListHostsYaml.yml @@ -20,6 +20,7 @@ spec: detail_updated_at: "0001-01-01T00:00:00Z" display_text: test_host distributed_interval: 0 + fleet_desktop_version: null gigs_disk_space_available: 0 gigs_total_disk_space: 0 hardware_model: "" @@ -42,6 +43,7 @@ spec: name: "" server_url: null memory: 0 + orbit_version: null os_version: "" osquery_version: "" pack_stats: null @@ -54,6 +56,7 @@ spec: primary_mac: "" refetch_requested: false refetch_critical_queries_until: null + scripts_enabled: null seen_time: "0001-01-01T00:00:00Z" software_updated_at: "0001-01-01T00:00:00Z" status: offline diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 611bcfe702..a1aea3b1bd 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -1644,6 +1644,32 @@ func (a *agent) diskEncryptionLinux() []map[string]string { } } +func (a *agent) orbitInfo() []map[string]string { + version := "1.22.0" + desktopVersion := version + if a.disableFleetDesktop { + desktopVersion = "" + } + deviceAuthToken := "" + if a.deviceAuthToken != nil { + deviceAuthToken = *a.deviceAuthToken + } + return []map[string]string{ + { + "version": version, + "device_auth_token": deviceAuthToken, + "enrolled": "true", + "last_recorded_error": "", + "orbit_channel": "stable", + "osqueryd_channel": "stable", + "desktop_channel": "stable", + "desktop_version": desktopVersion, + "uptime": "10000", + "scripts_enabled": "1", + }, + } +} + func (a *agent) runLiveQuery(query string) (results []map[string]string, status *fleet.OsqueryStatus, message *string, stats *fleet.Stats) { if a.liveQueryFailProb > 0.0 && rand.Float64() <= a.liveQueryFailProb { ss := fleet.OsqueryStatus(1) @@ -1800,6 +1826,11 @@ func (a *agent) processQuery(name, query string) ( // the caller knows it is handled, will not try to return lorem-ipsum-style // results. return true, nil, &statusNotOK, nil, nil + case name == hostDetailQueryPrefix+"orbit_info": + if a.orbitNodeKey == nil { + return true, nil, &statusNotOK, nil, nil + } + return true, a.orbitInfo(), &statusOK, nil, nil default: // Look for results in the template file. if t := a.templates.Lookup(name); t == nil { diff --git a/orbit/changes/17362-desktop-version-and-scripts-enabled b/orbit/changes/17362-desktop-version-and-scripts-enabled new file mode 100644 index 0000000000..8c6104850f --- /dev/null +++ b/orbit/changes/17362-desktop-version-and-scripts-enabled @@ -0,0 +1 @@ +In orbit_info table, added desktop_version and scripts_enabled fields. diff --git a/orbit/cmd/desktop/desktop.go b/orbit/cmd/desktop/desktop.go index c6d84be9eb..89b8e88475 100644 --- a/orbit/cmd/desktop/desktop.go +++ b/orbit/cmd/desktop/desktop.go @@ -59,6 +59,13 @@ func setupRunners() { } func main() { + // Orbits uses --version to get the fleet-desktop version. Logs do not need to be set up when running this. + if len(os.Args) > 1 && os.Args[1] == "--version" { + // Must work with update.GetVersion + fmt.Println("fleet-desktop", version) + return + } + setupLogs() setupStderr() diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 1fd3d79ba2..87bd73a56c 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -495,6 +495,7 @@ func main() { } // Get current version of osquery + log.Info().Msgf("orbit version: %s", build.Version) osquerydPath, err = updater.ExecutableLocalPath("osqueryd") if err != nil { log.Info().Err(err).Msg("Could not find local osqueryd executable") @@ -803,7 +804,9 @@ func main() { windowsMDMBitlockerCommandFrequency = time.Hour ) configFetcher := update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(orbitClient, renewEnrollmentProfileCommandFrequency, fleetURL) - configFetcher = update.ApplyRunScriptsConfigFetcherMiddleware(configFetcher, c.Bool("enable-scripts"), orbitClient) + configFetcher, scriptsEnabledFn := update.ApplyRunScriptsConfigFetcherMiddleware( + configFetcher, c.Bool("enable-scripts"), orbitClient, + ) switch runtime.GOOS { case "darwin": @@ -1079,6 +1082,20 @@ func main() { checkerClient.GetServerCapabilities().Copy(orbitClient.GetServerCapabilities()) g.Add(capabilitiesChecker.actor()) + var desktopVersion string + if c.Bool("fleet-desktop") { + runPath := desktopPath + if runtime.GOOS == "darwin" { + runPath = filepath.Join(desktopPath, "Contents", "MacOS", constant.DesktopAppExecName) + } + desktopVersion, err = update.GetVersion(runPath) + if err == nil && desktopVersion != "" { + log.Info().Msgf("Found fleet-desktop version: %s", desktopVersion) + } else { + desktopVersion = "unknown" + } + } + registerExtensionRunner( &g, r.ExtensionSocketPath(), @@ -1087,8 +1104,10 @@ func main() { c.String("orbit-channel"), c.String("osqueryd-channel"), c.String("desktop-channel"), + desktopVersion, trw, startTime, + scriptsEnabledFn, )), ) diff --git a/orbit/pkg/table/orbit_info/orbit_info.go b/orbit/pkg/table/orbit_info/orbit_info.go index 95b426a2ec..5ac71fe278 100644 --- a/orbit/pkg/table/orbit_info/orbit_info.go +++ b/orbit/pkg/table/orbit_info/orbit_info.go @@ -19,19 +19,26 @@ type Extension struct { orbitChannel string osquerydChannel string desktopChannel string + dektopVersion string trw *token.ReadWriter + scriptsEnabled func() bool } var _ orbit_table.Extension = (*Extension)(nil) -func New(orbitClient *service.OrbitClient, orbitChannel, osquerydChannel, desktopChannel string, trw *token.ReadWriter, startTime time.Time) *Extension { +func New( + orbitClient *service.OrbitClient, orbitChannel, osquerydChannel, desktopChannel string, desktopVersion string, trw *token.ReadWriter, + startTime time.Time, scriptsEnabled func() bool, +) *Extension { return &Extension{ startTime: startTime, orbitClient: orbitClient, orbitChannel: orbitChannel, osquerydChannel: osquerydChannel, desktopChannel: desktopChannel, + dektopVersion: desktopVersion, trw: trw, + scriptsEnabled: scriptsEnabled, } } @@ -50,7 +57,9 @@ func (o Extension) Columns() []table.ColumnDefinition { table.TextColumn("orbit_channel"), table.TextColumn("osqueryd_channel"), table.TextColumn("desktop_channel"), + table.TextColumn("desktop_version"), table.BigIntColumn("uptime"), + table.IntegerColumn("scripts_enabled"), } } @@ -73,6 +82,17 @@ func (o Extension) GenerateFunc(_ context.Context, _ table.QueryContext) ([]map[ } } + boolToInt := func(b bool) int64 { + // Fast implementation according to https://0x0f.me/blog/golang-compiler-optimization/ + var i int64 + if b { + i = 1 + } else { + i = 0 + } + return i + } + return []map[string]string{{ "version": v, "device_auth_token": token, @@ -81,6 +101,8 @@ func (o Extension) GenerateFunc(_ context.Context, _ table.QueryContext) ([]map[ "orbit_channel": o.orbitChannel, "osqueryd_channel": o.osquerydChannel, "desktop_channel": o.desktopChannel, + "desktop_version": o.dektopVersion, "uptime": strconv.FormatInt(int64(time.Since(o.startTime).Seconds()), 10), + "scripts_enabled": strconv.FormatInt(boolToInt(o.scriptsEnabled()), 10), }}, nil } diff --git a/orbit/pkg/update/notifications.go b/orbit/pkg/update/notifications.go index 174b2d76c3..febb8cc054 100644 --- a/orbit/pkg/update/notifications.go +++ b/orbit/pkg/update/notifications.go @@ -314,7 +314,9 @@ type runScriptsConfigFetcher struct { mu sync.Mutex } -func ApplyRunScriptsConfigFetcherMiddleware(fetcher OrbitConfigFetcher, scriptsEnabled bool, scriptsClient scripts.Client) OrbitConfigFetcher { +func ApplyRunScriptsConfigFetcherMiddleware( + fetcher OrbitConfigFetcher, scriptsEnabled bool, scriptsClient scripts.Client, +) (OrbitConfigFetcher, func() bool) { scriptsFetcher := &runScriptsConfigFetcher{ Fetcher: fetcher, ScriptsExecutionEnabled: scriptsEnabled, @@ -323,7 +325,7 @@ func ApplyRunScriptsConfigFetcherMiddleware(fetcher OrbitConfigFetcher, scriptsE } // start the dynamic check for scripts enabled if required scriptsFetcher.runDynamicScriptsEnabledCheck() - return scriptsFetcher + return scriptsFetcher, scriptsFetcher.scriptsEnabled } func (h *runScriptsConfigFetcher) runDynamicScriptsEnabledCheck() { @@ -372,10 +374,7 @@ func (h *runScriptsConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) { log.Debug().Msgf("received request to run scripts %v", cfg.Notifications.PendingScriptExecutionIDs) runner := &scripts.Runner{ - // scripts are always enabled if the agent is started with the - // --scripts-enabled flag. If it is not started with this flag, then - // scripts are enabled only if the mdm profile says so. - ScriptExecutionEnabled: h.ScriptsExecutionEnabled || h.dynamicScriptsEnabled.Load(), + ScriptExecutionEnabled: h.scriptsEnabled(), Client: h.ScriptsClient, } fn := runner.Run @@ -399,6 +398,13 @@ func (h *runScriptsConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) { return cfg, err } +func (h *runScriptsConfigFetcher) scriptsEnabled() bool { + // scripts are always enabled if the agent is started with the + // --scripts-enabled flag. If it is not started with this flag, then + // scripts are enabled only if the mdm profile says so. + return h.ScriptsExecutionEnabled || h.dynamicScriptsEnabled.Load() +} + type DiskEncryptionKeySetter interface { SetOrUpdateDiskEncryptionKey(diskEncryptionStatus fleet.OrbitHostDiskEncryptionKeyPayload) error } diff --git a/orbit/pkg/update/runner_test.go b/orbit/pkg/update/runner_test.go index 98781de717..035f9fd8b2 100644 --- a/orbit/pkg/update/runner_test.go +++ b/orbit/pkg/update/runner_test.go @@ -78,6 +78,10 @@ func TestGetVersion(t *testing.T) { cmd: "#!/bin/bash\n/bin/echo orbit 4.5.6", version: "4.5.6", }, + "42.0.0": { + cmd: "#!/bin/bash\n/bin/echo fleet-desktop 42.0.0", + version: "42.0.0", + }, "5.10.2-26-gc396d07b4-dirty": { cmd: "#!/bin/bash\n/bin/echo osquery version 5.10.2-26-gc396d07b4-dirty", version: "5.10.2-26-gc396d07b4-dirty", diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index f8a07b8eda..2a675fa0ef 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -630,7 +630,10 @@ SELECT host_id = h.id ) AS additional, COALESCE(failing_policies.count, 0) AS failing_policies_count, - COALESCE(failing_policies.count, 0) AS total_issues_count + COALESCE(failing_policies.count, 0) AS total_issues_count, + hoi.version AS orbit_version, + hoi.desktop_version AS fleet_desktop_version, + hoi.scripts_enabled AS scripts_enabled ` + hostMDMSelect + ` FROM hosts h @@ -638,6 +641,7 @@ FROM LEFT JOIN host_seen_times hst ON (h.id = hst.host_id) LEFT JOIN host_updates hu ON (h.id = hu.host_id) LEFT JOIN host_disks hd ON hd.host_id = h.id + LEFT JOIN host_orbit_info hoi ON hoi.host_id = h.id ` + hostMDMJoin + ` JOIN ( SELECT @@ -3693,12 +3697,14 @@ func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) return &key, nil } -func (ds *Datastore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error { +func (ds *Datastore) SetOrUpdateHostOrbitInfo( + ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool, +) error { return ds.updateOrInsert( ctx, - `UPDATE host_orbit_info SET version = ? WHERE host_id = ?`, - `INSERT INTO host_orbit_info (version, host_id) VALUES (?, ?)`, - version, hostID, + `UPDATE host_orbit_info SET version = ?, desktop_version = ?, scripts_enabled = ? WHERE host_id = ?`, + `INSERT INTO host_orbit_info (version, desktop_version, scripts_enabled, host_id) VALUES (?, ?, ?, ?)`, + version, desktopVersion, scriptsEnabled, hostID, ) } diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 106cd57fff..e2841849e2 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -211,16 +211,32 @@ func testUpdateHost(t *testing.T, ds *Datastore, updateHostFunc func(context.Con assert.Equal(t, policyUpdatedAt.UTC(), host.PolicyUpdatedAt) assert.NotNil(t, host.RefetchCriticalQueriesUntil) assert.True(t, time.Now().Before(*host.RefetchCriticalQueriesUntil)) + assert.Nil(t, host.OrbitVersion) + assert.Nil(t, host.DesktopVersion) + assert.Nil(t, host.ScriptsEnabled) additionalJSON := json.RawMessage(`{"foobar": "bim"}`) err = ds.SaveHostAdditional(context.Background(), host.ID, &additionalJSON) require.NoError(t, err) + // set host orbit info + var ( + orbitVersion = "1.1.0" + desktopVersion = "2.1.0" + ) + err = ds.SetOrUpdateHostOrbitInfo( + context.Background(), host.ID, orbitVersion, sql.NullString{String: desktopVersion, Valid: true}, + sql.NullBool{Bool: true, Valid: true}, + ) + require.NoError(t, err) host, err = ds.Host(context.Background(), host.ID) require.NoError(t, err) require.NotNil(t, host) require.NotNil(t, host.Additional) assert.Equal(t, additionalJSON, *host.Additional) + assert.Equal(t, orbitVersion, *host.OrbitVersion) + assert.Equal(t, desktopVersion, *host.DesktopVersion) + assert.True(t, *host.ScriptsEnabled) err = updateHostFunc(context.Background(), host) require.NoError(t, err) @@ -229,10 +245,18 @@ func testUpdateHost(t *testing.T, ds *Datastore, updateHostFunc func(context.Con err = updateHostFunc(context.Background(), host) require.NoError(t, err) + err = ds.SetOrUpdateHostOrbitInfo( + context.Background(), host.ID, orbitVersion, sql.NullString{Valid: false}, sql.NullBool{Valid: false}, + ) + require.NoError(t, err) + host, err = ds.Host(context.Background(), host.ID) require.NoError(t, err) require.NotNil(t, host) require.Nil(t, host.RefetchCriticalQueriesUntil) + assert.Equal(t, orbitVersion, *host.OrbitVersion) + assert.Nil(t, host.DesktopVersion) + assert.Nil(t, host.ScriptsEnabled) p, err := ds.NewPack(context.Background(), &fleet.Pack{ Name: t.Name(), @@ -6515,7 +6539,9 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) { err = ds.SetOrUpdateHostDisksSpace(context.Background(), host.ID, 12, 25, 40.0) require.NoError(t, err) // set host orbit info - err = ds.SetOrUpdateHostOrbitInfo(context.Background(), host.ID, "1.1.0") + err = ds.SetOrUpdateHostOrbitInfo( + context.Background(), host.ID, "1.1.0", sql.NullString{String: "2.1.0", Valid: true}, sql.NullBool{Bool: true, Valid: true}, + ) require.NoError(t, err) // set an encryption key err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "TESTKEY", "", nil) diff --git a/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields.go b/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields.go new file mode 100644 index 0000000000..6ad62adc3d --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields.go @@ -0,0 +1,27 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20240408085837, Down_20240408085837) +} + +func Up_20240408085837(tx *sql.Tx) error { + _, err := tx.Exec( + `ALTER TABLE host_orbit_info ADD COLUMN ( + desktop_version VARCHAR(50) DEFAULT NULL, + scripts_enabled TINYINT(1) DEFAULT NULL + )`, + ) + if err != nil { + return fmt.Errorf("failed to add desktop_version and scripts_enabled to host_orbit_info: %w", err) + } + return nil +} + +func Down_20240408085837(*sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields_test.go b/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields_test.go new file mode 100644 index 0000000000..e62b00e7da --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields_test.go @@ -0,0 +1,56 @@ +package tables + +import ( + "context" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestUp_20240408085837(t *testing.T) { + db := applyUpToPrev(t) + + // Insert data into orbit_info + id := 1 + execNoErr(t, db, "INSERT INTO host_orbit_info (host_id, version) VALUES (?, ?)", id, "") + + applyNext(t, db) + + type orbitInfo struct { + HostID int64 `db:"host_id"` + Version string `db:"version"` + DesktopVersion *string `db:"desktop_version"` + ScriptsEnabled *bool `db:"scripts_enabled"` + } + + var results []orbitInfo + err := db.SelectContext(context.Background(), &results, `SELECT * FROM host_orbit_info WHERE host_id = ?`, id) + require.NoError(t, err) + assert.Len(t, results, 1) + assert.Nil(t, results[0].DesktopVersion) + assert.Nil(t, results[0].ScriptsEnabled) + + id = 2 + results = nil + execNoErr(t, db, "INSERT INTO host_orbit_info (host_id, version) VALUES (?, ?)", id, "") + err = db.SelectContext(context.Background(), &results, `SELECT * FROM host_orbit_info WHERE host_id = ?`, id) + require.NoError(t, err) + assert.Len(t, results, 1) + assert.Nil(t, results[0].DesktopVersion) + assert.Nil(t, results[0].ScriptsEnabled) + + id = 3 + results = nil + const desktopVersion = "1.0.0" + const scriptsEnabled = true + execNoErr( + t, db, "INSERT INTO host_orbit_info (host_id, version, desktop_version, scripts_enabled) VALUES (?, ?, ?, ?)", id, "", + desktopVersion, + scriptsEnabled, + ) + err = db.SelectContext(context.Background(), &results, `SELECT * FROM host_orbit_info WHERE host_id = ?`, id) + require.NoError(t, err) + assert.Len(t, results, 1) + assert.Equal(t, desktopVersion, *results[0].DesktopVersion) + assert.Equal(t, scriptsEnabled, *results[0].ScriptsEnabled) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 1d8726bbfb..08da189437 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -421,6 +421,8 @@ CREATE TABLE `host_operating_system` ( CREATE TABLE `host_orbit_info` ( `host_id` int(10) unsigned NOT NULL, `version` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `desktop_version` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `scripts_enabled` tinyint(1) DEFAULT NULL, PRIMARY KEY (`host_id`), KEY `idx_host_orbit_info_version` (`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -883,9 +885,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=262 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=263 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/datastore/mysql/statistics_test.go b/server/datastore/mysql/statistics_test.go index 0dd5a97b99..d7648c7859 100644 --- a/server/datastore/mysql/statistics_test.go +++ b/server/datastore/mysql/statistics_test.go @@ -2,6 +2,7 @@ package mysql import ( "context" + "database/sql" "encoding/json" "testing" "time" @@ -97,7 +98,11 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) { require.NoError(t, err) // Create host_orbit_info record for test - require.NoError(t, ds.SetOrUpdateHostOrbitInfo(ctx, h1.ID, "1.1.0")) + require.NoError( + t, ds.SetOrUpdateHostOrbitInfo( + ctx, h1.ID, "1.1.0", sql.NullString{String: "1.1.0", Valid: true}, sql.NullBool{Bool: true, Valid: true}, + ), + ) // Create two new users for test u1, err := ds.NewUser(ctx, &fleet.User{ diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 1c03aca415..d549464d2a 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -3,6 +3,7 @@ package fleet import ( "context" "crypto/x509" + "database/sql" "encoding/json" "errors" "io" @@ -836,7 +837,9 @@ type Datastore interface { GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *Host, cmdUUID string) (HostMDMProfileRetryCount, error) // SetOrUpdateHostOrbitInfo inserts of updates the orbit info for a host - SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error + SetOrUpdateHostOrbitInfo( + ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool, + ) error ReplaceHostDeviceMapping(ctx context.Context, id uint, mappings []*HostDeviceMapping, source string) error diff --git a/server/fleet/errors.go b/server/fleet/errors.go index ed519cf270..ed6bf27187 100644 --- a/server/fleet/errors.go +++ b/server/fleet/errors.go @@ -545,6 +545,7 @@ const ( RunScriptHostTimeoutErrMsg = "Fleet didn’t hear back from the host in under 5 minutes (timeout for live scripts). Fleet doesn’t know if the script ran because it didn’t receive the result. Please try again." RunScriptScriptsDisabledGloballyErrMsg = "Running scripts is disabled in organization settings." RunScriptDisabledErrMsg = "Scripts are disabled for this host. To run scripts, deploy the fleetd agent with scripts enabled." + RunScriptsOrbitDisabledErrMsg = "Couldn't run script. To run a script, deploy the fleetd agent with --enable-scripts." RunScriptScriptTimeoutErrMsg = "Timeout. Fleet stopped the script after 5 minutes to protect host performance." RunScriptAsyncScriptEnqueuedErrMsg = "Script is running or will run when the host comes online." RunScripSavedMaxLenErrMsg = "Script is too large. It's limited to 500,000 characters (approximately 10,000 lines)." diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 1a7344729e..441e9e7b2e 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -255,6 +255,9 @@ type Host struct { // Platform is the host's platform as defined by osquery's os_version.platform. Platform string `json:"platform" csv:"platform"` OsqueryVersion string `json:"osquery_version" db:"osquery_version" csv:"osquery_version"` + OrbitVersion *string `json:"orbit_version" db:"orbit_version" csv:"orbit_version"` + DesktopVersion *string `json:"fleet_desktop_version" db:"fleet_desktop_version" csv:"fleet_desktop_version"` + ScriptsEnabled *bool `json:"scripts_enabled" db:"scripts_enabled" csv:"scripts_enabled"` OSVersion string `json:"os_version" db:"os_version" csv:"os_version"` Build string `json:"build" csv:"build"` PlatformLike string `json:"platform_like" db:"platform_like" csv:"platform_like"` diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index c42c08ee6c..c94e88e953 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -5,6 +5,7 @@ package mock import ( "context" "crypto/x509" + "database/sql" "encoding/json" "math/big" "sync" @@ -586,7 +587,7 @@ type GetHostMDMProfilesRetryCountsFunc func(ctx context.Context, host *fleet.Hos type GetHostMDMProfileRetryCountByCommandUUIDFunc func(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) -type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string) error +type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool) error type ReplaceHostDeviceMappingFunc func(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping, source string) error @@ -4210,11 +4211,11 @@ func (s *DataStore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context return s.GetHostMDMProfileRetryCountByCommandUUIDFunc(ctx, host, cmdUUID) } -func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error { +func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool) error { s.mu.Lock() s.SetOrUpdateHostOrbitInfoFuncInvoked = true s.mu.Unlock() - return s.SetOrUpdateHostOrbitInfoFunc(ctx, hostID, version) + return s.SetOrUpdateHostOrbitInfoFunc(ctx, hostID, version, desktopVersion, scriptsEnabled) } func (s *DataStore) ReplaceHostDeviceMapping(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping, source string) error { diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 43be6eff3c..41b8f74a54 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -7464,14 +7464,14 @@ func (s *integrationTestSuite) TestHostsReportDownload() { res.Body.Close() require.NoError(t, err) require.Len(t, rows, len(hosts)+1) // all hosts + header row - assert.Len(t, rows[0], 51) // total number of cols + assert.Len(t, rows[0], 54) // total number of cols const ( idCol = 3 - issuesCol = 42 - gigsDiskCol = 39 - pctDiskCol = 40 - gigsTotalCol = 41 + issuesCol = 45 + gigsDiskCol = 42 + pctDiskCol = 43 + gigsTotalCol = 44 ) // find the row for hosts[1], it should have issues=1 (1 failing policy) and the expected disk space @@ -8507,6 +8507,10 @@ func createOrbitEnrolledHost(t *testing.T, os, suffix string, ds fleet.Datastore HardwareSerial: h.HardwareSerial, }, orbitKey, nil) require.NoError(t, err) + err = ds.SetOrUpdateHostOrbitInfo( + context.Background(), h.ID, "1.22.0", sql.NullString{String: "42", Valid: true}, sql.NullBool{Bool: true, Valid: true}, + ) + require.NoError(t, err) h.OrbitNodeKey = &orbitKey return h } @@ -9564,7 +9568,7 @@ func (s *integrationTestSuite) TestHostsReportWithPolicyResults() { res.Body.Close() require.NoError(t, err) require.Len(t, rows1, len(hosts)+1) // all hosts + header row - assert.Len(t, rows1[0], 51) // total number of cols + assert.Len(t, rows1[0], 54) // total number of cols var ( idIdx int @@ -9591,7 +9595,7 @@ func (s *integrationTestSuite) TestHostsReportWithPolicyResults() { res.Body.Close() require.NoError(t, err) require.Len(t, rows2, len(hosts)+1) // all hosts + header row - assert.Len(t, rows2[0], 51) // total number of cols + assert.Len(t, rows2[0], 54) // total number of cols // Check that all hosts have 0 issues and that they match the previous call to `/hosts/report`. for i := 1; i < len(hosts)+1; i++ { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 06d2e1e388..957f41623f 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -5106,6 +5106,27 @@ func (s *integrationEnterpriseTestSuite) TestRunHostScript() { require.True(t, scriptResultResp.HostTimeout) require.Contains(t, scriptResultResp.Message, fleet.RunScriptHostTimeoutErrMsg) + // Disable scripts on the host + scriptsEnabled := false + err = s.ds.SetOrUpdateHostOrbitInfo( + context.Background(), host.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: scriptsEnabled, Valid: true}, + ) + require.NoError(t, err) + s.DoJSON( + "POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"}, + http.StatusUnprocessableEntity, &runResp, + ) + s.DoJSON( + "POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"}, + http.StatusUnprocessableEntity, &runResp, + ) + // Re-enable scripts on the host + scriptsEnabled = true + err = s.ds.SetOrUpdateHostOrbitInfo( + context.Background(), host.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: scriptsEnabled, Valid: true}, + ) + require.NoError(t, err) + // make the host "offline" err = s.ds.MarkHostsSeen(context.Background(), []uint{host.ID}, time.Now().Add(-time.Hour)) require.NoError(t, err) diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 6d38c9203c..d6e34624f6 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -3,6 +3,7 @@ package service import ( "bytes" "context" + "database/sql" "encoding/json" "errors" "fmt" @@ -1676,8 +1677,12 @@ func TestDetailQueries(t *testing.T) { require.Equal(t, "3.4.5", version) return nil } - ds.SetOrUpdateHostOrbitInfoFunc = func(ctx context.Context, hostID uint, version string) error { + ds.SetOrUpdateHostOrbitInfoFunc = func( + ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool, + ) error { require.Equal(t, "42", version) + require.Equal(t, sql.NullString{String: "1.2.3", Valid: true}, desktopVersion) + require.Equal(t, sql.NullBool{Bool: true, Valid: true}, scriptsEnabled) return nil } ds.SetOrUpdateDeviceAuthTokenFunc = func(ctx context.Context, hostID uint, authToken string) error { @@ -1851,7 +1856,9 @@ func TestDetailQueries(t *testing.T) { ], "fleet_detail_query_orbit_info": [ { - "version": "42" + "version": "42", + "desktop_version": "1.2.3", + "scripts_enabled": "1" } ] } diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index b7b3d6409f..7c6db09356 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -2,6 +2,7 @@ package osquery_utils import ( "context" + "database/sql" "encoding/base64" "encoding/hex" "fmt" @@ -607,8 +608,9 @@ var extraDetailQueries = map[string]DetailQuery{ DirectIngestFunc: directIngestOSUnixLike, }, "orbit_info": { - Query: `SELECT version FROM orbit_info`, + Query: `SELECT * FROM orbit_info`, DirectIngestFunc: directIngestOrbitInfo, + Platforms: append(fleet.HostLinuxOSs, "darwin", "windows"), Discovery: discoveryTable("orbit_info"), }, "disk_encryption_darwin": { @@ -1054,7 +1056,15 @@ func directIngestOrbitInfo(ctx context.Context, logger log.Logger, host *fleet.H return ctxerr.Errorf(ctx, "directIngestOrbitInfo invalid number of rows: %d", len(rows)) } version := rows[0]["version"] - if err := ds.SetOrUpdateHostOrbitInfo(ctx, host.ID, version); err != nil { + var desktopVersion sql.NullString + desktopVersion.String, desktopVersion.Valid = rows[0]["desktop_version"] + var scriptsEnabled sql.NullBool + scriptsEnabledStr, ok := rows[0]["scripts_enabled"] + if ok { + scriptsEnabled.Bool = scriptsEnabledStr == "1" + scriptsEnabled.Valid = true + } + if err := ds.SetOrUpdateHostOrbitInfo(ctx, host.ID, version, desktopVersion, scriptsEnabled); err != nil { return ctxerr.Wrap(ctx, err, "directIngestOrbitInfo update host orbit info") } diff --git a/server/service/scripts.go b/server/service/scripts.go index e383b49746..0017648a4b 100644 --- a/server/service/scripts.go +++ b/server/service/scripts.go @@ -202,6 +202,13 @@ func (svc *Service) RunHostScript(ctx context.Context, request *fleet.HostScript return nil, fleet.NewUserMessageError(errors.New(fleet.RunScriptDisabledErrMsg), http.StatusUnprocessableEntity) } + // If scripts are disabled (according to the last detail query), we return an error. + // host.ScriptsEnabled may be nil for older orbit versions. + if host.ScriptsEnabled != nil && !*host.ScriptsEnabled { + svc.authz.SkipAuthorization(ctx) + return nil, fleet.NewUserMessageError(errors.New(fleet.RunScriptsOrbitDisabledErrMsg), http.StatusUnprocessableEntity) + } + maxPending := maxPendingScripts // authorize with the host's team and the script id provided, as both affect diff --git a/tools/tuf/test/README.md b/tools/tuf/test/README.md index dacac03e75..6bf057e602 100644 --- a/tools/tuf/test/README.md +++ b/tools/tuf/test/README.md @@ -9,6 +9,7 @@ Scripts in this directory aim to ease the testing of Orbit and the [TUF](https:/ 1. The script is executed on a macOS host. 2. Fleet server also running on the same macOS host. 3. All VMs (and the macOS host itself) are configured to resolve `host.docker.internal` to the macOS host IP (by modifying their `hosts` file). +4. The hosts are running on the same GOARCH as the macOS host. If not, you can set the `GOARCH` environment variable to compile for the desired architecture. For example: `GOARCH=amd64` > PS: We use `host.docker.internal` because the testing certificate `./tools/osquery/fleet.crt` > has such hostname (and `localhost`) defined as SANs. From 23772c69b7f6d6ca3e0503a9d5352b40f14607c3 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Tue, 9 Apr 2024 18:37:55 -0300 Subject: [PATCH 28/83] Create `releaser.sh` to release fleetd updates (#18134) #16131 `releaser.sh` was used to release fleetd 1.23.0. --- .vscode/extensions.json | 3 +- Makefile | 6 +- orbit/docs/TUF-Update-Guide.md | 392 ------------------ tools/tuf/README.md | 292 +++++++++++++ .../download-artifacts/download-artifacts.go | 97 +++-- tools/tuf/promote_edge_to_stable.sh | 51 --- tools/tuf/releaser.sh | 305 ++++++++++++++ tools/tuf/replace/main.go | 31 ++ tools/tuf/test/create_repository.sh | 2 +- 9 files changed, 702 insertions(+), 477 deletions(-) delete mode 100644 orbit/docs/TUF-Update-Guide.md create mode 100644 tools/tuf/README.md delete mode 100755 tools/tuf/promote_edge_to_stable.sh create mode 100755 tools/tuf/releaser.sh create mode 100644 tools/tuf/replace/main.go diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a8597204ad..8725e4126f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,6 +7,7 @@ "redhat.vscode-yaml", "dbaeumer.vscode-eslint", "firefox-devtools.vscode-firefox-debug", - "editorconfig.editorconfig" + "editorconfig.editorconfig", + "timonwong.shellcheck" ] } \ No newline at end of file diff --git a/Makefile b/Makefile index fa4f92572c..03b20a73b1 100644 --- a/Makefile +++ b/Makefile @@ -318,7 +318,9 @@ changelog: sh -c "git rm changes/*" changelog-orbit: - sh -c "find orbit/changes -type file | grep -v .keep | xargs -I {} sh -c 'grep \"\S\" {}; echo' > new-CHANGELOG.md" + $(eval TODAY_DATE := $(shell date "+%b %d, %Y")) + @echo -e "## Orbit $(version) ($(TODAY_DATE))\n" > new-CHANGELOG.md + sh -c "find orbit/changes -type file | grep -v .keep | xargs -I {} sh -c 'grep \"\S\" {} | sed -E "s/^-/*/"; echo' >> new-CHANGELOG.md" sh -c "cat new-CHANGELOG.md orbit/CHANGELOG.md > tmp-CHANGELOG.md && rm new-CHANGELOG.md && mv tmp-CHANGELOG.md orbit/CHANGELOG.md" sh -c "git rm orbit/changes/*" @@ -394,7 +396,7 @@ ifneq ($(shell uname), Darwin) @exit 1 endif # locking the version of swiftDialog to 2.2.1-4591 as newer versions - # migth have layout issues. + # might have layout issues. ifneq ($(version), 2.2.1) @echo "Version is locked at 2.1.0, see comments in Makefile target for details" @exit 1 diff --git a/orbit/docs/TUF-Update-Guide.md b/orbit/docs/TUF-Update-Guide.md deleted file mode 100644 index e2f4946ecf..0000000000 --- a/orbit/docs/TUF-Update-Guide.md +++ /dev/null @@ -1,392 +0,0 @@ -# Pushing new releases to TUF - -This document is a walkthrough guide for: -- A Fleet member to publish new updates to [Fleet's TUF service](tuf.fleetctl.com). See [Pushing updates](#pushing-updates). -- A Fleet member to delete targets from [Fleet's TUF service](tuf.fleetctl.com). See [Removing unused targets](#removing-unused-targets). -- A Fleet member to become a publisher of updates for [Fleet's TUF service](tuf.fleetctl.com). See [Becoming a new Fleet publisher](#becoming-a-new-fleet-publisher). - -Video walkthrough related to this process for additional [context](https://drive.google.com/file/d/1c_iukFEMne12Cxx9WVTt_j1Wp0sC1kQU/view?usp=drive_link). - -## Security - -- The TUF keys for `targets`, `snapshot` and `timestamp` should be stored on a USB stick (used solely for this purpose). Whenever you need to push updates to Fleet's TUF repository you can temporarily copy the encrypted keys to your workstation (under the `keys/` folder, more on this below). -- The keys should be stored encrypted with its passphrase stored in 1Password (on a private vault). -- Every `fleetctl updates` command will prompt for the passphrases to decrypt the encrypted keys. You can input the passphrases every time or can alternatively set the following environment variables: `FLEET_TIMESTAMP_PASSPHRASE`, `FLEET_SNAPSHOT_PASSPHRASE` and `FLEET_TARGETS_PASSPHRASE`. Make sure to not leave traces of the passphrases (scripts, history and/or environment) when you are done. - -## Syncing Fleet's TUF repository - -> The `fleetctl updates` commands assume the folders `keys/`, `staged/` and `repository/` exist on the current working directory. - -> IMPORTANT: When syncing the repository make sure to use `--exact-timestamps`. Otherwise `aws s3 sync` may not sync files that do not change in size, like `timestamp.json`. - -- The `keys/` folder contains the encrypted private keys. -- The `staged/` folder contains uncommitted changes (usually empty because `fleetctl updates` commands automatically commit the changes). -- The `repository/` folder contains the full TUF repository. - -Following are the commands to initialize the repository on your workstation: -```sh -mkdir /path/to/tuf.fleetctl.com - -cd /path/to/tuf.fleetctl.com -mkdir -p ./repository -cp /Volumes/YOUR-USB-NAME/keys ./keys -mkdir -p ./staged - -export AWS_PROFILE=tuf -aws sso login - -aws s3 sync s3://fleet-tuf-repo ./repository --exact-timestamps -``` - -## Building the components for releasing to `edge` - -### fleetd - -> Assuming we are releasing version 1.21.0 of fleetd. - -1. Create the fleetd changelog for the new release: -```sh -git checkout main -git pull origin main -git checkout -b release-fleetd-v1.21.0 -make changelog-orbit -``` -2. Edit `orbit/CHANGELOG.md` accordingly -3. Bump Fleet Desktop version in https://github.com/fleetdm/fleet/blob/9ca85411a16c504087d2793f8b9099f98054c93f/.github/workflows/generate-desktop-targets.yml#L27. This will trigger a github action to build the Fleet Desktop executables: https://github.com/fleetdm/fleet/actions/workflows/generate-desktop-targets.yml. -4. Commit the changes, push the branch and create a PR. -5. Add the following git tag with the following format: `orbit-v1.21.0`. Once pushed this will trigger a github action to build the orbit executables: https://github.com/fleetdm/fleet/blob/main/.github/workflows/goreleaser-orbit.yaml. -```sh -git tag orbit-v1.21.0 -git push origin --tags -``` -6. Once the two github actions finish their runs, use the following scripts that will download the artifacts to a folder in your workstation (on this guide we assume you are using `$HOME/release-friday`). -NOTE: The `goreleaser-macos` job is unstable and may need several re-runs until it works. -```sh -go run ./tools/tuf/download-artifacts desktop \ - --git-branch release-fleetd-v1.21.0 \ - --output-directory $HOME/release-friday/desktop \ - --github-username $GITHUB_USERNAME --github-api-token $GITHUB_TOKEN -go run ./tools/tuf/download-artifacts orbit \ - --git-tag orbit-v1.21.0 \ - --output-directory $HOME/release-friday/orbit \ - --github-username $GITHUB_USERNAME --github-api-token $GITHUB_TOKEN -tree $HOME/release-friday -$HOME/release-friday -├── desktop -│   ├── linux -│   │   └── desktop.tar.gz -│   ├── macos -│   │   └── desktop.app.tar.gz -│   └── windows -│   └── fleet-desktop.exe -└── orbit - ├── linux - │   └── orbit - ├── macos - │   └── orbit - └── windows - └── orbit.exe -``` -7. With the executables on your workstation, proceed to [Pushing updates](#pushing-updates) (`edge`). -8. Manually run (`Run workflow`) this action that will update the released versions on our doc: https://github.com/fleetdm/fleet/actions/workflows/fleetd-tuf.yml. - -### osqueryd - -> Assuming we are releasing version 5.12.0 of osqueryd. - -1. Bump osquery version in https://github.com/fleetdm/fleet/blob/30a36b0b3a1fd50e48d98a4c3c955595022f5277/.github/workflows/generate-osqueryd-targets.yml#L27. -2. Commit the changes, push the branch (assuming branch name is `bump-osqueryd-5.12.0`) and create a PR. -3. Once the Github action completes run the following (the [GitHub API token](https://github.com/settings/tokens?type=beta) does not need any special permissions -- public repository access is sufficient): -```sh -go run ./tools/tuf/download-artifacts osqueryd \ - --git-branch bump-osqueryd-5.12.0 \ - --output-directory $HOME/release-friday/osqueryd \ - --github-username $GITHUB_USERNAME \ - --github-api-token $GITHUB_TOKEN -tree $HOME/release-friday/osqueryd -$HOME/release-friday/osqueryd -├── linux -│   └── osqueryd -├── macos -│   └── osqueryd.app.tar.gz -└── windows - └── osqueryd.exe -``` -4. With the executables on your workstation, proceed to [Pushing updates](#pushing-updates) (`edge`). -5. Manually run (`Run workflow`) this action that will update the released versions on our docs: https://github.com/fleetdm/fleet/actions/workflows/fleetd-tuf.yml. - -## Pushing updates - -> Before performing any actions on Fleet's TUF repository you must: -> 1. Make sure your local copy of the repository is up-to-date. See [Syncing Fleet's TUF repository](#syncing-fleets-tuf-repository). -> 2. Create a local backup in case we mess up with the repository: -> ```sh -> mkdir ~/tuf.fleetctl.com/backup -> cp -r ~/tuf.fleetctl.com ~/tuf.fleetctl.com-backup -> ``` -> 3. Install fleetd on macOS, Linux and Windows VMs using the channel (`stable` or `edge`) you are about to release. -> You can do this using the following flags in `fleetctl package`: `--orbit-channel`, `--desktop-channel`, `--osqueryd-channel`. - -### Releasing to the `edge` channel - -The commands shown here update the local repository. After you are done running the commands below for each component, see [Pushing releases to Fleet's TUF repository](#pushing-releases-to-fleets-tuf-repository) to push the updates to Fleet's TUF repository (https://tuf.fleetctl.com). - -#### Setup - -Make sure to install fleetd components using the `edge` channels in the three supported OSs (this is useful to smoke test the update). -Here's how to generate the packages: -```sh -# (The same for --type=deb and --type=msi.) -fleetctl package --type=pkg \ - --enable-scripts \ - --fleet-desktop \ - --fleet-url=... --enroll-secret=... \ - --update-interval 10s \ - --orbit-channel edge --desktop-channel edge --osqueryd-channel edge -``` - -#### orbit - -The `orbit` executables are downloaded from the [GoReleaser Orbit action](https://github.com/fleetdm/fleet/actions/workflows/goreleaser-orbit.yaml). -Such action is triggered when git tagging a new orbit version with a tag of the form: `orbit-v1.21.0`. - -> IMPORTANT: If there are only `orbit` changes on a release we still have to release the `desktop` component with its version string bumped (even if there are no changes in it). -> This is due to the fact that we want users to see the new version in the tray icon, e.g. `"Fleet Desktop v1.21.0"`. -> Technical debt: We could improve this process to reduce the complexity of releasing fleetd when there are no Fleet Desktop changes. - -> The following commands assume you are pushing version `1.21.0`. - -```sh -# macOS -fleetctl updates add --target $HOME/release-friday/orbit/macos/orbit --platform macos --name orbit --version 1.21.0 -t edge -# Linux -fleetctl updates add --target $HOME/release-friday/orbit/linux/orbit --platform linux --name orbit --version 1.21.0 -t edge -# Windows -fleetctl updates add --target $HOME/release-friday/orbit/windows/orbit.exe --platform windows --name orbit --version 1.21.0 -t edge -``` - -#### desktop - -The Fleet Desktop executables are downloaded from the [Generate Fleet Desktop targets for Orbit action](https://github.com/fleetdm/fleet/actions/workflows/generate-desktop-targets.yml). -Such action is triggered by submitting a PR with the [following version string](https://github.com/fleetdm/fleet/blob/4a6bf0d447a2080f994da1e2f36ce6d51db88109/.github/workflows/generate-desktop-targets.yml#L27) changed. - -> The following commands assume you are pushing version `1.21.0`. - -```sh -# macOS -fleetctl updates add --target $HOME/release-friday/desktop/macos/desktop.app.tar.gz --platform macos --name desktop --version 1.21.0 -t edge -# Linux -fleetctl updates add --target $HOME/release-friday/desktop/linux/desktop.tar.gz --platform linux --name desktop --version 1.21.0 -t edge -# Windows -fleetctl updates add --target $HOME/release-friday/desktop/windows/fleet-desktop.exe --platform windows --name desktop --version 1.21.0 -t edge -``` - -#### swiftDialog - -> macOS only component - -The `swiftDialog` executable can be generated from a macOS host by running: -```sh -make swift-dialog-app-tar-gz version=2.2.1 build=4591 out-path=. -``` - -```sh -fleetctl updates add --target /path/to/macos/swiftDialog.app.tar.gz --platform macos --name swiftDialog --version 2.2.1 -t edge -``` - -#### nudge - -> macOS only component - -The `nudge` executable can be generated from a macOS host by running: -```sh -make nudge-app-tar-gz version=1.1.10.81462 out-path=. -``` - -```sh -fleetctl updates add --target /path/to/macos/nudge.app.tar.gz --platform macos --name nudge --version 1.1.10.81462 -t edge -``` - -#### osqueryd - -Osquery executables are downloaded from the [Generate osqueryd targets for Fleetd action](https://github.com/fleetdm/fleet/blob/main/.github/workflows/generate-osqueryd-targets.yml). -Such action is triggered by submitting a PR with the [following version string](https://github.com/fleetdm/fleet/blob/7067ca586a4aa1a0377b387d4b4478a5958193ff/.github/workflows/generate-osqueryd-targets.yml#L27) changed. - -> The following commands assume you are pushing version `5.9.1`. - -```sh -# macOS -fleetctl updates add --target $HOME/release-friday/osqueryd/macos/osqueryd.app.tar.gz --platform macos-app --name osqueryd --version 5.9.1 -t edge -# Linux -fleetctl updates add --target $HOME/release-friday/osqueryd/linux/osqueryd --platform linux --name osqueryd --version 5.9.1 -t edge -# Windows -fleetctl updates add --target $HOME/release-friday/osqueryd/windows/osqueryd.exe --platform windows --name osqueryd --version 5.9.1 -t edge -``` - -#### Push updates - -Once all components are updated in your local repository we need to push the changes to the remote repository. -See [Pushing releases to Fleet's TUF repository](#pushing-releases-to-fleets-tuf-repository). - -### Promoting `edge` to the `stable` channel - -> Make sure to install fleetd components using the `stable` channels in the three supported OSs (this is useful to smoke test the update). - -Following is the list of components and each command for each operating system. - -The commands show here update the local repository. After you are done running the commands below for each component, see [Pushing releases to Fleet's TUF repository](#pushing-releases-to-fleets-tuf-repository) to push the updates to Fleet's TUF repository (https://tuf.fleetctl.com). - -#### orbit - -> IMPORTANT: If there are only `orbit` changes on a release we still have to release the `desktop` component with its version string bumped (even if there are no changes in it). -> This is due to the fact that we want users to see the new version in the tray icon, e.g. `"Fleet Desktop v1.21.0"`. -> Technical debt: We could improve this process to reduce the complexity of releasing fleetd when there are no Fleet Desktop changes. - -> The following command assumes you are pushing version `1.21.0`: -```sh -/fleet/repo/tools/tuf/promote_edge_to_stable.sh orbit 1.21.0 -``` - -#### desktop - -> The following command assumes you are pushing version `1.21.0`: -```sh -/fleet/repo/tools/tuf/promote_edge_to_stable.sh desktop 1.21.0 -``` - -#### swiftDialog - -> The following command assumes you are pushing version `2.2.1`: -```sh -/fleet/repo/tools/tuf/promote_edge_to_stable.sh swiftDialog 2.2.1 -``` - -#### nudge - -> The following command assumes you are pushing version `1.1.10.81462`: -```sh -/fleet/repo/tools/tuf/promote_edge_to_stable.sh nudge 1.1.10.81462 -``` - -#### osqueryd - -> The following command assumes you are pushing version `5.9.1`. -```sh -/fleet/repo/tools/tuf/promote_edge_to_stable.sh osqueryd 5.9.1 -``` - -#### Push updates - -Once all components are updated in your local repository we need to push the changes to the remote repository. -See [Pushing releases to Fleet's TUF repository](#pushing-releases-to-fleets-tuf-repository). - -### Pushing releases to Fleet's TUF repository - -Once you are done with the changes on your local repository, you can use the following command to review the changes before pushing (`--dryrun` allows us to verify the upgrade before pushing): -```sh -AWS_PROFILE=tuf aws s3 sync ./repository s3://fleet-tuf-repo --dryrun -(dryrun) upload: repository/snapshot.json to s3://fleet-tuf-repo/snapshot.json -(dryrun) upload: repository/targets.json to s3://fleet-tuf-repo/targets.json -[...] -(dryrun) upload: repository/timestamp.json to s3://fleet-tuf-repo/timestamp.json -``` - -If all looks good, run the same command without the `--dryrun` flag. - -> NOTE: Some things to note after the changes are pushed: -> - Once pushed you might see some clients failing to upgrade due to some sha256 mismatches. These temporary failures are expected because it takes some time for caches to be invalidated (these errors should go away after a few minutes). -> - The auto-update routines in orbit runs every 15 minutes, so you might need to wait up to 15 minutes to verify online hosts are auto-updating properly. - -## Removing Unused Targets - -If you've inadvertently published a target that is no longer in use, follow these steps to remove it. - -> Before performing any actions on Fleet's TUF repository you must: -> 1. Make sure your local copy of the repository is up-to-date. See [Syncing Fleet's TUF repository](#syncing-fleets-tuf-repository). -> 2. Create a local backup in case we mess up with the repository: -> ```sh -> mkdir ~/tuf.fleetctl.com/backup -> cp -r ~/tuf.fleetctl.com ~/tuf.fleetctl.com-backup -> ``` - -1. You'll need the [`go-tuf`](https://github.com/theupdateframework/go-tuf) binary. The removal operations aren't integrated into `fleetctl` at the moment. -2. Use `tuf remove` to remove the target and update `targets.json`. Substitute `desktop/windows/stable/desktop.exe` with the target you intend to delete. -```sh -tuf remove desktop/windows/stable/desktop.exe -``` -3. Snapshot, timestamp, and commit the changes. -```sh -tuf snapshot -tuf timestamp -tuf commit -``` -4. Run the following command to generate a timestamp that expires in two weeks (otherwise the default expiration when using `go-tuf` commands is 1 day) -```sh -fleetctl updates timestamp -``` -5. Confirm that the version of the local `timestamp.json` file is more recent than that of the remote server. -6. Verify the changes that will be synced by running a dry sync. Include the `--delete` flag as you're removing targets. -```sh -aws s3 sync ./repository s3://fleet-tuf-repo --delete --dryrun -``` -7. `diff` the local `targets.json` file with its remote version. -8. To upload the changes, perform a sync without the `--dryrun`: -```sh -aws s3 sync ./repository s3://fleet-tuf-repo --delete -``` - -## Becoming a New Fleet Publisher - -> Before performing any actions on Fleet's TUF repository you must: -> 1. Make sure your local copy of the repository is up-to-date. See [Syncing Fleet's TUF repository](#syncing-fleets-tuf-repository). -> 2. Create a local backup in case we mess up with the repository: -> ```sh -> mkdir ~/tuf.fleetctl.com/backup -> cp -r ~/tuf.fleetctl.com ~/tuf.fleetctl.com-backup -> ``` - -### Generate targets+snapshot+timestamp keys - -All commands shown in this guide are executed from `/path/to/tuf.fleetctl.com`: -```sh -cd /path/to/tuf.fleetctl.com -``` - -```sh -tuf gen-key targets -Enter targets keys passphrase: -Repeat targets keys passphrase: -Generated targets key with ID ae943cb8be8a849b37c66ed46bdd7e905ba3118c0c051a6ee3cd30625855a076 -``` -```sh -tuf gen-key snapshot -Enter snapshot keys passphrase: -Repeat snapshot keys passphrase: -Generated snapshot key with ID 1a4d9beb826d1ff4e036d757cfcd6e36d0f041e58d25f99ef3a20ae3f8dd71e3 -``` -```sh -tuf gen-key timestamp -Enter timestamp keys passphrase: -Repeat timestamp keys passphrase: -Generated timestamp key with ID d940df08b59b12c30f95622a05cc40164b78a11dd7d408395ee4f79773331b30 -``` - -Share `staged/root.json` with Fleet member with the `root` role, who will sign with its root key and push to the repository. - -### Root role signs the `staged/root.json` - -Essentially the following commands are executed to sign the new keys: -- `tuf sign` -- `tuf snapshot` -- `tuf timestamp` -- `tuf commit` - -## Misc issues - -### Invalid timestamp.json version - -The following issue was solved by resigning the timestamp metadata `fleetctl updates timestamp` (executed three times to increase the version to `4175`) -```sh -2022-08-23T13:44:48-03:00 INF update failed error="update metadata: update metadata: tuf: failed to decode timestamp.json: version 4172 is lower than current version 4174" -2022-08-23T13:59:48-03:00 INF update failed error="update metadata: update metadata: tuf: failed to decode timestamp.json: version 4172 is lower than current version 4174" -``` diff --git a/tools/tuf/README.md b/tools/tuf/README.md new file mode 100644 index 0000000000..126b2610bc --- /dev/null +++ b/tools/tuf/README.md @@ -0,0 +1,292 @@ +# Releasing updates to Fleet's TUF repository + +The `releaser.sh` script automates the building and releasing of fleetd and osquery updates on [Fleet's TUF repository](https://tuf.fleetctl.com). + +> - The script was developed and tested on macOS Intel. +> - It currently supports pushing new `fleetd` and `osqueryd` versions. +> - By storing credentials encrypted in a USB flash drive and storing their decryption passphrase on 1Password we are enforcing a form of 2FA. + +```mermaid +graph LR; + subgraph Workstation; + releaser[releaser.sh]; + 1password("
1Password"); + usb("
USB flash drive"); + repository[(./repository)]; + end; + s3("
s3://fleet-tuf-repo"); + github("
Github Action\n(signing and notarization)"); + + usb--(1) copy encrypted signing keys-->releaser; + 1password--(2) get passphrases to decrypt encrypted signing keys-->releaser; + 1password--(3) get Github API token-->releaser; + s3--(4) pull TUF repository-->releaser; + releaser--(5) build components (new updates)\n(osqueryd, orbit, Fleet Desktop)-->github; + github--(6) download built components-->releaser; + releaser--(7) push updates and signed metadata-->s3; +``` + +## Permissions and configuration + +Following is the checklist for all credentials and configuration needed to run the script. + +### Dependencies + +- `make` +- `git` +- 1Password 8 application. +- Install and configure 1Password's `op` cli to connect to the application: https://developer.1password.com/docs/cli/get-started/ +- `aws` cli :`brew install awscli`. +- `fleetctl`: Either built from source or installed by npm. +- `tuf`: Download the release from https://github.com/theupdateframework/go-tuf/releases/download/v0.7.0/tuf_0.7.0_darwin_amd64.tar.gz and place the `tuf` executable in `/usr/local/bin/tuf`. You will need to make an exception in "Privacy & Security" because the executable is not signed. + +### 1Password + +You need to create three passphrases on your private 1Password vault for encrypting the signing keys (more on signing keys below). +Create three private "passwords" with the following names: `TUF TARGETS`, `TUF SNAPSHOT` and `TUF TIMESTAMP`. +The resulting credentials will have the following "path" within 1Password (these paths will be provided to the `releaser.sh` script) +```sh +Private/TUF TARGETS/password +Private/TUF SNAPSHOT/password +Private/TUF TIMESTAMP/password +``` + +### AWS + +The following is required to be able to run `aws` cli commands. + +1. You will need to request the infrastructure team to add the "TUFAdministrators" role to your Google account. +2. Configure AWS SSO with the following steps: https://github.com/fleetdm/confidential/tree/main/infrastructure/sso#how-to-use-sso. +Set the profile name as `tuf` (the profile name will be provided to the `releaser.sh` script). +3. Test the access by running: +```sh +AWS_PROFILE=tuf aws sso login +``` + +### TUF signing keys + +> You can skip this step if you already have authorized keys to sign and publish updates. + +To release updates to our TUF repository you need the `root` role (ask in Slack who has such `root` role) to sign your signing keys. +First, run the following script +```sh +AWS_PROFILE=tuf \ +ACTION=generate-signing-keys \ +TUF_DIRECTORY=/Users/luk/tuf3.fleetctl.com \ +TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \ +SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \ +TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \ +./tools/tuf/releaser.sh +``` + +The human with the `root` role will run the following commands to sign the provided `staged/root.json`: +```sh +tuf sign +tuf snapshot +tuf timestamp +tuf commit +``` +And push the newly signed `root.json` to the remote repository. + +### Encrypted keys in USB + +For releasing fleetd you need to plug in the USB that contains encrypted signing keys. +In this guide we assume the USB device will be mounted in `/Volumes/FLEET-TUF/` and it ONLY contains a `keys/` directory. + +### Github + +#### Personal access token + +> A personal access token is required to download artifacts from Github Actions using the Github API. + +1. Create a fine-grained personal access token at https://github.com/settings/tokens?type=beta +2. Store the token on 1Password as a "password" with name "Github Token" +The resulting credential will have the following "path" within 1Password (this path will be provided to the script) +```sh +Private/Github Token/password +``` + +#### Github session + +You need to log in to your Github account with your default browser. +It will be used to open your browser and allow you to create the PR needed to build artifacts (this can be improved later, see TODOs). + +## Samples + +Following are samples of the script execution to release components to `edge` and `stable`. + +> When releasing fleetd you need to checkout the branch (e.g. `main`) you want to release. + +> NOTE: When releasing fleetd: +> If there are only `orbit` changes on a release we still have to release the `desktop` component with its version string bumped +> (even if there are no changes in it). This is due to the fact that we want users to see the new version in the tray icon, +> e.g. `"Fleet Desktop v1.21.0"`. Technical debt: We could improve this process to reduce the complexity of releasing +> fleetd when there are no Fleet Desktop changes. + +### Releasing to `edge` + +#### Releasing fleetd `1.23.0` to `edge` + +```sh +AWS_PROFILE=tuf \ +TUF_DIRECTORY=/Users/foobar/tuf.fleetctl.com \ +COMPONENT=fleetd \ +ACTION=release-to-edge \ +VERSION=1.23.0 \ +KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \ +TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \ +SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \ +TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \ +GITHUB_USERNAME=foobar \ +GITHUB_TOKEN_1PASSWORD_PATH="Private/Github Token/password" \ +PUSH_TO_REMOTE=1 \ +./tools/tuf/releaser.sh +``` + +#### Releasing osquery `5.12.1` to `edge` + +```sh +AWS_PROFILE=tuf \ +TUF_DIRECTORY=/Users/luk/tuf.fleetctl.com \ +COMPONENT=osqueryd \ +ACTION=release-to-edge \ +VERSION=5.12.1 \ +KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \ +TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \ +SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \ +TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \ +GITHUB_USERNAME=foobar \ +GITHUB_TOKEN_1PASSWORD_PATH="Private/Github Token/password" \ +PUSH_TO_REMOTE=1 \ +./tools/tuf/releaser.sh +``` + +### Promoting from `edge` to `stable` + +#### Promoting fleetd `1.23.0` from `edge` to `stable` + +```sh +AWS_PROFILE=tuf \ +TUF_DIRECTORY=/Users/foobar/tuf.fleetctl.com \ +COMPONENT=fleetd \ +ACTION=promote-edge-to-stable \ +VERSION=1.23.0 \ +KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \ +TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \ +SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \ +TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \ +GITHUB_USERNAME=foobar \ +GITHUB_TOKEN_1PASSWORD_PATH="Private/Github Token/password" \ +PUSH_TO_REMOTE=1 \ +./tools/tuf/releaser.sh +``` + +#### Promoting osqueryd `5.12.1` from `edge` to `stable` + +```sh +AWS_PROFILE=tuf \ +TUF_DIRECTORY=/Users/foobar/tuf.fleetctl.com \ +COMPONENT=osqueryd \ +ACTION=promote-edge-to-stable \ +VERSION=5.12.1 \ +KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \ +TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \ +SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \ +TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \ +GITHUB_USERNAME=foobar \ +GITHUB_TOKEN_1PASSWORD_PATH="Private/Github Token/password" \ +PUSH_TO_REMOTE=1 \ +./tools/tuf/releaser.sh +``` + +#### Releasing `swiftDialog` to `stable` + +> `releaser.sh` doesn't support `swiftDialog` yet. +> macOS only component + +The `swiftDialog` executable can be generated from a macOS host by running: +```sh +make swift-dialog-app-tar-gz version=2.2.1 build=4591 out-path=. +``` +```sh +fleetctl updates add --target /path/to/macos/swiftDialog.app.tar.gz --platform macos --name swiftDialog --version 2.2.1 -t edge +``` + +#### Releasing `nudge` to `stable` + +> `releaser.sh` doesn't support `nudge` yet. +> macOS only component + +The `nudge` executable can be generated from a macOS host by running: +```sh +make nudge-app-tar-gz version=1.1.10.81462 out-path=. +``` +```sh +fleetctl updates add --target /path/to/macos/nudge.app.tar.gz --platform macos --name nudge --version 1.1.10.81462 -t edge +``` + +## Testing and improving the script + +- You can specify `GIT_REPOSITORY_DIRECTORY` to set a separate path for the Fleet repository (it uses the current by default). +This is sometimes necessary if the tooling the script uses is not present in the branch we are trying to release from. +```sh +git clone git@github.com:fleetdm/fleet.git +GIT_REPOSITORY_DIRECTORY= +``` + +- If the PR and orbit tag were already generated but you need to run the script again you can set `SKIP_PR_AND_TAG_PUSH=1` to skip that part. + +- While developing you can run with `PUSH_TO_REMOTE=0` to prevent pushing invalid metadata/components to the production repository. + +## TODOs to improve releaser.sh + +- Create the pull requests automatically using `gh` or the Github API. +- Support releasing `nudge` and `swiftDialog`. + +## Troubleshooting + +### Removing Unused Targets + +If you've inadvertently published a target that is no longer in use, follow these steps to remove it. + +> Before performing any actions on Fleet's TUF repository you must: +> 1. Make sure your local copy of the repository is up-to-date. See [Syncing Fleet's TUF repository](#syncing-fleets-tuf-repository). +> 2. Create a local backup in case we mess up with the repository: +> ```sh +> mkdir ~/tuf.fleetctl.com/backup +> cp -r ~/tuf.fleetctl.com ~/tuf.fleetctl.com-backup +> ``` + +1. You'll need the [`go-tuf`](https://github.com/theupdateframework/go-tuf) binary. The removal operations aren't integrated into `fleetctl` at the moment. +2. Use `tuf remove` to remove the target and update `targets.json`. Substitute `desktop/windows/stable/desktop.exe` with the target you intend to delete. +```sh +tuf remove desktop/windows/stable/desktop.exe +``` +3. Snapshot, timestamp, and commit the changes. +```sh +tuf snapshot +tuf timestamp +tuf commit +``` +4. Run the following command to generate a timestamp that expires in two weeks (otherwise the default expiration when using `go-tuf` commands is 1 day) +```sh +fleetctl updates timestamp +``` +5. Confirm that the version of the local `timestamp.json` file is more recent than that of the remote server. +6. Verify the changes that will be synced by running a dry sync. Include the `--delete` flag as you're removing targets. +```sh +aws s3 sync ./repository s3://fleet-tuf-repo --delete --dryrun +``` +7. `diff` the local `targets.json` file with its remote version. +8. To upload the changes, perform a sync without the `--dryrun`: +```sh +aws s3 sync ./repository s3://fleet-tuf-repo --delete +``` + +### Invalid timestamp.json version + +The following issue was solved by resigning the timestamp metadata `fleetctl updates timestamp` (executed three times to increase the version to `4175`) +```sh +2022-08-23T13:44:48-03:00 INF update failed error="update metadata: update metadata: tuf: failed to decode timestamp.json: version 4172 is lower than current version 4174" +2022-08-23T13:59:48-03:00 INF update failed error="update metadata: update metadata: tuf: failed to decode timestamp.json: version 4172 is lower than current version 4174" +``` diff --git a/tools/tuf/download-artifacts/download-artifacts.go b/tools/tuf/download-artifacts/download-artifacts.go index e0df2e3dec..2f0d333cff 100644 --- a/tools/tuf/download-artifacts/download-artifacts.go +++ b/tools/tuf/download-artifacts/download-artifacts.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/fleetdm/fleet/v4/orbit/pkg/constant" "github.com/fleetdm/fleet/v4/pkg/fleethttp" @@ -37,6 +38,7 @@ func orbitCommand() *cli.Command { outputDirectory string githubUsername string githubAPIToken string + retry bool ) return &cli.Command{ Name: "orbit", @@ -70,13 +72,19 @@ func orbitCommand() *cli.Command { Destination: &githubAPIToken, Usage: "Github API token (https://github.com/settings/tokens)", }, + &cli.BoolFlag{ + Name: "retry", + EnvVars: []string{"DOWNLOAD_ARTIFACTS_RETRY"}, + Destination: &retry, + Usage: "Whether to retry if the artifact doesn't exist yet", + }, }, Action: func(c *cli.Context) error { return downloadComponents("goreleaser-orbit.yaml", gitTag, map[string]string{ "macos": "orbit-macos", "linux": "orbit-linux", "windows": "orbit-windows", - }, outputDirectory, githubUsername, githubAPIToken) + }, outputDirectory, githubUsername, githubAPIToken, retry) }, } } @@ -87,6 +95,7 @@ func desktopCommand() *cli.Command { outputDirectory string githubUsername string githubAPIToken string + retry bool ) return &cli.Command{ Name: "desktop", @@ -120,13 +129,19 @@ func desktopCommand() *cli.Command { Destination: &githubAPIToken, Usage: "Github API token (https://github.com/settings/tokens)", }, + &cli.BoolFlag{ + Name: "retry", + EnvVars: []string{"DOWNLOAD_ARTIFACTS_RETRY"}, + Destination: &retry, + Usage: "Whether to retry if the artifact doesn't exist yet", + }, }, Action: func(c *cli.Context) error { return downloadComponents("generate-desktop-targets.yml", gitBranch, map[string]string{ "macos": "desktop.app.tar.gz", "linux": "desktop.tar.gz", "windows": "fleet-desktop.exe", - }, outputDirectory, githubUsername, githubAPIToken) + }, outputDirectory, githubUsername, githubAPIToken, retry) }, } } @@ -231,7 +246,7 @@ func extractZipFile(archiveReader *zip.File, destPath string) error { return nil } -func downloadComponents(workflowName string, headBranch string, artifactNames map[string]string, outputDirectory string, githubUsername string, githubAPIToken string) error { +func downloadComponents(workflowName string, headBranch string, artifactNames map[string]string, outputDirectory string, githubUsername string, githubAPIToken string, retry bool) error { if err := os.RemoveAll(outputDirectory); err != nil { return err } @@ -241,40 +256,55 @@ func downloadComponents(workflowName string, headBranch string, artifactNames ma } } ctx := context.Background() - gc := github.NewClient(fleethttp.NewClient()) - workflow, _, err := gc.Actions.GetWorkflowByFileName(ctx, "fleetdm", "fleet", workflowName) - if err != nil { - return err - } - workflowRuns, _, err := gc.Actions.ListWorkflowRunsByID(ctx, "fleetdm", "fleet", *workflow.ID, nil) - if err != nil { - return err - } var workflowRun *github.WorkflowRun - for _, wr := range workflowRuns.WorkflowRuns { - if headBranch == *wr.HeadBranch { - workflowRun = wr + gc := github.NewClient(fleethttp.NewClient()) + for { + workflow, _, err := gc.Actions.GetWorkflowByFileName(ctx, "fleetdm", "fleet", workflowName) + if err != nil { + return err + } + workflowRuns, _, err := gc.Actions.ListWorkflowRunsByID(ctx, "fleetdm", "fleet", *workflow.ID, nil) + if err != nil { + return err + } + for _, wr := range workflowRuns.WorkflowRuns { + if headBranch == *wr.HeadBranch { + workflowRun = wr + break + } + } + if workflowRun != nil || !retry { break } + fmt.Printf("Workflow not available yet, it might be queued, retrying in 60s...\n") + time.Sleep(60 * time.Second) } if workflowRun == nil { return fmt.Errorf("workflow with tag %s not found", headBranch) } - artifactList, _, err := gc.Actions.ListWorkflowRunArtifacts(ctx, "fleetdm", "fleet", *workflowRun.ID, nil) - if err != nil { - return err - } - urls := make(map[string]string) - for _, artifact := range artifactList.Artifacts { - if *artifact.Name == artifactNames["linux"] { - urls["linux"] = *artifact.ArchiveDownloadURL - } else if *artifact.Name == artifactNames["macos"] { - urls["macos"] = *artifact.ArchiveDownloadURL - } else if *artifact.Name == artifactNames["windows"] { - urls["windows"] = *artifact.ArchiveDownloadURL - } else { - return fmt.Errorf("unknown artifact name: %s", *artifact.Name) + var urls map[string]string + for { + artifactList, _, err := gc.Actions.ListWorkflowRunArtifacts(ctx, "fleetdm", "fleet", *workflowRun.ID, nil) + if err != nil { + return err } + urls = make(map[string]string) + for _, artifact := range artifactList.Artifacts { + if *artifact.Name == artifactNames["linux"] { + urls["linux"] = *artifact.ArchiveDownloadURL + } else if *artifact.Name == artifactNames["macos"] { + urls["macos"] = *artifact.ArchiveDownloadURL + } else if *artifact.Name == artifactNames["windows"] { + urls["windows"] = *artifact.ArchiveDownloadURL + } else { + return fmt.Errorf("unknown artifact name: %s", *artifact.Name) + } + } + if len(urls) == 3 || !retry { + break + } + fmt.Printf("All artifacts are not available yet, the workflow might still be running, retrying in 60s...\n") + time.Sleep(60 * time.Second) } if len(urls) != 3 { return fmt.Errorf("missing some artifact: %+v", urls) @@ -295,6 +325,7 @@ func osquerydCommand() *cli.Command { outputDirectory string githubUsername string githubAPIToken string + retry bool ) return &cli.Command{ Name: "osqueryd", @@ -328,13 +359,19 @@ func osquerydCommand() *cli.Command { Destination: &githubAPIToken, Usage: "Github API token (https://github.com/settings/tokens)", }, + &cli.BoolFlag{ + Name: "retry", + EnvVars: []string{"DOWNLOAD_ARTIFACTS_RETRY"}, + Destination: &retry, + Usage: "Whether to retry if the artifact doesn't exist yet", + }, }, Action: func(c *cli.Context) error { return downloadComponents("generate-osqueryd-targets.yml", gitBranch, map[string]string{ "macos": "osqueryd.app.tar.gz", "linux": "osqueryd", "windows": "osqueryd.exe", - }, outputDirectory, githubUsername, githubAPIToken) + }, outputDirectory, githubUsername, githubAPIToken, retry) }, } } diff --git a/tools/tuf/promote_edge_to_stable.sh b/tools/tuf/promote_edge_to_stable.sh deleted file mode 100755 index 32829deff1..0000000000 --- a/tools/tuf/promote_edge_to_stable.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - - -component=$1 -version=$2 - -if [[ -z $component || -z $version ]]; then - echo "Usage: $0 " - exit 1 -fi - -if [[ ! -d "./repository" ]]; then - echo "Directory ./repository doesn't exist" - exit 1 -fi - -version_parts=(${version//./ }) -major=${version_parts[0]} -minor=${version_parts[1]} - -echo "Promoting $component from edge to stable, version='$version'" -echo "Press any key to continue..." -read -s -n 1 - -case $1 in - orbit) - fleetctl updates add --target ./repository/targets/orbit/macos/edge/orbit --platform macos --name orbit --version $version -t $major.$minor -t $major -t stable - fleetctl updates add --target ./repository/targets/orbit/linux/edge/orbit --platform linux --name orbit --version $version -t $major.$minor -t $major -t stable - fleetctl updates add --target ./repository/targets/orbit/windows/edge/orbit.exe --platform windows --name orbit --version $version -t $major.$minor -t $major -t stable - ;; - desktop) - fleetctl updates add --target ./repository/targets/desktop/macos/edge/desktop.app.tar.gz --platform macos --name desktop --version $version -t $major.$minor -t $major -t stable - fleetctl updates add --target ./repository/targets/desktop/linux/edge/desktop.tar.gz --platform linux --name desktop --version $version -t $major.$minor -t $major -t stable - fleetctl updates add --target ./repository/targets/desktop/windows/edge/fleet-desktop.exe --platform windows --name desktop --version $version -t $major.$minor -t $major -t stable - ;; - osqueryd) - fleetctl updates add --target ./repository/targets/osqueryd/macos-app/edge/osqueryd.app.tar.gz --platform macos-app --name osqueryd --version $version -t $major.$minor -t $major -t stable - fleetctl updates add --target ./repository/targets/osqueryd/linux/edge/osqueryd --platform linux --name osqueryd --version $version -t $major.$minor -t $major -t stable - fleetctl updates add --target ./repository/targets/osqueryd/windows/edge/osqueryd.exe --platform windows --name osqueryd --version $version -t $major.$minor -t $major -t stable - ;; - nudge) - fleetctl updates add --target ./repository/targets/nudge/macos/edge/nudge.app.tar.gz --platform macos --name nudge --version $version -t stable - ;; - swiftDialog) - fleetctl updates add --target ./repository/targets/swiftDialog/macos/edge/swiftDialog.app.tar.gz --platform macos --name swiftDialog --version $version -t stable - ;; - *) - echo Unknown component $1 - exit 1 - ;; -esac diff --git a/tools/tuf/releaser.sh b/tools/tuf/releaser.sh new file mode 100755 index 0000000000..527314bd01 --- /dev/null +++ b/tools/tuf/releaser.sh @@ -0,0 +1,305 @@ +#!/bin/bash + +# +# For usage documentation, see the README.md. +# + +set -e + +# +# Input environment variables: +# +# AWS_PROFILE +# TUF_DIRECTORY +# COMPONENT +# ACTION +# VERSION +# KEYS_SOURCE_DIRECTORY +# TARGETS_PASSPHRASE_1PASSWORD_PATH +# SNAPSHOT_PASSPHRASE_1PASSWORD_PATH +# TIMESTAMP_PASSPHRASE_1PASSWORD_PATH +# GITHUB_USERNAME +# GITHUB_TOKEN_1PASSWORD_PATH +# SKIP_PR_AND_TAG_PUSH +# + +# +# Dev environment variables: +# PUSH_TO_REMOTE +# GIT_REPOSITORY_DIRECTORY +# + +clean_up () { + echo "Cleaning up directories..." + + # Make sure (best effort) to remove the keys after we are done. + rm -rf "$KEYS_DIRECTORY" + rm -rf "$ARTIFACTS_DOWNLOAD_DIRECTORY" + rm -rf "$GO_TOOLS_DIRECTORY" + ARG=$? + exit $ARG +} + +setup () { + echo "Running setup..." + + GO_TOOLS_DIRECTORY=$(mktemp -d) + ARTIFACTS_DOWNLOAD_DIRECTORY=$(mktemp -d) + SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + REPOSITORY_DIRECTORY=$TUF_DIRECTORY/repository + STAGED_DIRECTORY=$TUF_DIRECTORY/staged + KEYS_DIRECTORY=$TUF_DIRECTORY/keys + if [[ -z $GIT_REPOSITORY_DIRECTORY ]]; then + GIT_REPOSITORY_DIRECTORY=$( realpath "$SCRIPT_DIR/../.." ) + fi + + mkdir -p "$REPOSITORY_DIRECTORY" + mkdir -p "$STAGED_DIRECTORY" + cp -r "$KEYS_SOURCE_DIRECTORY" "$KEYS_DIRECTORY" + + if ! aws sts get-caller-identity &> /dev/null; then + aws sso login + prompt "AWS SSO login was successful, press any key to continue..." + fi + + GITHUB_TOKEN=$(op read "op://$GITHUB_TOKEN_1PASSWORD_PATH") + + # These need to be exported for use by `fleetctl updates` commands. + FLEET_TARGETS_PASSPHRASE=$(op read "op://$TARGETS_PASSPHRASE_1PASSWORD_PATH") + export FLEET_TARGETS_PASSPHRASE + FLEET_SNAPSHOT_PASSPHRASE=$(op read "op://$SNAPSHOT_PASSPHRASE_1PASSWORD_PATH") + export FLEET_SNAPSHOT_PASSPHRASE + FLEET_TIMESTAMP_PASSPHRASE=$(op read "op://$TIMESTAMP_PASSPHRASE_1PASSWORD_PATH") + export FLEET_TIMESTAMP_PASSPHRASE + + go build -o "$GO_TOOLS_DIRECTORY/replace" "$SCRIPT_DIR/../../tools/tuf/replace" + go build -o "$GO_TOOLS_DIRECTORY/download-artifacts" "$SCRIPT_DIR/../../tools/tuf/download-artifacts" +} + +pull_from_remote () { + echo "Pulling repository from tuf.fleetctl.com... (--dryrun first)" + aws s3 sync s3://fleet-tuf-repo "$REPOSITORY_DIRECTORY" --exact-timestamps --dryrun + prompt "If the --dryrun looks good, press any key to continue... (no output means nothing to update)" + aws s3 sync s3://fleet-tuf-repo "$REPOSITORY_DIRECTORY" --exact-timestamps +} + +promote_component_edge_to_stable () { + component_name=$1 + component_version=$2 + + version_parts=("${component_version//./ }") + major=${version_parts[0]} + minor=${version_parts[1]} + + case $component_name in + orbit) + fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/macos/edge/orbit" --platform macos --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable + fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/linux/edge/orbit" --platform linux --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable + fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/windows/edge/orbit.exe" --platform windows --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable + ;; + desktop) + fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/macos/edge/desktop.app.tar.gz" --platform macos --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable + fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/linux/edge/desktop.tar.gz" --platform linux --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable + fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/windows/edge/fleet-desktop.exe" --platform windows --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable + ;; + osqueryd) + fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/macos-app/edge/osqueryd.app.tar.gz" --platform macos-app --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable + fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/linux/edge/osqueryd" --platform linux --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable + fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/windows/edge/osqueryd.exe" --platform windows --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable + ;; + *) + echo "Unknown component $component_name" + exit 1 + ;; + esac +} + +promote_edge_to_stable () { + cd "$REPOSITORY_DIRECTORY" + if [[ $COMPONENT == "fleetd" ]]; then + echo "Promoting fleetd from edge to stable..." + promote_component_edge_to_stable orbit "$VERSION" + promote_component_edge_to_stable desktop "$VERSION" + elif [[ $COMPONENT == "osqueryd" ]]; then + echo "Promoting osqueryd from edge to stable..." + promote_component_edge_to_stable osqueryd "$VERSION" + else + echo "Unsupported component: $COMPONENT" + exit 1 + fi +} + +release_fleetd_to_edge () { + echo "Releasing fleetd to edge..." + BRANCH_NAME="release-fleetd-v$VERSION" + ORBIT_TAG="orbit-v$VERSION" + if [[ "$SKIP_PR_AND_TAG_PUSH" != "1" ]]; then + prompt "A PR for bumping the fleetd version will be created to trigger a Github Action that will build 'Fleet Desktop'. Press any key to continue..." + pushd "$GIT_REPOSITORY_DIRECTORY" + git checkout -b "$BRANCH_NAME" + make changelog-orbit version="$VERSION" + ORBIT_CHANGELOG=orbit/CHANGELOG.md + "$GO_TOOLS_DIRECTORY/replace" .github/workflows/generate-desktop-targets.yml "FLEET_DESKTOP_VERSION: .+\n" "FLEET_DESKTOP_VERSION: $VERSION\n" + git add .github/workflows/generate-desktop-targets.yml "$ORBIT_CHANGELOG" + git commit -m "Release fleetd $VERSION" + git push origin "$BRANCH_NAME" + open "https://github.com/fleetdm/fleet/pull/new/$BRANCH_NAME" + prompt "Press any key to continue after the PR is created..." + prompt "A 'git tag' will be created to trigger a Github Action to build orbit, press any key to continue..." + git tag "$ORBIT_TAG" + git push origin "$ORBIT_TAG" + popd + fi + DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY="$ARTIFACTS_DOWNLOAD_DIRECTORY/desktop" + mkdir -p "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY" + "$GO_TOOLS_DIRECTORY/download-artifacts" desktop \ + --git-branch "$BRANCH_NAME" \ + --output-directory "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY" \ + --github-username "$GITHUB_USERNAME" --github-api-token "$GITHUB_TOKEN" \ + --retry + ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY="$ARTIFACTS_DOWNLOAD_DIRECTORY/orbit" + mkdir -p "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY" + "$GO_TOOLS_DIRECTORY/download-artifacts" orbit \ + --git-tag "$ORBIT_TAG" \ + --output-directory "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY" \ + --github-username "$GITHUB_USERNAME" --github-api-token "$GITHUB_TOKEN" \ + --retry + pushd "$TUF_DIRECTORY" + fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/macos/orbit" --platform macos --name orbit --version "$VERSION" -t edge + fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/linux/orbit" --platform linux --name orbit --version "$VERSION" -t edge + fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/windows/orbit.exe" --platform windows --name orbit --version "$VERSION" -t edge + fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/macos/desktop.app.tar.gz" --platform macos --name desktop --version "$VERSION" -t edge + fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/linux/desktop.tar.gz" --platform linux --name desktop --version "$VERSION" -t edge + fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/windows/fleet-desktop.exe" --platform windows --name desktop --version "$VERSION" -t edge + popd +} + +release_osqueryd_to_edge () { + echo "Releasing osqueryd to edge..." + prompt "A branch and PR for bumping the osquery version will be created. Press any key to continue..." + BRANCH_NAME=release-osqueryd-v$VERSION + if [[ "$SKIP_PR_AND_TAG_PUSH" != "1" ]]; then + pushd "$GIT_REPOSITORY_DIRECTORY" + git checkout -b "$BRANCH_NAME" + "$GO_TOOLS_DIRECTORY/replace" .github/workflows/generate-osqueryd-targets.yml "OSQUERY_VERSION: .+\n" "OSQUERY_VERSION: $VERSION\n" + git add .github/workflows/generate-osqueryd-targets.yml + git commit -m "Bump osqueryd version to $VERSION" + git push origin "$BRANCH_NAME" + open "https://github.com/fleetdm/fleet/pull/new/$BRANCH_NAME" + prompt "Press any key to continue after the PR is created..." + popd + fi + OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY="$ARTIFACTS_DOWNLOAD_DIRECTORY/osqueryd" + mkdir -p "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY" + "$GO_TOOLS_DIRECTORY/download-artifacts" osqueryd \ + --git-branch "$BRANCH_NAME" \ + --output-directory "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY" \ + --github-username "$GITHUB_USERNAME" \ + --github-api-token "$GITHUB_TOKEN" \ + --retry + pushd "$TUF_DIRECTORY" + fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/macos/osqueryd.app.tar.gz" --platform macos-app --name osqueryd --version "$VERSION" -t edge + fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/linux/osqueryd" --platform linux --name osqueryd --version "$VERSION" -t edge + fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/windows/osqueryd.exe" --platform windows --name osqueryd --version "$VERSION" -t edge + popd +} + +release_to_edge () { + if [[ $COMPONENT == "fleetd" ]]; then + release_fleetd_to_edge + elif [[ $COMPONENT == "osqueryd" ]]; then + release_osqueryd_to_edge + else + echo "Unsupported component: $COMPONENT" + exit 1 + fi +} + +push_to_remote () { + echo "Running --dryrun push of repository to tuf.fleetctl.com..." + aws s3 sync "$REPOSITORY_DIRECTORY" s3://fleet-tuf-repo --dryrun + if [[ $PUSH_TO_REMOTE == "1" ]]; then + echo "WARNING: This step will push the release to tuf.fleetctl.com (production)..." + prompt "If the --dryrun looks good, press any key to continue..." + aws s3 sync "$REPOSITORY_DIRECTORY" s3://fleet-tuf-repo + echo "Release has been pushed!" + echo "NOTE: You might see some clients failing to upgrade due to some sha256 mismatches." + echo "These temporary failures are expected because it takes some time for caches to be invalidated (these errors should go away after ~15-30 minutes)." + else + echo "PUSH_TO_REMOTE not set to 1, so not pushing." + fi +} + +prompt () { + printf "%s\n" "$1" + read -r -s -n 1 +} + +setup_to_become_publisher () { + echo "Running setup to become publisher..." + + REPOSITORY_DIRECTORY=$TUF_DIRECTORY/repository + STAGED_DIRECTORY=$TUF_DIRECTORY/staged + KEYS_DIRECTORY=$TUF_DIRECTORY/keys + mkdir -p "$REPOSITORY_DIRECTORY" + mkdir -p "$STAGED_DIRECTORY" + mkdir -p "$KEYS_DIRECTORY" + if ! aws sts get-caller-identity &> /dev/null; then + aws sso login + prompt "AWS SSO login was successful, press any key to continue..." + fi + # These need to be exported for use by `tuf` commands. + FLEET_TARGETS_PASSPHRASE=$(op read "op://$TARGETS_PASSPHRASE_1PASSWORD_PATH") + export TUF_TARGETS_PASSPHRASE=$FLEET_TARGETS_PASSPHRASE + FLEET_SNAPSHOT_PASSPHRASE=$(op read "op://$SNAPSHOT_PASSPHRASE_1PASSWORD_PATH") + export TUF_SNAPSHOT_PASSPHRASE=$FLEET_SNAPSHOT_PASSPHRASE + FLEET_TIMESTAMP_PASSPHRASE=$(op read "op://$TIMESTAMP_PASSPHRASE_1PASSWORD_PATH") + export TUF_TIMESTAMP_PASSPHRASE=$FLEET_TIMESTAMP_PASSPHRASE +} + +if [[ $ACTION == "generate-signing-keys" ]]; then + setup_to_become_publisher + pull_from_remote + cd "$TUF_DIRECTORY" + tuf gen-key targets && echo + tuf gen-key snapshot && echo + tuf gen-key timestamp && echo + echo "Keys have been generated, now do the following actions:" + echo "- Share '$TUF_DIRECTORY/staged/root.json' with Fleet member with the 'root' role, who will sign with its root key and push it to the remote repository." + echo "- Store the '$TUF_DIRECTORY/keys' folder (that contains the encrypted keys) on a USB flash drive that you will ONLY use for releasing fleetd updates." + exit 0 +fi + +print_reminder () { + if [[ $ACTION == "release-to-edge" ]]; then + if [[ $COMPONENT == "fleetd" ]]; then + prompt "Make sure to install fleetd with '--orbit-channel=edge --desktop-channel=edge' on a Linux, Windows and macOS VM. (To smoke test the release.) Press any key to continue..." + elif [[ $COMPONENT == "osqueryd" ]]; then + prompt "Make sure to install fleetd with '--osqueryd-channel=edge' on a Linux, Windows and macOS VM. (To smoke test the release.) Press any key to continue..." + fi + elif [[ $ACTION == "promote-edge-to-stable" ]]; then + if [[ $COMPONENT == "fleetd" ]]; then + prompt "Make sure to install fleetd with '--orbit-channel=stable --desktop-channel=stable' on a Linux, Windows and macOS VM. (To smoke test the release.) Press any key to continue..." + elif [[ $COMPONENT == "osqueryd" ]]; then + prompt "Make sure to install fleetd with '--osqueryd-channel=stable' on a Linux, Windows and macOS VM. (To smoke test the release.) Press any key to continue..." + fi + else + echo "Unsupported action: $ACTION" + fi +} + +trap clean_up EXIT +print_reminder +setup +pull_from_remote + +if [[ $ACTION == "release-to-edge" ]]; then + release_to_edge +elif [[ $ACTION == "promote-edge-to-stable" ]]; then + promote_edge_to_stable +else + echo "Unsupported action: $ACTION" + exit 1 +fi + +push_to_remote \ No newline at end of file diff --git a/tools/tuf/replace/main.go b/tools/tuf/replace/main.go new file mode 100644 index 0000000000..ba742f09e7 --- /dev/null +++ b/tools/tuf/replace/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + "regexp" + "strings" +) + +// This tool was created to prevent issues between GNU's sed and OSX's sed. + +func main() { + inputPath := os.Args[1] + expression := os.Args[2] + replace := os.Args[3] + r := regexp.MustCompile(expression) + stat, err := os.Stat(inputPath) + if err != nil { + panic(err) + } + input, err := os.ReadFile(inputPath) + if err != nil { + panic(err) + } + if strings.HasSuffix(replace, `\n`) { + replace = strings.TrimSuffix(replace, `\n`) + "\n" + } + output := r.ReplaceAllString(string(input), replace) + if err := os.WriteFile(inputPath, []byte(output), stat.Mode()); err != nil { + panic(err) + } +} diff --git a/tools/tuf/test/create_repository.sh b/tools/tuf/test/create_repository.sh index 8772db5b37..9131852d5b 100755 --- a/tools/tuf/test/create_repository.sh +++ b/tools/tuf/test/create_repository.sh @@ -82,7 +82,7 @@ for system in $SYSTEMS; do ORBIT_BINARY_PATH=$orbit_target \ go run ./orbit/tools/build/build.go else - GOOS=$goose_value GOARCH=$goarch_value go build -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=42" -o $orbit_target ./orbit/cmd/orbit + CGO_ENABLED=0 GOOS=$goose_value GOARCH=$goarch_value go build -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=42" -o $orbit_target ./orbit/cmd/orbit fi ./build/fleetctl updates add \ From 3ca567f46716618981531e056b025a41c3fe91bd Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:51:55 -0500 Subject: [PATCH 29/83] Fix crazy formatting (#18162) ![image](https://github.com/fleetdm/fleet/assets/108141731/db51a115-01b5-4f38-bc88-775008554145) --- handbook/company/leadership.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/handbook/company/leadership.md b/handbook/company/leadership.md index 7e333b583e..b76d42083b 100644 --- a/handbook/company/leadership.md +++ b/handbook/company/leadership.md @@ -281,7 +281,7 @@ Fleet maintains a list of companies with whom Fleet has do-not-solicit terms tha Fleet is unable to hire team members in some countries. See [this internal document](https://docs.google.com/document/d/1jHHJqShIyvlVwzx1C-FB9GC74Di_Rfdgmhpai1SPC0g/edit) for the list. #### Interviewing -> TODO: Rewrite this section for the hiring manager as our audience. + We're glad you're interested in joining the team! Here are some of the things you can anticipate throughout this process: @@ -302,7 +302,7 @@ Here are the steps hiring managers follow to get an offer out to a candidate: 1. **Call references:** Before proceeding, make sure you have 2-5+ references. Ask the candidate for at least 2-5+ references and contact each reference in parallel using the instructions in [Fleet's reference check template](https://docs.google.com/document/d/1LMOUkLJlAohuFykdgxTPL0RjAQxWkypzEYP_AT-bUAw/edit?usp=sharing). Be respectful and keep these calls very short. 2. **Add to team database:** Update the [Fleeties](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0) doc to accurately reflect the candidate's: - Start date - > _**Tip:** No need to check with the candidate if you haven't already. Just guess. First Mondays tend to make good start dates. When hiring an international employee, Pilot.co recommends starting the hiring process a month before the new employee's start date._ +> _**Tip:** No need to check with the candidate if you haven't already. Just guess. First Mondays tend to make good start dates. When hiring an international employee, Pilot.co recommends starting the hiring process a month before the new employee's start date._ - First and last name - Preferred pronoun _("them", "her", or "him")_ - LinkedIn URL _(If the fleetie does not have a LinkedIn account, enter `N/A`)_ @@ -311,9 +311,8 @@ Here are the steps hiring managers follow to get an offer out to a candidate: 4. **Confirm intent to offer:** Compile feedback about the candidate into a single document and share that document (the "interview packet") with the Head of Business Operations via Google Drive. _This will be interpreted as a signal that you are ready for them to make an offer to this candidate._ - _Compile feedback into a single doc:_ Include feedback from interviews, reference checks, and challenge submissions. Include any other notes you can think of offhand, and embed links to any supporting documents that were impactful in your final decision-making, such as portfolios or challenge submissions. - _Share_ this single document with the Head of Business Operations via email. - - Share only _one, single Google Doc, please_; with a short, formulaic name that's easy to understand in an instant from just an email subject line. For example, you could title it: - >Why hire Jane Doe ("Train Conductor") - 2023-03-21 - - When the Head of Business Operations receives this doc shared doc in their email with the compiled feedback about the candidate, they will understand that to mean that it is time for Fleet to make an offer to the candidate. + - Share only _one, single Google Doc, please_; with a short, formulaic name that's easy to understand in an instant from just an email subject line (e.g. "_Why hire Jane Doe ("Train Conductor") - 2023-03-21_"). + - When the Head of Business Operations receives this doc shared doc in their email with the compiled feedback about the candidate, they will understand that to mean that it is time for Fleet to make an offer to the candidate. #### Making an offer After receiving the interview packet, the Head of Business Operations uses the following steps to make an offer: From f4d6136236c4cf6d384bf6e0ca38cf5bc2fd5fbb Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 9 Apr 2024 18:12:51 -0400 Subject: [PATCH 30/83] Ignore pending DDM profiles when waiting to release a device (#18159) #18160 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. - [x] Manual QA for all new/changed functionality --- .../18160-fix-release-device-with-pending-ddm-profiles | 1 + server/worker/apple_mdm.go | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 changes/18160-fix-release-device-with-pending-ddm-profiles diff --git a/changes/18160-fix-release-device-with-pending-ddm-profiles b/changes/18160-fix-release-device-with-pending-ddm-profiles new file mode 100644 index 0000000000..d780f184f3 --- /dev/null +++ b/changes/18160-fix-release-device-with-pending-ddm-profiles @@ -0,0 +1 @@ +* Fixed an issue with automatic release of the device after setup when a DDM profile is pending. diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go index 01624d85d0..0537af05e8 100644 --- a/server/worker/apple_mdm.go +++ b/server/worker/apple_mdm.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" @@ -238,6 +239,13 @@ func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArg return ctxerr.Wrap(ctx, err, "failed to get host MDM profiles") } for _, prof := range profs { + // NOTE: DDM profiles (declarations) are ignored because while a device is + // awaiting to be released, it cannot process a DDM session (at least + // that's what we noticed during testing). + if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) { + continue + } + // if it has any pending profiles, then its profiles are not done being // delivered (installed or removed). if prof.Status == nil || *prof.Status == fleet.MDMDeliveryPending { From b1eb1ae4565e2ea1c8da0b7aaef256e830fa1ceb Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 9 Apr 2024 17:32:56 -0500 Subject: [PATCH 31/83] Website: remove script tag (#18163) Related to: #18153 Changes: - Removed the Snitcher script tag from the Fleet website (It will be added back via GTM after we are sure it is not causing issues for website users.) --- website/views/layouts/layout.ejs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/website/views/layouts/layout.ejs b/website/views/layouts/layout.ejs index a40637b66c..9825336ecc 100644 --- a/website/views/layouts/layout.ejs +++ b/website/views/layouts/layout.ejs @@ -102,14 +102,6 @@ window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o - <% /* Snitcher analytics code */ %> - <% /* HubSpot Embed Code */ %> <% } From d2d1b8f0cab69a89c260f6f3d41d607b2dc254cc Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 9 Apr 2024 18:02:26 -0500 Subject: [PATCH 32/83] Website: Update styles for "tip" block quotes in the handbook (#18166) Closes: #18161 Changes: - Updated `basic-handbook.less` to fix a style issue with "tip" blockquotes --- website/assets/styles/pages/handbook/basic-handbook.less | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/website/assets/styles/pages/handbook/basic-handbook.less b/website/assets/styles/pages/handbook/basic-handbook.less index 0cd2959a2d..941231abb6 100644 --- a/website/assets/styles/pages/handbook/basic-handbook.less +++ b/website/assets/styles/pages/handbook/basic-handbook.less @@ -402,12 +402,9 @@ padding: 16px; border-radius: 8px; display: flex; - div.d-block { - margin-left: 12px; - } img { display: flex; - margin-top: 4px; + margin: 4px 12px 0 0; height: 16px; width: 16px; padding: 0px; From e99eaf6cd31d8e5e65c3af4715380c702bbb0942 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:48:46 -0400 Subject: [PATCH 33/83] Fleet UI: Add advanced setting to set expiry window for activity log (#17989) --- changes/16989-ui-to-delete-old-activities | 1 + frontend/__mocks__/configMock.ts | 4 + frontend/interfaces/config.ts | 45 ++----- .../cards/Advanced/Advanced.tsx | 114 ++++++++++++++---- .../OrgSettingsPage/cards/Agents/Agents.tsx | 18 ++- .../cards/FleetDesktop/FleetDesktop.tsx | 21 ++-- .../GlobalHostStatusWebhook.tsx | 41 ++++--- .../admin/OrgSettingsPage/cards/Info/Info.tsx | 29 +++-- .../admin/OrgSettingsPage/cards/Smtp/Smtp.tsx | 50 +++++--- .../admin/OrgSettingsPage/cards/Sso/Sso.tsx | 54 +++++---- .../cards/Statistics/Statistics.tsx | 14 ++- .../cards/WebAddress/WebAddress.tsx | 28 +++-- .../admin/OrgSettingsPage/cards/constants.ts | 26 +--- frontend/utilities/constants.tsx | 6 + 14 files changed, 271 insertions(+), 180 deletions(-) create mode 100644 changes/16989-ui-to-delete-old-activities diff --git a/changes/16989-ui-to-delete-old-activities b/changes/16989-ui-to-delete-old-activities new file mode 100644 index 0000000000..6897b0212a --- /dev/null +++ b/changes/16989-ui-to-delete-old-activities @@ -0,0 +1 @@ +- Add advanced setting to set expiry window for activity log diff --git a/frontend/__mocks__/configMock.ts b/frontend/__mocks__/configMock.ts index 5ede26b805..af7bd63524 100644 --- a/frontend/__mocks__/configMock.ts +++ b/frontend/__mocks__/configMock.ts @@ -46,6 +46,10 @@ const DEFAULT_CONFIG_MOCK: IConfig = { host_expiry_enabled: false, host_expiry_window: 0, }, + activity_expiry_settings: { + activity_expiry_enabled: true, + activity_expiry_window: 90, + }, agent_options: "", license: { tier: "free", diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index f5f52a544d..bc8f0cb759 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -65,41 +65,6 @@ export interface IFleetDesktopSettings { transparency_url: string; } -export interface IConfigFormData { - smtpAuthenticationMethod: string; - smtpAuthenticationType: string; - domain: string; - smtpEnableSslTls: boolean; - enableStartTls: boolean; - serverUrl: string; - orgLogoUrl: string; - orgName: string; - smtpPassword: string; - smtpPort?: number; - smtpSenderAddress: string; - smtpServer: string; - smtpUsername: string; - verifySslCerts: boolean; - entityId: string; - idpImageUrl: string; - metadata: string; - metadataUrl: string; - idpName: string; - enableSso: boolean; - enableSsoIdpLogin: boolean; - enableSmtp: boolean; - enableHostExpiry: boolean; - hostExpiryWindow: number; - disableLiveQuery: boolean; - agentOptions: any; - enableHostStatusWebhook: boolean; - hostStatusWebhookDestinationUrl?: string; - hostStatusWebhookHostPercentage?: number; - hostStatusWebhookDaysCount?: number; - enableUsageStatistics: boolean; - transparencyUrl: string; -} - export interface IConfigFeatures { enable_host_users: boolean; enable_software_inventory: boolean; @@ -125,7 +90,7 @@ export interface IConfig { server_settings: IConfigServerSettings; smtp_settings?: { enable_smtp: boolean; - configured: boolean; + configured?: boolean; sender_address: string; server: string; port?: number; @@ -152,10 +117,14 @@ export interface IConfig { }; host_expiry_settings: { host_expiry_enabled: boolean; - host_expiry_window: number; + host_expiry_window?: number; + }; + activity_expiry_settings: { + activity_expiry_enabled: boolean; + activity_expiry_window?: number; }; features: IConfigFeatures; - agent_options: string; + agent_options: unknown; // Can pass empty object update_interval: { osquery_detail: number; osquery_policy: number; diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx index bd0704ce43..4ea4371521 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx @@ -1,31 +1,53 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import Button from "components/buttons/Button"; import Checkbox from "components/forms/fields/Checkbox"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import SectionHeader from "components/SectionHeader"; +// @ts-ignore +import Dropdown from "components/forms/fields/Dropdown"; -import { - IAppConfigFormProps, - IFormField, - IAppConfigFormErrors, -} from "../constants"; +import { ACTIVITY_EXPIRY_WINDOW_DROPDOWN_OPTIONS } from "utilities/constants"; +import { getCustomDropdownOptions } from "utilities/helpers"; + +import { IAppConfigFormProps, IFormField } from "../constants"; const baseClass = "app-config-form"; +interface IAdvancedConfigFormData { + domain: string; + verifySSLCerts: boolean; + enableStartTLS?: boolean; + enableHostExpiry: boolean; + hostExpiryWindow: number; + deleteActivities: boolean; + activityExpiryWindow: number; + disableLiveQuery: boolean; + disableScripts: boolean; + disableQueryReports: boolean; +} + +interface IAdvancedConfigFormErrors { + host_expiry_window?: string | null; +} + const Advanced = ({ appConfig, handleSubmit, isUpdatingSettings, }: IAppConfigFormProps): JSX.Element => { - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ domain: appConfig.smtp_settings?.domain || "", verifySSLCerts: appConfig.smtp_settings?.verify_ssl_certs || false, enableStartTLS: appConfig.smtp_settings?.enable_start_tls, enableHostExpiry: appConfig.host_expiry_settings.host_expiry_enabled || false, hostExpiryWindow: appConfig.host_expiry_settings.host_expiry_window || 0, + deleteActivities: + appConfig.activity_expiry_settings?.activity_expiry_enabled || false, + activityExpiryWindow: + appConfig.activity_expiry_settings?.activity_expiry_window || 30, disableLiveQuery: appConfig.server_settings.live_query_disabled || false, disableQueryReports: appConfig.server_settings.query_reports_disabled || false, @@ -38,20 +60,35 @@ const Advanced = ({ enableStartTLS, enableHostExpiry, hostExpiryWindow, + deleteActivities, + activityExpiryWindow, disableLiveQuery, disableScripts, disableQueryReports, } = formData; - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); - const handleInputChange = ({ name, value }: IFormField) => { + const activityExpiryWindowOptions = useMemo( + () => + getCustomDropdownOptions( + ACTIVITY_EXPIRY_WINDOW_DROPDOWN_OPTIONS, + activityExpiryWindow, + // it's safe to assume that frequency is a number + (frequency: number | string) => `${frequency as number} days` + ), + // intentionally leave activityExpiryWindow out of the dependencies, so that the custom + // options are maintained even if the user changes the frequency in the UI + [deleteActivities] + ); + + const onInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); }; useEffect(() => { // validate desired form fields - const errors: IAppConfigFormErrors = {}; + const errors: IAdvancedConfigFormErrors = {}; if (enableHostExpiry && (!hostExpiryWindow || hostExpiryWindow <= 0)) { errors.host_expiry_window = @@ -72,12 +109,13 @@ const Advanced = ({ enable_analytics: appConfig.server_settings.enable_analytics, query_reports_disabled: disableQueryReports, scripts_disabled: disableScripts, + deferred_save_host: appConfig.server_settings.deferred_save_host, }, smtp_settings: { enable_smtp: appConfig.smtp_settings?.enable_smtp || false, sender_address: appConfig.smtp_settings?.sender_address || "", server: appConfig.smtp_settings?.server || "", - port: Number(appConfig.smtp_settings?.port), + port: appConfig.smtp_settings?.port || undefined, authentication_type: appConfig.smtp_settings?.authentication_type || "", user_name: appConfig.smtp_settings?.user_name || "", password: appConfig.smtp_settings?.password || "", @@ -86,11 +124,15 @@ const Advanced = ({ appConfig.smtp_settings?.authentication_method || "", domain, verify_ssl_certs: verifySSLCerts, - enable_start_tls: enableStartTLS, + enable_start_tls: enableStartTLS || false, }, host_expiry_settings: { host_expiry_enabled: enableHostExpiry, - host_expiry_window: Number(hostExpiryWindow), + host_expiry_window: hostExpiryWindow || undefined, + }, + activity_expiry_settings: { + activity_expiry_enabled: deleteActivities, + activity_expiry_window: activityExpiryWindow || undefined, }, }; @@ -107,7 +149,7 @@ const Advanced = ({

)} + When enabled, allows automatic cleanup of audit logs older than + the number of days specified in the{" "} + Audit log retention window setting. + + (Default: Off) + + + } + > + Delete activities + + {deleteActivities && ( + + )} + { const { ADMIN_TEAMS } = paths; - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ agentOptions: agentOptionsToYaml(appConfig.agent_options), }); - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); const { agentOptions } = formData; @@ -37,7 +45,7 @@ const Agents = ({ }; const validateForm = () => { - const errors: IAppConfigFormErrors = {}; + const errors: IAgentOptionsFormErrors = {}; if (agentOptions) { const { error: yamlError, valid: yamlValid } = validateYaml(agentOptions); @@ -58,7 +66,7 @@ const Agents = ({ evt.preventDefault(); // Formatting of API not UI and allows empty agent options - const formDataToSubmit = agentOptions + const formDataToSubmit: any = agentOptions ? { agent_options: yaml.load(agentOptions), } diff --git a/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx b/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx index 53e237d878..66cbe1a2be 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import { IConfig, IConfigFormData } from "interfaces/config"; +import { IConfig } from "interfaces/config"; import Button from "components/buttons/Button"; // @ts-ignore @@ -13,9 +13,14 @@ import { DEFAULT_TRANSPARENCY_URL, IAppConfigFormProps, IFormField, - IAppConfigFormErrors, } from "../constants"; +interface IFleetDesktopFormData { + transparencyUrl: string; +} +interface IFleetDesktopFormErrors { + transparency_url?: string | null; +} const baseClass = "app-config-form"; const FleetDesktop = ({ @@ -24,16 +29,14 @@ const FleetDesktop = ({ isPremiumTier, isUpdatingSettings, }: IAppConfigFormProps): JSX.Element => { - const [formData, setFormData] = useState< - Pick - >({ + const [formData, setFormData] = useState({ transparencyUrl: appConfig.fleet_desktop?.transparency_url || DEFAULT_TRANSPARENCY_URL, }); - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); - const handleInputChange = ({ value }: IFormField) => { + const onInputChange = ({ value }: IFormField) => { setFormData({ transparencyUrl: value.toString() }); setFormErrors({}); }; @@ -41,7 +44,7 @@ const FleetDesktop = ({ const validateForm = () => { const { transparencyUrl } = formData; - const errors: IAppConfigFormErrors = {}; + const errors: IFleetDesktopFormErrors = {}; if (transparencyUrl && !validUrl({ url: transparencyUrl })) { errors.transparency_url = `${transparencyUrl} is not a valid URL`; } @@ -72,7 +75,7 @@ const FleetDesktop = ({
({}); + const [ + formErrors, + setFormErrors, + ] = useState({}); - const handleInputChange = ({ name, value }: IFormField) => { + const onInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); setFormErrors({}); }; const validateForm = () => { - const errors: IAppConfigFormErrors = {}; + const errors: IGlobalHostStatusWebhookFormErrors = {}; if (enableHostStatusWebhook) { if (!destination_url) { @@ -103,6 +106,10 @@ const GlobalHostStatusWebhook = ({ host_percentage: hostStatusWebhookHostPercentage, days_count: hostStatusWebhookWindow, }, + failing_policies_webhook: + appConfig.webhook_settings.failing_policies_webhook, + vulnerabilities_webhook: + appConfig.webhook_settings.vulnerabilities_webhook, }, }; @@ -139,7 +146,7 @@ const GlobalHostStatusWebhook = ({ Send an alert if a portion of your hosts go offline.

({}); + const [formErrors, setFormErrors] = useState({}); - const handleInputChange = ({ name, value }: IFormField) => { + const onInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); setFormErrors({}); }; const validateForm = () => { - const errors: IAppConfigFormErrors = {}; + const errors: IOrgInfoFormErrors = {}; if (!orgName) { errors.org_name = "Organization name must be present"; @@ -96,7 +99,7 @@ const Info = ({ { const { isPremiumTier } = useContext(AppContext); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ enableSMTP: appConfig.smtp_settings?.enable_smtp || false, smtpSenderAddress: appConfig.smtp_settings?.sender_address || "", smtpServer: appConfig.smtp_settings?.server || "", @@ -55,16 +74,16 @@ const Smtp = ({ smtpAuthenticationMethod, } = formData; - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); const sesConfigured = appConfig.email?.backend === "ses" || false; - const handleInputChange = ({ name, value }: IFormField) => { + const onInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); }; const validateForm = () => { - const errors: IAppConfigFormErrors = {}; + const errors: ISmtpConfigFormErrors = {}; if (enableSMTP) { if (!smtpSenderAddress) { @@ -118,7 +137,8 @@ const Smtp = ({ authentication_method: smtpAuthenticationMethod, domain: appConfig.smtp_settings?.domain || "", verify_ssl_certs: appConfig.smtp_settings?.verify_ssl_certs || false, - enable_start_tls: appConfig.smtp_settings?.enable_start_tls, + enable_start_tls: appConfig.smtp_settings?.enable_start_tls || false, + configured: appConfig.smtp_settings?.configured || false, }, }; @@ -134,7 +154,7 @@ const Smtp = ({ <>
({}); + const [formErrors, setFormErrors] = useState({}); - const handleInputChange = ({ name, value }: IFormField) => { + const onInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); }; const validateForm = () => { - const errors: IAppConfigFormErrors = {}; + const errors: ISsoFormErrors = {}; if (enableSso) { if (idpImageUrl && !validUrl({ url: idpImageUrl })) { @@ -113,6 +117,8 @@ const Sso = ({ enable_sso: enableSso, enable_sso_idp_login: enableSsoIdpLogin, enable_jit_provisioning: enableJitProvisioning, + issuer_uri: appConfig.sso_settings.issuer_uri, + enable_jit_role_sync: appConfig.sso_settings.enable_jit_role_sync, }, }; @@ -125,7 +131,7 @@ const Sso = ({ {isPremiumTier && ( { - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ enableUsageStatistics: appConfig.server_settings.enable_analytics, }); const { enableUsageStatistics } = formData; - const handleInputChange = ({ name, value }: IFormField) => { + const onInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); }; @@ -35,6 +39,10 @@ const Statistics = ({ live_query_disabled: appConfig.server_settings.live_query_disabled || false, enable_analytics: enableUsageStatistics, + deferred_save_host: appConfig.server_settings.deferred_save_host, + query_reports_disabled: + appConfig.server_settings.query_reports_disabled, + scripts_disabled: appConfig.server_settings.scripts_disabled, }, }; @@ -63,7 +71,7 @@ const Statistics = ({ />

{ - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ serverURL: appConfig.server_settings.server_url || "", }); const { serverURL } = formData; - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); - const handleInputChange = ({ name, value }: IFormField) => { + const onInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); setFormErrors({}); }; const validateForm = () => { - const errors: IAppConfigFormErrors = {}; + const errors: IWebAddressFormErrors = {}; if (!serverURL) { errors.server_url = "Fleet server URL must be present"; } else if (!validUrl({ url: serverURL, protocol: "http" })) { @@ -52,6 +56,10 @@ const WebAddress = ({ server_url: serverURL, live_query_disabled: appConfig.server_settings.live_query_disabled, enable_analytics: appConfig.server_settings.enable_analytics, + deferred_save_host: appConfig.server_settings.deferred_save_host, + query_reports_disabled: + appConfig.server_settings.query_reports_disabled, + scripts_disabled: appConfig.server_settings.scripts_disabled, }, }; @@ -70,7 +78,7 @@ const WebAddress = ({ Include base path only (eg. no /latest) } - onChange={handleInputChange} + onChange={onInputChange} name="serverURL" value={serverURL} parseTarget diff --git a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts index 33b3d6a0d3..b0fbf223c2 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts +++ b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts @@ -6,7 +6,7 @@ export interface IAppConfigFormProps { appConfig: IConfig; isPremiumTier?: boolean; isUpdatingSettings?: boolean; - handleSubmit: any; + handleSubmit: (formUpdates: Partial) => false | undefined; } export interface IFormField { @@ -14,30 +14,6 @@ export interface IFormField { value: string | boolean | number; } -export interface IAppConfigFormErrors { - metadata?: string | null; - metadata_url?: string | null; - entity_id?: string | null; - idp_name?: string | null; - server_url?: string | null; - org_name?: string | null; - org_logo_url?: string | null; - org_logo_url_light_background?: string | null; - org_support_url?: string | null; - idp_image_url?: string | null; - sender_address?: string | null; - server?: string | null; - server_port?: string | null; - user_name?: string | null; - password?: string | null; - destination_url?: string | null; - days_count?: string | null; - host_percentage?: string | null; - host_expiry_window?: string | null; - agent_options?: string | null; - transparency_url?: string | null; -} - export const authMethodOptions = [ { label: "Plain", value: "authmethod_plain" }, { label: "Cram MD5", value: "authmethod_cram_md5" }, diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx index e16a487145..d7c8bc9d92 100644 --- a/frontend/utilities/constants.tsx +++ b/frontend/utilities/constants.tsx @@ -26,6 +26,12 @@ export const DEFAULT_GRAVATAR_LINK_FALLBACK = export const DEFAULT_GRAVATAR_LINK_DARK_FALLBACK = "/assets/images/icon-avatar-default-dark-24x24%402x.png"; +export const ACTIVITY_EXPIRY_WINDOW_DROPDOWN_OPTIONS: IDropdownOption[] = [ + { value: 30, label: "30 days" }, + { value: 60, label: "60 days" }, + { value: 90, label: "90 days" }, +]; + export const FREQUENCY_DROPDOWN_OPTIONS: IDropdownOption[] = [ { value: 0, label: "Never" }, { value: 300, label: "Every 5 minutes" }, From a27634deb10d5a6f444aec7a692154df29824c08 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 10 Apr 2024 09:50:26 -0500 Subject: [PATCH 34/83] Updating orbit_info schema. (#18135) #17148 #17361 In orbit_info table, added the following fields: - desktop_version - scripts_enabled --- schema/osquery_fleet_schema.json | 12 ++++++++++++ schema/tables/orbit_info.yml | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json index 6f88e524f6..bd13071fe1 100644 --- a/schema/osquery_fleet_schema.json +++ b/schema/osquery_fleet_schema.json @@ -17696,11 +17696,23 @@ "required": false, "description": "The Update Framework update channel used for the Fleet Desktop executable." }, + { + "name": "desktop_version", + "type": "text", + "required": false, + "description": "The version of the fleet-desktop instance. Blank if fleet-desktop is not installed." + }, { "name": "uptime", "type": "bigint", "required": false, "description": "Uptime of the orbit process in seconds." + }, + { + "name": "scripts_enabled", + "type": "integer", + "required": false, + "description": "1 if running scripts is enabled, 0 if disabled." } ], "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", diff --git a/schema/tables/orbit_info.yml b/schema/tables/orbit_info.yml index f626eb0c80..1dfa3a9ff0 100644 --- a/schema/tables/orbit_info.yml +++ b/schema/tables/orbit_info.yml @@ -33,9 +33,17 @@ columns: type: text required: false description: The Update Framework update channel used for the Fleet Desktop executable. + - name: desktop_version + type: text + required: false + description: The version of the fleet-desktop instance. Blank if fleet-desktop is not installed. - name: uptime type: bigint required: false description: Uptime of the orbit process in seconds. + - name: scripts_enabled + type: integer + required: false + description: 1 if running scripts is enabled, 0 if disabled. notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer). evented: false From e73fd55c5b37ce3842d4fda56433e2286512f2c6 Mon Sep 17 00:00:00 2001 From: George Karr Date: Wed, 10 Apr 2024 10:04:50 -0500 Subject: [PATCH 35/83] Updating vuln check support (#15174) --- server/vulnerabilities/oval/parsed/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/vulnerabilities/oval/parsed/utils.go b/server/vulnerabilities/oval/parsed/utils.go index d9fd835b2c..ad6ffafd67 100644 --- a/server/vulnerabilities/oval/parsed/utils.go +++ b/server/vulnerabilities/oval/parsed/utils.go @@ -20,7 +20,7 @@ func ReplaceFedoraOSVersion(version string) string { "Red Hat Enterprise Linux 6.0.0": regexp.MustCompile(`Fedora Linux (12|13|14|15|16|17|18)\.`), "Red Hat Enterprise Linux 7.0.0": regexp.MustCompile(`Fedora Linux (19|20|21|22|23|24|25|26|27)\.`), "Red Hat Enterprise Linux 8.0.0": regexp.MustCompile(`Fedora Linux (28|29|30|31|32|33)\.`), - "Red Hat Enterprise Linux 9.0.0": regexp.MustCompile(`Fedora Linux (34|35|36)\.`), + "Red Hat Enterprise Linux 9.0.0": regexp.MustCompile(`Fedora Linux (34|35|36|37|38|39|40)\.`), } for rep, pattern := range rules { if pattern.ReplaceAllString(version, rep) != version { From 24d0f7005e39cab3f219dda34a4dac05f318f02c Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 10 Apr 2024 10:14:35 -0500 Subject: [PATCH 36/83] Website: upgrade `sails` to 1.5.10 (#18177) Closes: #18048 Changes: - Updated the version of `sails` the website uses --- website/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/package.json b/website/package.json index 9c3601ae69..987acade2e 100644 --- a/website/package.json +++ b/website/package.json @@ -10,7 +10,7 @@ "@sailshq/socket.io-redis": "^6.1.2", "jsonwebtoken": "9.0.2", "moment": "2.29.4", - "sails": "^1.5.9", + "sails": "^1.5.10", "sails-hook-apianalytics": "^2.0.6", "sails-hook-organics": "^2.2.2", "sails-hook-orm": "^4.0.3", From 3859c971cf4891e8e027bd3758c99d33cb36c300 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 10 Apr 2024 11:29:46 -0500 Subject: [PATCH 37/83] Added error messages when scripts are disabled. (#18174) #17148 Added error messages to lock/unlock/wipe when scripts are disabled. # Checklist for submitter - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- cmd/fleetctl/mdm_test.go | 77 +++++++++++++++++-- ee/server/service/hosts.go | 42 ++++++++++ server/datastore/mysql/hosts.go | 19 +++++ server/datastore/mysql/hosts_test.go | 39 ++++++++++ server/fleet/datastore.go | 2 + server/fleet/hosts.go | 5 ++ server/mock/datastore_mock.go | 12 +++ server/service/integration_enterprise_test.go | 22 ++++++ 8 files changed, 213 insertions(+), 5 deletions(-) diff --git a/cmd/fleetctl/mdm_test.go b/cmd/fleetctl/mdm_test.go index 0a7062de30..e708c60dec 100644 --- a/cmd/fleetctl/mdm_test.go +++ b/cmd/fleetctl/mdm_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "slices" "testing" "time" @@ -487,6 +488,21 @@ func TestMDMLockCommand(t *testing.T) { return h.MDMInfo, nil } + ds.GetHostOrbitInfoFunc = func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) { + hostIDMod := hostID % 3 + switch hostIDMod { + case 0: + return nil, ¬FoundError{} + case 1: + return &fleet.HostOrbitInfo{}, nil + case 2: + return &fleet.HostOrbitInfo{ScriptsEnabled: ptr.Bool(true)}, nil + default: + t.Errorf("unexpected hostIDMod %v", hostIDMod) + return nil, nil + } + } + appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs() successfulOutput := func(ident string) string { @@ -730,6 +746,21 @@ func TestMDMUnlockCommand(t *testing.T) { return h.MDMInfo, nil } + ds.GetHostOrbitInfoFunc = func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) { + hostIDMod := hostID % 3 + switch hostIDMod { + case 0: + return nil, ¬FoundError{} + case 1: + return &fleet.HostOrbitInfo{}, nil + case 2: + return &fleet.HostOrbitInfo{ScriptsEnabled: ptr.Bool(true)}, nil + default: + t.Errorf("unexpected hostIDMod %v", hostIDMod) + return nil, nil + } + } + appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs() successfulOutput := func(ident string) string { @@ -793,11 +824,6 @@ func TestMDMWipeCommand(t *testing.T) { MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, } - linuxEnrolled := &fleet.Host{ - ID: 3, - UUID: "linux-enrolled", - Platform: "linux", - } winNotEnrolled := &fleet.Host{ ID: 4, UUID: "win-not-enrolled", @@ -892,6 +918,23 @@ func TestMDMWipeCommand(t *testing.T) { MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual")}, } + linuxEnrolled := &fleet.Host{ + ID: 18, + UUID: "linux-enrolled", + Platform: "linux", + } + linuxEnrolled2 := &fleet.Host{ + ID: 19, + UUID: "linux-enrolled", + Platform: "linux", + } + linuxEnrolled3 := &fleet.Host{ + ID: 20, + UUID: "linux-enrolled", + Platform: "linux", + } + + linuxHostIDs := []uint{linuxEnrolled.ID, linuxEnrolled2.ID, linuxEnrolled3.ID} hostByUUID := make(map[string]*fleet.Host) hostsByID := make(map[uint]*fleet.Host) @@ -899,6 +942,8 @@ func TestMDMWipeCommand(t *testing.T) { winEnrolled, macEnrolled, linuxEnrolled, + linuxEnrolled2, + linuxEnrolled3, macNotEnrolled, winNotEnrolled, macPending, @@ -1039,6 +1084,26 @@ func TestMDMWipeCommand(t *testing.T) { return h.MDMInfo, nil } + // This function should only run on linux + ds.GetHostOrbitInfoFunc = func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) { + if !slices.Contains(linuxHostIDs, hostID) { + t.Errorf("GetHostOrbitInfo should not be called for non-linux host %v", hostID) + return nil, nil + } + hostIDMod := hostID % 3 + switch hostIDMod { + case 0: + return nil, ¬FoundError{} + case 1: + return &fleet.HostOrbitInfo{}, nil + case 2: + return &fleet.HostOrbitInfo{ScriptsEnabled: ptr.Bool(true)}, nil + default: + t.Errorf("unexpected hostIDMod %v", hostIDMod) + return nil, nil + } + } + appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs() appCfgScriptsDisabled := &fleet.AppConfig{ServerSettings: fleet.ServerSettings{ScriptsDisabled: true}} @@ -1055,6 +1120,8 @@ func TestMDMWipeCommand(t *testing.T) { {appCfgAllMDM, "valid windows", []string{"--host", winEnrolled.UUID}, ""}, {appCfgAllMDM, "valid macos", []string{"--host", macEnrolled.UUID}, ""}, {appCfgNoMDM, "valid linux", []string{"--host", linuxEnrolled.UUID}, ""}, + {appCfgNoMDM, "valid linux 2", []string{"--host", linuxEnrolled2.UUID}, ""}, + {appCfgNoMDM, "valid linux 3", []string{"--host", linuxEnrolled3.UUID}, ""}, {appCfgNoMDM, "valid windows but no mdm", []string{"--host", winEnrolled.UUID}, `Windows MDM isn't turned on.`}, {appCfgMacMDM, "valid macos but not enrolled", []string{"--host", macNotEnrolled.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`}, {appCfgWinMDM, "valid windows but not enrolled", []string{"--host", winNotEnrolled.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`}, diff --git a/ee/server/service/hosts.go b/ee/server/service/hosts.go index f6f9168516..4bd442516a 100644 --- a/ee/server/service/hosts.go +++ b/ee/server/service/hosts.go @@ -96,6 +96,20 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error { if appCfg.ServerSettings.ScriptsDisabled { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock host because running scripts is disabled in organization settings.")) } + hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID) + switch { + case err != nil: + // If not found, then do nothing. We do not know if this host has scripts enabled or not + if !fleet.IsNotFound(err) { + return ctxerr.Wrap(ctx, err, "get host orbit info") + } + case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled: + return ctxerr.Wrap( + ctx, fleet.NewInvalidArgumentError( + "host_id", "Couldn't lock host. To lock, deploy the fleetd agent with --enable-scripts and refetch host vitals.", + ), + ) + } default: return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform))) @@ -166,6 +180,20 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error) if appCfg.ServerSettings.ScriptsDisabled { return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't unlock host because running scripts is disabled in organization settings.")) } + hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID) + switch { + case err != nil: + // If not found, then do nothing. We do not know if this host has scripts enabled or not + if !fleet.IsNotFound(err) { + return "", ctxerr.Wrap(ctx, err, "get host orbit info") + } + case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled: + return "", ctxerr.Wrap( + ctx, fleet.NewInvalidArgumentError( + "host_id", "Couldn't unlock host. To unlock, deploy the fleetd agent with --enable-scripts and refetch host vitals.", + ), + ) + } default: return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform))) @@ -248,6 +276,20 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error { if appCfg.ServerSettings.ScriptsDisabled { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe host because running scripts is disabled in organization settings.")) } + hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID) + switch { + case err != nil: + // If not found, then do nothing. We do not know if this host has scripts enabled or not + if !fleet.IsNotFound(err) { + return ctxerr.Wrap(ctx, err, "get host orbit info") + } + case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled: + return ctxerr.Wrap( + ctx, fleet.NewInvalidArgumentError( + "host_id", "Couldn't wipe host. To wipe, deploy the fleetd agent with --enable-scripts and refetch host vitals.", + ), + ) + } default: return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform))) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 2a675fa0ef..6daf64222b 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -3708,6 +3708,25 @@ func (ds *Datastore) SetOrUpdateHostOrbitInfo( ) } +func (ds *Datastore) GetHostOrbitInfo(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) { + var orbit fleet.HostOrbitInfo + err := sqlx.GetContext( + ctx, ds.reader(ctx), &orbit, ` + SELECT + scripts_enabled + FROM + host_orbit_info + WHERE host_id = ?`, hostID, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ctxerr.Wrap(ctx, notFound("HostOrbitInfo").WithID(hostID)) + } + return nil, ctxerr.Wrapf(ctx, err, "select host_orbit_info for host_id %d", hostID) + } + return &orbit, nil +} + func (ds *Datastore) getOrInsertMDMSolution(ctx context.Context, serverURL string, mdmName string) (mdmID uint, err error) { readStmt := ¶meterizedStmt{ Statement: `SELECT id FROM mobile_device_management_solutions WHERE name = ? AND server_url = ?`, diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index e2841849e2..16c3d08bf5 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -163,6 +163,7 @@ func TestHosts(t *testing.T) { {"ListHostsWithPagination", testListHostsWithPagination}, {"LastRestarted", testLastRestarted}, {"HostHealth", testHostHealth}, + {"GetHostOrbitInfo", testGetHostOrbitInfo}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -8756,3 +8757,41 @@ func testHostHealth(t *testing.T, ds *Datastore) { require.Empty(t, hh.VulnerableSoftware) require.Equal(t, h.TeamID, hh.TeamID) } + +func testGetHostOrbitInfo(t *testing.T, ds *Datastore) { + host, err := ds.NewHost( + context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("1"), + UUID: "1", + Hostname: "foo.local", + PrimaryIP: "192.168.1.1", + PrimaryMac: "30-65-EC-6F-C4-58", + }, + ) + require.NoError(t, err) + require.NotNil(t, host) + + _, err = ds.GetHostOrbitInfo(context.Background(), host.ID) + require.True(t, fleet.IsNotFound(err)) + + orbitVersion := "1.1.0" + err = ds.SetOrUpdateHostOrbitInfo( + context.Background(), host.ID, orbitVersion, sql.NullString{Valid: false}, sql.NullBool{Valid: false}, + ) + require.NoError(t, err) + hostOrbitInfo, err := ds.GetHostOrbitInfo(context.Background(), host.ID) + require.NoError(t, err) + assert.Nil(t, hostOrbitInfo.ScriptsEnabled) + + err = ds.SetOrUpdateHostOrbitInfo( + context.Background(), host.ID, orbitVersion, sql.NullString{Valid: false}, sql.NullBool{Bool: true, Valid: true}, + ) + require.NoError(t, err) + hostOrbitInfo, err = ds.GetHostOrbitInfo(context.Background(), host.ID) + require.NoError(t, err) + assert.True(t, *hostOrbitInfo.ScriptsEnabled) +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index d549464d2a..3292e8740d 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -841,6 +841,8 @@ type Datastore interface { ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool, ) error + GetHostOrbitInfo(ctx context.Context, hostID uint) (*HostOrbitInfo, error) + ReplaceHostDeviceMapping(ctx context.Context, id uint, mappings []*HostDeviceMapping, source string) error // ReplaceHostBatteries creates or updates the battery mappings of a host. diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 441e9e7b2e..809ab12d54 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -359,6 +359,11 @@ type Host struct { Policies *[]*HostPolicy `json:"policies,omitempty" csv:"-"` } +// HostOrbitInfo maps to the host_orbit_info table in the database, which maps to the orbit_info agent table. +type HostOrbitInfo struct { + ScriptsEnabled *bool `json:"scripts_enabled" db:"scripts_enabled"` +} + // HostHealth contains a subset of Host data that indicates how healthy a Host is. For fields with // the same name, see the comments/docs for the Host field above. type HostHealth struct { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index c94e88e953..1ed9fe53eb 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -589,6 +589,8 @@ type GetHostMDMProfileRetryCountByCommandUUIDFunc func(ctx context.Context, host type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool) error +type GetHostOrbitInfoFunc func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) + type ReplaceHostDeviceMappingFunc func(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping, source string) error type ReplaceHostBatteriesFunc func(ctx context.Context, id uint, mappings []*fleet.HostBattery) error @@ -1755,6 +1757,9 @@ type DataStore struct { SetOrUpdateHostOrbitInfoFunc SetOrUpdateHostOrbitInfoFunc SetOrUpdateHostOrbitInfoFuncInvoked bool + GetHostOrbitInfoFunc GetHostOrbitInfoFunc + GetHostOrbitInfoFuncInvoked bool + ReplaceHostDeviceMappingFunc ReplaceHostDeviceMappingFunc ReplaceHostDeviceMappingFuncInvoked bool @@ -4218,6 +4223,13 @@ func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, v return s.SetOrUpdateHostOrbitInfoFunc(ctx, hostID, version, desktopVersion, scriptsEnabled) } +func (s *DataStore) GetHostOrbitInfo(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) { + s.mu.Lock() + s.GetHostOrbitInfoFuncInvoked = true + s.mu.Unlock() + return s.GetHostOrbitInfoFunc(ctx, hostID) +} + func (s *DataStore) ReplaceHostDeviceMapping(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping, source string) error { s.mu.Lock() s.ReplaceHostDeviceMappingFuncInvoked = true diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 957f41623f..2b53cff98b 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -7244,6 +7244,28 @@ func (s *integrationEnterpriseTestSuite) TestLockUnlockWipeWindowsLinux() { errMsg = extractServerErrorText(res.Body) require.Contains(t, errMsg, "Windows MDM isn't turned on.") + // Disable scripts on Linux host + err := s.ds.SetOrUpdateHostOrbitInfo( + context.Background(), linuxHost.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: false, Valid: true}, + ) + require.NoError(t, err) + // try to lock/unlock/wipe the Linux host. Fails because scripts are not enabled. + res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", linuxHost.ID), nil, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Couldn't lock host. To lock, deploy the fleetd agent with --enable-scripts") + res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", linuxHost.ID), nil, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Couldn't unlock host. To unlock, deploy the fleetd agent with --enable-scripts") + res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", linuxHost.ID), nil, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Couldn't wipe host. To wipe, deploy the fleetd agent with --enable-scripts") + + // Enable scripts on Linux host + err = s.ds.SetOrUpdateHostOrbitInfo( + context.Background(), linuxHost.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: true, Valid: true}, + ) + require.NoError(t, err) + // try to lock/unlock/wipe the Linux host succeeds, no MDM constraints s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", linuxHost.ID), nil, http.StatusNoContent) From 0b5e0069d49edc4a91cc64e21430c40781a8f2de Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:30:39 -0400 Subject: [PATCH 38/83] Revert "Fleet UI: Add advanced setting to set expiry window for activity log (#17989) (#18185) ## Issue #17989 ## Description -BE not finished, in progress, but @lucasmrod is starting oncall so might not be part of sprint This reverts commit e99eaf6cd31d8e5e65c3af4715380c702bbb0942. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/` or `orbit/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)). --- changes/16989-ui-to-delete-old-activities | 1 - frontend/__mocks__/configMock.ts | 4 - frontend/interfaces/config.ts | 45 +++++-- .../cards/Advanced/Advanced.tsx | 114 ++++-------------- .../OrgSettingsPage/cards/Agents/Agents.tsx | 18 +-- .../cards/FleetDesktop/FleetDesktop.tsx | 21 ++-- .../GlobalHostStatusWebhook.tsx | 45 +++---- .../admin/OrgSettingsPage/cards/Info/Info.tsx | 29 ++--- .../admin/OrgSettingsPage/cards/Smtp/Smtp.tsx | 50 +++----- .../admin/OrgSettingsPage/cards/Sso/Sso.tsx | 54 ++++----- .../cards/Statistics/Statistics.tsx | 14 +-- .../cards/WebAddress/WebAddress.tsx | 28 ++--- .../admin/OrgSettingsPage/cards/constants.ts | 26 +++- frontend/utilities/constants.tsx | 6 - 14 files changed, 182 insertions(+), 273 deletions(-) delete mode 100644 changes/16989-ui-to-delete-old-activities diff --git a/changes/16989-ui-to-delete-old-activities b/changes/16989-ui-to-delete-old-activities deleted file mode 100644 index 6897b0212a..0000000000 --- a/changes/16989-ui-to-delete-old-activities +++ /dev/null @@ -1 +0,0 @@ -- Add advanced setting to set expiry window for activity log diff --git a/frontend/__mocks__/configMock.ts b/frontend/__mocks__/configMock.ts index af7bd63524..5ede26b805 100644 --- a/frontend/__mocks__/configMock.ts +++ b/frontend/__mocks__/configMock.ts @@ -46,10 +46,6 @@ const DEFAULT_CONFIG_MOCK: IConfig = { host_expiry_enabled: false, host_expiry_window: 0, }, - activity_expiry_settings: { - activity_expiry_enabled: true, - activity_expiry_window: 90, - }, agent_options: "", license: { tier: "free", diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index bc8f0cb759..f5f52a544d 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -65,6 +65,41 @@ export interface IFleetDesktopSettings { transparency_url: string; } +export interface IConfigFormData { + smtpAuthenticationMethod: string; + smtpAuthenticationType: string; + domain: string; + smtpEnableSslTls: boolean; + enableStartTls: boolean; + serverUrl: string; + orgLogoUrl: string; + orgName: string; + smtpPassword: string; + smtpPort?: number; + smtpSenderAddress: string; + smtpServer: string; + smtpUsername: string; + verifySslCerts: boolean; + entityId: string; + idpImageUrl: string; + metadata: string; + metadataUrl: string; + idpName: string; + enableSso: boolean; + enableSsoIdpLogin: boolean; + enableSmtp: boolean; + enableHostExpiry: boolean; + hostExpiryWindow: number; + disableLiveQuery: boolean; + agentOptions: any; + enableHostStatusWebhook: boolean; + hostStatusWebhookDestinationUrl?: string; + hostStatusWebhookHostPercentage?: number; + hostStatusWebhookDaysCount?: number; + enableUsageStatistics: boolean; + transparencyUrl: string; +} + export interface IConfigFeatures { enable_host_users: boolean; enable_software_inventory: boolean; @@ -90,7 +125,7 @@ export interface IConfig { server_settings: IConfigServerSettings; smtp_settings?: { enable_smtp: boolean; - configured?: boolean; + configured: boolean; sender_address: string; server: string; port?: number; @@ -117,14 +152,10 @@ export interface IConfig { }; host_expiry_settings: { host_expiry_enabled: boolean; - host_expiry_window?: number; - }; - activity_expiry_settings: { - activity_expiry_enabled: boolean; - activity_expiry_window?: number; + host_expiry_window: number; }; features: IConfigFeatures; - agent_options: unknown; // Can pass empty object + agent_options: string; update_interval: { osquery_detail: number; osquery_policy: number; diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx index 4ea4371521..bd0704ce43 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx @@ -1,53 +1,31 @@ -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect } from "react"; import Button from "components/buttons/Button"; import Checkbox from "components/forms/fields/Checkbox"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import SectionHeader from "components/SectionHeader"; -// @ts-ignore -import Dropdown from "components/forms/fields/Dropdown"; -import { ACTIVITY_EXPIRY_WINDOW_DROPDOWN_OPTIONS } from "utilities/constants"; -import { getCustomDropdownOptions } from "utilities/helpers"; - -import { IAppConfigFormProps, IFormField } from "../constants"; +import { + IAppConfigFormProps, + IFormField, + IAppConfigFormErrors, +} from "../constants"; const baseClass = "app-config-form"; -interface IAdvancedConfigFormData { - domain: string; - verifySSLCerts: boolean; - enableStartTLS?: boolean; - enableHostExpiry: boolean; - hostExpiryWindow: number; - deleteActivities: boolean; - activityExpiryWindow: number; - disableLiveQuery: boolean; - disableScripts: boolean; - disableQueryReports: boolean; -} - -interface IAdvancedConfigFormErrors { - host_expiry_window?: string | null; -} - const Advanced = ({ appConfig, handleSubmit, isUpdatingSettings, }: IAppConfigFormProps): JSX.Element => { - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ domain: appConfig.smtp_settings?.domain || "", verifySSLCerts: appConfig.smtp_settings?.verify_ssl_certs || false, enableStartTLS: appConfig.smtp_settings?.enable_start_tls, enableHostExpiry: appConfig.host_expiry_settings.host_expiry_enabled || false, hostExpiryWindow: appConfig.host_expiry_settings.host_expiry_window || 0, - deleteActivities: - appConfig.activity_expiry_settings?.activity_expiry_enabled || false, - activityExpiryWindow: - appConfig.activity_expiry_settings?.activity_expiry_window || 30, disableLiveQuery: appConfig.server_settings.live_query_disabled || false, disableQueryReports: appConfig.server_settings.query_reports_disabled || false, @@ -60,35 +38,20 @@ const Advanced = ({ enableStartTLS, enableHostExpiry, hostExpiryWindow, - deleteActivities, - activityExpiryWindow, disableLiveQuery, disableScripts, disableQueryReports, } = formData; - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); - const activityExpiryWindowOptions = useMemo( - () => - getCustomDropdownOptions( - ACTIVITY_EXPIRY_WINDOW_DROPDOWN_OPTIONS, - activityExpiryWindow, - // it's safe to assume that frequency is a number - (frequency: number | string) => `${frequency as number} days` - ), - // intentionally leave activityExpiryWindow out of the dependencies, so that the custom - // options are maintained even if the user changes the frequency in the UI - [deleteActivities] - ); - - const onInputChange = ({ name, value }: IFormField) => { + const handleInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); }; useEffect(() => { // validate desired form fields - const errors: IAdvancedConfigFormErrors = {}; + const errors: IAppConfigFormErrors = {}; if (enableHostExpiry && (!hostExpiryWindow || hostExpiryWindow <= 0)) { errors.host_expiry_window = @@ -109,13 +72,12 @@ const Advanced = ({ enable_analytics: appConfig.server_settings.enable_analytics, query_reports_disabled: disableQueryReports, scripts_disabled: disableScripts, - deferred_save_host: appConfig.server_settings.deferred_save_host, }, smtp_settings: { enable_smtp: appConfig.smtp_settings?.enable_smtp || false, sender_address: appConfig.smtp_settings?.sender_address || "", server: appConfig.smtp_settings?.server || "", - port: appConfig.smtp_settings?.port || undefined, + port: Number(appConfig.smtp_settings?.port), authentication_type: appConfig.smtp_settings?.authentication_type || "", user_name: appConfig.smtp_settings?.user_name || "", password: appConfig.smtp_settings?.password || "", @@ -124,15 +86,11 @@ const Advanced = ({ appConfig.smtp_settings?.authentication_method || "", domain, verify_ssl_certs: verifySSLCerts, - enable_start_tls: enableStartTLS || false, + enable_start_tls: enableStartTLS, }, host_expiry_settings: { host_expiry_enabled: enableHostExpiry, - host_expiry_window: hostExpiryWindow || undefined, - }, - activity_expiry_settings: { - activity_expiry_enabled: deleteActivities, - activity_expiry_window: activityExpiryWindow || undefined, + host_expiry_window: Number(hostExpiryWindow), }, }; @@ -149,7 +107,7 @@ const Advanced = ({

)} - When enabled, allows automatic cleanup of audit logs older than - the number of days specified in the{" "} - Audit log retention window setting. - - (Default: Off) - - - } - > - Delete activities - - {deleteActivities && ( - - )} - { const { ADMIN_TEAMS } = paths; - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ agentOptions: agentOptionsToYaml(appConfig.agent_options), }); - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); const { agentOptions } = formData; @@ -45,7 +37,7 @@ const Agents = ({ }; const validateForm = () => { - const errors: IAgentOptionsFormErrors = {}; + const errors: IAppConfigFormErrors = {}; if (agentOptions) { const { error: yamlError, valid: yamlValid } = validateYaml(agentOptions); @@ -66,7 +58,7 @@ const Agents = ({ evt.preventDefault(); // Formatting of API not UI and allows empty agent options - const formDataToSubmit: any = agentOptions + const formDataToSubmit = agentOptions ? { agent_options: yaml.load(agentOptions), } diff --git a/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx b/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx index 66cbe1a2be..53e237d878 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import { IConfig } from "interfaces/config"; +import { IConfig, IConfigFormData } from "interfaces/config"; import Button from "components/buttons/Button"; // @ts-ignore @@ -13,14 +13,9 @@ import { DEFAULT_TRANSPARENCY_URL, IAppConfigFormProps, IFormField, + IAppConfigFormErrors, } from "../constants"; -interface IFleetDesktopFormData { - transparencyUrl: string; -} -interface IFleetDesktopFormErrors { - transparency_url?: string | null; -} const baseClass = "app-config-form"; const FleetDesktop = ({ @@ -29,14 +24,16 @@ const FleetDesktop = ({ isPremiumTier, isUpdatingSettings, }: IAppConfigFormProps): JSX.Element => { - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState< + Pick + >({ transparencyUrl: appConfig.fleet_desktop?.transparency_url || DEFAULT_TRANSPARENCY_URL, }); - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); - const onInputChange = ({ value }: IFormField) => { + const handleInputChange = ({ value }: IFormField) => { setFormData({ transparencyUrl: value.toString() }); setFormErrors({}); }; @@ -44,7 +41,7 @@ const FleetDesktop = ({ const validateForm = () => { const { transparencyUrl } = formData; - const errors: IFleetDesktopFormErrors = {}; + const errors: IAppConfigFormErrors = {}; if (transparencyUrl && !validUrl({ url: transparencyUrl })) { errors.transparency_url = `${transparencyUrl} is not a valid URL`; } @@ -75,7 +72,7 @@ const FleetDesktop = ({ ({}); + const [formErrors, setFormErrors] = useState({}); - const onInputChange = ({ name, value }: IFormField) => { + const handleInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); setFormErrors({}); }; const validateForm = () => { - const errors: IGlobalHostStatusWebhookFormErrors = {}; + const errors: IAppConfigFormErrors = {}; if (enableHostStatusWebhook) { if (!destination_url) { @@ -106,10 +103,6 @@ const GlobalHostStatusWebhook = ({ host_percentage: hostStatusWebhookHostPercentage, days_count: hostStatusWebhookWindow, }, - failing_policies_webhook: - appConfig.webhook_settings.failing_policies_webhook, - vulnerabilities_webhook: - appConfig.webhook_settings.vulnerabilities_webhook, }, }; @@ -146,7 +139,7 @@ const GlobalHostStatusWebhook = ({ Send an alert if a portion of your hosts go offline.

({}); + const [formErrors, setFormErrors] = useState({}); - const onInputChange = ({ name, value }: IFormField) => { + const handleInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); setFormErrors({}); }; const validateForm = () => { - const errors: IOrgInfoFormErrors = {}; + const errors: IAppConfigFormErrors = {}; if (!orgName) { errors.org_name = "Organization name must be present"; @@ -99,7 +96,7 @@ const Info = ({ { const { isPremiumTier } = useContext(AppContext); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ enableSMTP: appConfig.smtp_settings?.enable_smtp || false, smtpSenderAddress: appConfig.smtp_settings?.sender_address || "", smtpServer: appConfig.smtp_settings?.server || "", @@ -74,16 +55,16 @@ const Smtp = ({ smtpAuthenticationMethod, } = formData; - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); const sesConfigured = appConfig.email?.backend === "ses" || false; - const onInputChange = ({ name, value }: IFormField) => { + const handleInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); }; const validateForm = () => { - const errors: ISmtpConfigFormErrors = {}; + const errors: IAppConfigFormErrors = {}; if (enableSMTP) { if (!smtpSenderAddress) { @@ -137,8 +118,7 @@ const Smtp = ({ authentication_method: smtpAuthenticationMethod, domain: appConfig.smtp_settings?.domain || "", verify_ssl_certs: appConfig.smtp_settings?.verify_ssl_certs || false, - enable_start_tls: appConfig.smtp_settings?.enable_start_tls || false, - configured: appConfig.smtp_settings?.configured || false, + enable_start_tls: appConfig.smtp_settings?.enable_start_tls, }, }; @@ -154,7 +134,7 @@ const Smtp = ({ <>
({}); + const [formErrors, setFormErrors] = useState({}); - const onInputChange = ({ name, value }: IFormField) => { + const handleInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); }; const validateForm = () => { - const errors: ISsoFormErrors = {}; + const errors: IAppConfigFormErrors = {}; if (enableSso) { if (idpImageUrl && !validUrl({ url: idpImageUrl })) { @@ -117,8 +113,6 @@ const Sso = ({ enable_sso: enableSso, enable_sso_idp_login: enableSsoIdpLogin, enable_jit_provisioning: enableJitProvisioning, - issuer_uri: appConfig.sso_settings.issuer_uri, - enable_jit_role_sync: appConfig.sso_settings.enable_jit_role_sync, }, }; @@ -131,7 +125,7 @@ const Sso = ({ {isPremiumTier && ( { - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ enableUsageStatistics: appConfig.server_settings.enable_analytics, }); const { enableUsageStatistics } = formData; - const onInputChange = ({ name, value }: IFormField) => { + const handleInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); }; @@ -39,10 +35,6 @@ const Statistics = ({ live_query_disabled: appConfig.server_settings.live_query_disabled || false, enable_analytics: enableUsageStatistics, - deferred_save_host: appConfig.server_settings.deferred_save_host, - query_reports_disabled: - appConfig.server_settings.query_reports_disabled, - scripts_disabled: appConfig.server_settings.scripts_disabled, }, }; @@ -71,7 +63,7 @@ const Statistics = ({ />

{ - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ serverURL: appConfig.server_settings.server_url || "", }); const { serverURL } = formData; - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); - const onInputChange = ({ name, value }: IFormField) => { + const handleInputChange = ({ name, value }: IFormField) => { setFormData({ ...formData, [name]: value }); setFormErrors({}); }; const validateForm = () => { - const errors: IWebAddressFormErrors = {}; + const errors: IAppConfigFormErrors = {}; if (!serverURL) { errors.server_url = "Fleet server URL must be present"; } else if (!validUrl({ url: serverURL, protocol: "http" })) { @@ -56,10 +52,6 @@ const WebAddress = ({ server_url: serverURL, live_query_disabled: appConfig.server_settings.live_query_disabled, enable_analytics: appConfig.server_settings.enable_analytics, - deferred_save_host: appConfig.server_settings.deferred_save_host, - query_reports_disabled: - appConfig.server_settings.query_reports_disabled, - scripts_disabled: appConfig.server_settings.scripts_disabled, }, }; @@ -78,7 +70,7 @@ const WebAddress = ({ Include base path only (eg. no /latest) } - onChange={onInputChange} + onChange={handleInputChange} name="serverURL" value={serverURL} parseTarget diff --git a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts index b0fbf223c2..33b3d6a0d3 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts +++ b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts @@ -6,7 +6,7 @@ export interface IAppConfigFormProps { appConfig: IConfig; isPremiumTier?: boolean; isUpdatingSettings?: boolean; - handleSubmit: (formUpdates: Partial) => false | undefined; + handleSubmit: any; } export interface IFormField { @@ -14,6 +14,30 @@ export interface IFormField { value: string | boolean | number; } +export interface IAppConfigFormErrors { + metadata?: string | null; + metadata_url?: string | null; + entity_id?: string | null; + idp_name?: string | null; + server_url?: string | null; + org_name?: string | null; + org_logo_url?: string | null; + org_logo_url_light_background?: string | null; + org_support_url?: string | null; + idp_image_url?: string | null; + sender_address?: string | null; + server?: string | null; + server_port?: string | null; + user_name?: string | null; + password?: string | null; + destination_url?: string | null; + days_count?: string | null; + host_percentage?: string | null; + host_expiry_window?: string | null; + agent_options?: string | null; + transparency_url?: string | null; +} + export const authMethodOptions = [ { label: "Plain", value: "authmethod_plain" }, { label: "Cram MD5", value: "authmethod_cram_md5" }, diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx index d7c8bc9d92..e16a487145 100644 --- a/frontend/utilities/constants.tsx +++ b/frontend/utilities/constants.tsx @@ -26,12 +26,6 @@ export const DEFAULT_GRAVATAR_LINK_FALLBACK = export const DEFAULT_GRAVATAR_LINK_DARK_FALLBACK = "/assets/images/icon-avatar-default-dark-24x24%402x.png"; -export const ACTIVITY_EXPIRY_WINDOW_DROPDOWN_OPTIONS: IDropdownOption[] = [ - { value: 30, label: "30 days" }, - { value: 60, label: "60 days" }, - { value: 90, label: "90 days" }, -]; - export const FREQUENCY_DROPDOWN_OPTIONS: IDropdownOption[] = [ { value: 0, label: "Never" }, { value: 300, label: "Every 5 minutes" }, From 38e1307d6b9b20247c225ff073a34bd88d0cb5ea Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:23:22 -0400 Subject: [PATCH 39/83] Fleet UI: Show hidden columns in schema with a note (#17983) --- changes/17787-hidden-columns | 1 + .../QuerySidePanel/QuerySidePanel.tests.tsx | 16 +++++++++++++-- .../ColumnListItem/ColumnListItem.tsx | 9 +++++++++ .../QueryTableColumns/QueryTableColumns.tsx | 20 +++++++++---------- 4 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 changes/17787-hidden-columns diff --git a/changes/17787-hidden-columns b/changes/17787-hidden-columns new file mode 100644 index 0000000000..79509a758f --- /dev/null +++ b/changes/17787-hidden-columns @@ -0,0 +1 @@ +- UI and website show hidden columns in schema with a note that they won't be returned by running select \* from table diff --git a/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.tests.tsx b/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.tests.tsx index 25aa78ba5a..63ca4ac0ce 100644 --- a/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.tests.tsx +++ b/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.tests.tsx @@ -38,7 +38,7 @@ describe("QuerySidePanel - component", () => { expect(platformCompatibility).toHaveTextContent(/chromeos/i); }); - it("renders the correct number of columns including hiding columns set to hidden", () => { + it("renders the correct number of columns including rendering hidden columns", () => { const { container } = render( { ); const platformList = container.getElementsByClassName("column-list-item"); - expect(platformList.length).toBe(11); // 2 columns are set to hidden + expect(platformList.length).toBe(13); // 2 of 13 columns are set to hidden but still show }); + it("renders the hidden column tooltip", async () => { + render( + noop} + onClose={noop} + /> + ); + await fireEvent.mouseEnter(screen.getByText("type")); + const tooltip = screen.getByText(/Not returned in SELECT */i); + expect(tooltip).toBeInTheDocument(); + }); it("renders the platform specific column tooltip", async () => { render( { + return ( + + Not returned in SELECT * FROM {selectedTableName} + + ); + }; + const renderPlatformFootnotes = (columnPlatforms: OsqueryPlatform[]) => { let platformsCopy; switch (columnPlatforms.length) { @@ -78,6 +86,7 @@ const renderTooltip = ( {FOOTNOTES.required} )} {column.requires_user_context && renderUserContextFootnote()} + {column.hidden && renderHiddenFootnote()} {column.platforms && renderPlatformFootnotes(column.platforms)} ); diff --git a/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/QueryTableColumns.tsx b/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/QueryTableColumns.tsx index a1454324f0..4f3f5f4c9b 100644 --- a/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/QueryTableColumns.tsx +++ b/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/QueryTableColumns.tsx @@ -35,17 +35,15 @@ const baseClass = "query-table-columns"; const QueryTableColumns = ({ columns }: IQueryTableColumnsProps) => { const { selectedOsqueryTable } = useContext(QueryContext); - const columnListItems = orderColumns(columns) - .filter((column) => !column.hidden) - .map((column) => { - return ( - - ); - }); + const columnListItems = orderColumns(columns).map((column) => { + return ( + + ); + }); return (
From ffddd76b9c182f870c1852c78a386a7bbe22115e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:35:42 -0500 Subject: [PATCH 40/83] Bump tar from 6.1.13 to 6.2.1 (#18180) --- yarn.lock | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 03932cde62..e585bc7203 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13040,10 +13040,10 @@ minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3, minipass@^3. dependencies: yallist "^4.0.0" -minipass@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.3.tgz#00bfbaf1e16e35e804f4aa31a7c1f6b8d9f0ee72" - integrity sha512-OW2r4sQ0sI+z5ckEt5c1Tri4xTgZwYDxpE54eqWlQloQRoWtXjqt9udJ5Z4dSv7wK+nfFI7FRXyCpBSft+gpFw== +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": version "7.0.4" @@ -15797,7 +15797,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -15811,13 +15811,6 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -15971,13 +15964,13 @@ tar-stream@^2.1.4: readable-stream "^3.1.1" tar@^6.0.2, tar@^6.1.11, tar@^6.1.13, tar@^6.1.2: - version "6.1.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" - integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" - minipass "^4.0.0" + minipass "^5.0.0" minizlib "^2.1.1" mkdirp "^1.0.3" yallist "^4.0.0" From 8d810a3a03076c1828ed8b879caf52388e6ddf4b Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Wed, 10 Apr 2024 12:36:49 -0500 Subject: [PATCH 41/83] Update contact.page.js (#18171) Co-authored-by: Eric --- website/assets/js/pages/contact.page.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/website/assets/js/pages/contact.page.js b/website/assets/js/pages/contact.page.js index 2b629aa352..541e375f7b 100644 --- a/website/assets/js/pages/contact.page.js +++ b/website/assets/js/pages/contact.page.js @@ -48,7 +48,11 @@ parasails.registerPage('contact', { if(this.formToShow === 'contact'){ this.formToDisplay = this.formToShow; } - if(this.prefillFormDataFromUserRecord){ + if(this.primaryBuyingSituation){ // If the user has a priamry buying situation set in their sesssion, pre-fill the form. + // Note: this will be overriden if the user is logged in and has a primaryBuyingSituation set in the database. + this.formData.primaryBuyingSituation = this.primaryBuyingSituation; + } + if(this.prefillFormDataFromUserRecord){// prefill from database this.formDataToPrefillForLoggedInUsers.emailAddress = this.me.emailAddress; this.formDataToPrefillForLoggedInUsers.firstName = this.me.firstName; this.formDataToPrefillForLoggedInUsers.lastName = this.me.lastName; @@ -59,12 +63,9 @@ parasails.registerPage('contact', { } this.formData = _.clone(this.formDataToPrefillForLoggedInUsers); } - if(window.location.search){ + if(window.location.search){// auto-clear query string (TODO: Document why we're doing this further. I think this shouldn't exist in the frontend code, instead in the hook. Because analytics corruption.) window.history.replaceState({}, document.title, '/contact' ); } - if(this.primaryBuyingSituation){ - this.formData.primaryBuyingSituation = this.primaryBuyingSituation;// prefill form - } }, mounted: async function() { //… From 21d1d90e3cc0d65d3bd5cf35a80b2ebb432567e1 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:54:31 -0400 Subject: [PATCH 42/83] Fleet UI: Select a query modal revamp (#18001) --- changes/12290-run-query-on-host | 1 + .../SelectQueryModal/SelectQueryModal.tsx | 104 ++++++++---------- .../modals/SelectQueryModal/_styles.scss | 88 ++++----------- 3 files changed, 70 insertions(+), 123 deletions(-) create mode 100644 changes/12290-run-query-on-host diff --git a/changes/12290-run-query-on-host b/changes/12290-run-query-on-host new file mode 100644 index 0000000000..a2459e28e0 --- /dev/null +++ b/changes/12290-run-query-on-host @@ -0,0 +1 @@ +- UI revamp: Run query on an online host diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx index e28d5e7b52..2012a29b9a 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx @@ -80,19 +80,25 @@ const SelectQueryModal = ({ const queriesCount = queriesFiltered.length; - const customQueryButton = () => { + const renderDescription = (): JSX.Element => { return ( - +
+ Choose a query to run on this host + {(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && ( + <> + {" "} + or{" "} + + + )} + . +
); }; - const results = (): JSX.Element => { + const renderResults = (): JSX.Element => { if (queryErrors) { return ; } @@ -105,10 +111,6 @@ const SelectQueryModal = ({ Expecting to see queries? Try again in a few seconds as the system catches up. -
- {(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && - customQueryButton()} -
); } @@ -131,55 +133,38 @@ const SelectQueryModal = ({ ); }); + return ( -
-
-
- -
- {(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && ( -
- OR - {customQueryButton()} -
- )} -
+ <> +
{queryList}
-
+ ); } if (queriesFilter && queriesCount === 0) { return ( -
-
-
- -
- {(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && ( -
- OR - {customQueryButton()} -
- )} + <> +
+
-
+
No queries match the current search criteria. @@ -188,7 +173,7 @@ const SelectQueryModal = ({ catches up.
-
+ ); } return <>; @@ -198,10 +183,13 @@ const SelectQueryModal = ({ - {results()} + <> + {renderDescription()} + {renderResults()} + ); }; diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss index 9cff3c71b3..393296cdfc 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss @@ -1,76 +1,34 @@ .select-query-modal { + min-height: 400px; + height: 80%; + overflow: hidden; + + .modal__content { + display: flex; + flex-direction: column; + gap: $pad-large; + height: 80%; + overflow: auto; + } + &__no-queries { - .info { - &__header { - margin: $pad-medium 0 $pad-medium 0; - } - &__data { - margin: 0 0 $pad-large 0; - } - } - } - - &__modal { - .info { - &__header { - display: block; - color: $core-fleet-black; - font-weight: $bold; - font-size: $x-small; - text-align: center; - } - - &__data { - display: block; - color: $core-fleet-black; - font-weight: normal; - font-size: $x-small; - text-align: center; - } - } - .modal-cta-wrap { - justify-content: center; - } - } - - &__filter-create-wrapper { display: flex; - align-items: center; - margin-bottom: 1.5rem; + flex-direction: column; + gap: $pad-medium; + margin: $pad-large 0; } - &__filter-queries { - flex-grow: 3; - position: relative; - - .form-field { - margin-bottom: 0; + .info { + &__header, + &__data { + display: block; + color: $core-fleet-black; + font-size: $x-small; + text-align: center; } - .input-field { - padding-left: 44px; - } - - .fleeticon { - position: absolute; - top: 10px; - left: 10px; - font-size: $medium; - color: $ui-fleet-black-25; - } - } - - &__create-query { - display: flex; - align-items: center; - - span { - margin: 15px; + &__header { font-weight: $bold; } } - - &__custom-query-button { - font-size: $x-small; - } } From dfe51474cdcb5a41b417d963caf76ae0ed8f68f6 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:55:38 -0700 Subject: [PATCH 43/83] BadRequest when no payloads present; BadRequest for invalid payload types (#18169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #17157 ![Screenshot 2024-04-09 at 4 53 22 PM](https://github.com/fleetdm/fleet/assets/61553566/a6a57f55-275f-4a06-89e7-085262d2672c) - [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 --- changes/17157-translate-api-error | 3 +++ server/service/integration_core_test.go | 5 +++++ server/service/translator.go | 17 ++++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 changes/17157-translate-api-error diff --git a/changes/17157-translate-api-error b/changes/17157-translate-api-error new file mode 100644 index 0000000000..f9c9573e15 --- /dev/null +++ b/changes/17157-translate-api-error @@ -0,0 +1,3 @@ +- Fix a bug where the translate API returned "forbidden" instead of "bad request" for an empty JSON body. +- Fixed an uncaught bug where "forbidden" would be returned for invalid payload type, which should + also be a bad request. diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 41b8f74a54..33e07a0465 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -690,6 +690,11 @@ func (s *integrationTestSuite) TestTranslator() { require.Len(t, payload.List, 1) assert.Equal(t, s.users[payload.List[0].Payload.Identifier].ID, payload.List[0].Payload.ID) + + // empty body + s.DoJSON("POST", "/api/latest/fleet/translate", &translatorRequest{}, http.StatusBadRequest, &payload) + + s.DoJSON("POST", "/api/latest/fleet/translate", &translatorRequest{List: []fleet.TranslatePayload{{Type: "notavalidtype", Payload: fleet.StringIdentifierToIDPayload{}}}}, http.StatusBadRequest, &payload) } func (s *integrationTestSuite) TestVulnerableSoftware() { diff --git a/server/service/translator.go b/server/service/translator.go index 078fbf09d9..f81177e6b4 100644 --- a/server/service/translator.go +++ b/server/service/translator.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "github.com/fleetdm/fleet/v4/server/fleet" ) @@ -61,6 +62,12 @@ func translateHostToID(ctx context.Context, ds fleet.Datastore, identifier strin } func (svc *Service) Translate(ctx context.Context, payloads []fleet.TranslatePayload) ([]fleet.TranslatePayload, error) { + if len(payloads) == 0 { + // skip auth since there is no case in which this request will make sense with no payloads + svc.authz.SkipAuthorization(ctx) + return nil, badRequest("payloads must not be empty") + } + var finalPayload []fleet.TranslatePayload for _, payload := range payloads { @@ -88,7 +95,15 @@ func (svc *Service) Translate(ctx context.Context, payloads []fleet.TranslatePay } translateFunc = translateHostToID default: - return nil, fleet.NewErrorf(fleet.ErrNoUnknownTranslate, "Type %s is unknown.", payload.Type) + // if no supported payload type, this is bad regardless of authorization + svc.authz.SkipAuthorization(ctx) + return nil, badRequestErr( + fmt.Sprintf("Type %s is unknown. ", payload.Type), + fleet.NewErrorf( + fleet.ErrNoUnknownTranslate, + "Type %s is unknown.", + payload.Type), + ) } id, err := translateFunc(ctx, svc.ds, payload.Payload.Identifier) From f02d4706f50cdd2103de08c0c8b6c230fcd1b382 Mon Sep 17 00:00:00 2001 From: George Karr Date: Wed, 10 Apr 2024 13:21:59 -0500 Subject: [PATCH 44/83] Adding changes for patch 4.48.2 (#18148) --- CHANGELOG.md | 13 ++++++++++--- charts/fleet/Chart.yaml | 2 +- charts/fleet/values.yaml | 2 +- infrastructure/dogfood/terraform/aws/variables.tf | 2 +- infrastructure/dogfood/terraform/gcp/variables.tf | 2 +- terraform/byo-vpc/README.md | 2 +- terraform/byo-vpc/byo-db/byo-ecs/variables.tf | 2 +- terraform/byo-vpc/byo-db/variables.tf | 2 +- terraform/byo-vpc/example/main.tf | 2 +- terraform/byo-vpc/variables.tf | 2 +- terraform/example/main.tf | 4 ++-- terraform/variables.tf | 2 +- tools/fleetctl-npm/package.json | 2 +- 13 files changed, 23 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bad9917bc8..988ff40126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Fleet 4.48.2 (Apr 09, 2024) + +### Bug fixes + +* Fixed an issue with the `20240327115617_CreateTableNanoDDMRequests` database migration where it could fail if the database did not default to the `utf8mb4_unicode_ci` collation. +* Fixed an issue with automatic release of the device after setup when a DDM profile is pending. + ## Fleet 4.48.1 (Apr 08, 2024) ### Bug fixes @@ -35,7 +42,7 @@ - Fixed a bug where valid MDM enrollments would show up as unmanaged (EnrollmentState 3). - Fixed flash message from closing when a modal closes. - Fixed a bug where OS version information would not get detected on Windows Server 2019. -- Fixed issue where getting host details failed when attempting to read the host's BitLocker status from the datastore. +- Fixed issue where getting host details failed when attempting to read the host's bitlocker status from the datastore. - Fixed false negative vulnerabilities on macOS Homebrew python packages. - Fixed styling of live query disabled warning. - Fixed issue where Windows MDM profile processing was skipping `` commands. @@ -44,11 +51,11 @@ - Fixed `GET fleet/os_versions` and `GET fleet/os_versions/[id]` so team users no longer have access to os versions on hosts from other teams. - `fleetctl gitops` now batch processes queries and policies. - Fixed UI bug to render the query platform correctly for queries imported from the standard query library. -- Fixed issue where Microsoft Edge was not reporting vulnerabilities. +- Fixed issue where microsoft edge was not reporting vulnerabilities. - Fixed a bug where all Windows MDM enrollments were detected as automatic. - Fixed a bug where `null` or excluded `smtp_settings` caused a UI 500. - Fixed query reports so they reset when there is a change to the selected platform or selected minimum osquery version. -- Fixed live query sort of SQL result sort for both string and numerical columns. +- Fixed live query sort of sql result sort for both string and numerical columns. ## Fleet 4.47.3 (Mar 26, 2024) diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index ca14828253..f076ca1a86 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.48.1 +appVersion: v4.48.2 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 13eaabda25..3720ff529d 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.48.1 # Version of Fleet to deploy +imageTag: v4.48.2 # 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 8b800b268f..fdccef8481 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.48.1" + default = "fleetdm/fleet:v4.48.2" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index 3a01f75209..88982208df 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.48.1" + default = "fleet:v4.48.2" } diff --git a/terraform/byo-vpc/README.md b/terraform/byo-vpc/README.md index 2ef3b21c3c..88fccdd0b9 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.48.1")
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)
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
},
"redis": {
"address": null,
"use_tls": true
},
"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.48.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)
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
},
"redis": {
"address": null,
"use_tls": true
},
"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.02.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.02.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/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf index d62b9deb61..7dc23f8b20 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.48.1") + image = optional(string, "fleetdm/fleet:v4.48.2") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index d8dec47a9b..1da53d22ce 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.48.1") + image = optional(string, "fleetdm/fleet:v4.48.2") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf index ed30ae8608..38d32e4cc0 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.48.1" + fleet_image = "fleetdm/fleet:v4.48.2" domain_name = "example.com" } diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index cbf4b49df9..f45a1229d9 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.48.1") + image = optional(string, "fleetdm/fleet:v4.48.2") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/example/main.tf b/terraform/example/main.tf index c503048375..adfafe3cfb 100644 --- a/terraform/example/main.tf +++ b/terraform/example/main.tf @@ -59,8 +59,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.48.1" - image = "fleetdm/fleet:v4.48.1" # override default to deploy the image you desire + # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.48.2" + image = "fleetdm/fleet:v4.48.2" # 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 347d339ee1..06cedeb8bd 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.48.1") + image = optional(string, "fleetdm/fleet:v4.48.2") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index f14ddce042..4067a3c169 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.48.1", + "version": "v4.48.2", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" From 3f32c870a61c98c1740370b9881edd2a96aa7903 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:50:08 -0700 Subject: [PATCH 45/83] Ignore leading/trailing whitespace when filtering entities; fix url param handling; test broad range of affected endpoints (#17455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #16615 and #18041 ### #16615: - Trim whitespace for backend filtered tables - Trim whitespace in UI for Queries table search string Screenshot 2024-03-06 at 11 46 56 AM Screenshot 2024-03-06 at 11 47 21 AM Screenshot 2024-03-06 at 3 18 31 PM Screenshot 2024-03-06 at 5 05 25 PM _______________________ ### #18041: #### Global: ![Screenshot 2024-04-03 at 12 21 19 PM](https://github.com/fleetdm/fleet/assets/61553566/9a45a3a6-4136-4851-b887-bd176299d0c2) #### Team: ![Screenshot 2024-04-03 at 12 56 36 PM](https://github.com/fleetdm/fleet/assets/61553566/793c70e7-384b-4705-a373-7e30a5188b03) #### **Similar fix for `GET` ...`vulnerabilities`** - [x] Changes file added for user-visible changes in `changes/` - [x] Add integration tests for all affected endpoints - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- ...ding-trailing-whitespace-in-search-strings | 2 + changes/18041-query-params-parsing | 6 + .../TableContainer/TableContainer.tsx | 2 +- .../components/QueriesTable/QueriesTable.tsx | 8 +- server/datastore/mysql/labels.go | 7 +- server/datastore/mysql/vulnerabilities.go | 8 +- .../datastore/mysql/vulnerabilities_test.go | 6 +- server/fleet/vulnerabilities.go | 7 +- server/service/endpoint_utils.go | 3 +- server/service/global_policies.go | 4 +- server/service/integration_core_test.go | 289 +++++++++++++++++- server/service/integration_enterprise_test.go | 134 +++++++- server/service/team_policies.go | 6 +- server/service/transport.go | 2 +- server/service/vulnerabilities_test.go | 2 +- 15 files changed, 435 insertions(+), 51 deletions(-) create mode 100644 changes/16615-ignore-leading-trailing-whitespace-in-search-strings create mode 100644 changes/18041-query-params-parsing diff --git a/changes/16615-ignore-leading-trailing-whitespace-in-search-strings b/changes/16615-ignore-leading-trailing-whitespace-in-search-strings new file mode 100644 index 0000000000..d77a60c1d4 --- /dev/null +++ b/changes/16615-ignore-leading-trailing-whitespace-in-search-strings @@ -0,0 +1,2 @@ +* Ignore leading and trailing whitespace when filtering Fleet entities by name + diff --git a/changes/18041-query-params-parsing b/changes/18041-query-params-parsing new file mode 100644 index 0000000000..f728cfe584 --- /dev/null +++ b/changes/18041-query-params-parsing @@ -0,0 +1,6 @@ +- Correctly parse query params for `GET` ...`policies/count`, `GET` ...`teams/:id/policies/count`, and + `GET` ...`vulnerabilities` +- Also updates `GET` ...`labels` to return `400` when the non-supported `query` url param is + included in the request. Previous behavior was to silently ignore that param and return `200`. + This is technically a minor breaking change, but one that breaks in the right direction, i.e., if + you see this break, you were using a URL param that was being ignored, which you are now aware of. diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index 8a99bb46de..4435260157 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -193,7 +193,7 @@ const TableContainer = ({ ); const onSearchQueryChange = (value: string) => { - setSearchQuery(value); + setSearchQuery(value.trim()); }; const hasPageIndexChangedRef = useRef(false); diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index d984dae7a7..d58bd75bed 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -123,7 +123,8 @@ const QueriesTable = ({ ? parseInt(queryParams?.inherited_page, 10) : 0)(); - // Never set as state as URL is source of truth + // Source of truth is state held within TableContainer. That state is initialized using URL + // params, then subsquent updates to that state are pushed to the URL. const searchQuery = initialSearchQuery; const platform = initialPlatform; const page = isInherited ? initialInheritedPage : initialPage; @@ -286,6 +287,7 @@ const QueriesTable = ({ const searchable = !(queriesList?.length === 0 && searchQuery === "") && !isInherited; + const trimmedSearchQuery = searchQuery.trim(); return columnConfigs && !isLoading ? (
0} - if len(vulns) > int(opt.PerPage) { + metaData = &fleet.PaginationMetadata{HasPreviousResults: opt.ListOptions.Page > 0} + if len(vulns) > int(opt.ListOptions.PerPage) { metaData.HasNextResults = true vulns = vulns[:len(vulns)-1] } @@ -304,7 +304,7 @@ func (ds *Datastore) CountVulnerabilities(ctx context.Context, opt fleet.VulnLis selectStmt = selectStmt + " AND cm.cisa_known_exploit = 1" } - if match := opt.MatchQuery; match != "" { + if match := opt.ListOptions.MatchQuery; match != "" { selectStmt, args = searchLike(selectStmt, args, match, "vhc.cve") } diff --git a/server/datastore/mysql/vulnerabilities_test.go b/server/datastore/mysql/vulnerabilities_test.go index 29236f4715..fca02a6bf7 100644 --- a/server/datastore/mysql/vulnerabilities_test.go +++ b/server/datastore/mysql/vulnerabilities_test.go @@ -342,7 +342,7 @@ func testVulnerabilitiesPagination(t *testing.T, ds *Datastore) { require.False(t, meta.HasPreviousResults) require.True(t, meta.HasNextResults) - opts.Page = 1 + opts.ListOptions.Page = 1 list, meta, err = ds.ListVulnerabilities(context.Background(), opts) require.NoError(t, err) require.Len(t, list, 2) @@ -399,8 +399,8 @@ func testListVulnerabilitiesSort(t *testing.T, ds *Datastore) { require.Equal(t, "CVE-2020-1237", list[3].CVE.CVE) require.Equal(t, "CVE-2020-1236", list[4].CVE.CVE) - opts.OrderKey = "published" - opts.OrderDirection = fleet.OrderAscending + opts.ListOptions.OrderKey = "published" + opts.ListOptions.OrderDirection = fleet.OrderAscending list, _, err = ds.ListVulnerabilities(context.Background(), opts) require.NoError(t, err) require.Len(t, list, 5) diff --git a/server/fleet/vulnerabilities.go b/server/fleet/vulnerabilities.go index b8980cb93a..a15468f31f 100644 --- a/server/fleet/vulnerabilities.go +++ b/server/fleet/vulnerabilities.go @@ -136,7 +136,8 @@ type VulnerabilityWithMetadata struct { } type VulnListOptions struct { - ListOptions + // ListOptions cannot be embedded in order to unmarshall with validation. + ListOptions ListOptions `url:"list_options"` IsEE bool ValidSortColumns []string TeamID uint `query:"team_id,optional"` @@ -144,11 +145,11 @@ type VulnListOptions struct { } func (opt VulnListOptions) HasValidSortColumn() bool { - if opt.OrderKey == "" || len(opt.ValidSortColumns) == 0 { + if opt.ListOptions.OrderKey == "" || len(opt.ValidSortColumns) == 0 { return true } for _, c := range opt.ValidSortColumns { - if c == opt.OrderKey { + if c == opt.ListOptions.OrderKey { return true } } diff --git a/server/service/endpoint_utils.go b/server/service/endpoint_utils.go index 712f4519f5..97ca541b87 100644 --- a/server/service/endpoint_utils.go +++ b/server/service/endpoint_utils.go @@ -94,7 +94,7 @@ type bodyDecoder interface { // struct has at least 1 json tag it'll unmarshall the body. If the struct has // a `url` tag with value list_options it'll gather fleet.ListOptions from the // URL (similarly for host_options, carve_options, user_options that derive -// from the common list_options). +// from the common list_options). Note that these behaviors do not work for embedded structs. // // Finally, any other `url` tag will be treated as a path variable (of the form // /path/{name} in the route's path) from the URL path pattern, and it'll be @@ -172,7 +172,6 @@ func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc { if err != nil { return nil, err } - switch urlTagValue { case "list_options": opts, err := listOptionsFromRequest(r) diff --git a/server/service/global_policies.go b/server/service/global_policies.go index a0d3afff15..2cecd274e9 100644 --- a/server/service/global_policies.go +++ b/server/service/global_policies.go @@ -157,7 +157,7 @@ func (svc Service) GetPolicyByIDQueries(ctx context.Context, policyID uint) (*fl // /////////////////////////////////////////////////////////////////////////////// type countGlobalPoliciesRequest struct { - fleet.ListOptions `url:"list_options"` + ListOptions fleet.ListOptions `url:"list_options"` } type countGlobalPoliciesResponse struct { Count int `json:"count"` @@ -168,7 +168,7 @@ func (r countGlobalPoliciesResponse) error() error { return r.Err } func countGlobalPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*countGlobalPoliciesRequest) - resp, err := svc.CountGlobalPolicies(ctx, req.MatchQuery) + resp, err := svc.CountGlobalPolicies(ctx, req.ListOptions.MatchQuery) if err != nil { return countGlobalPoliciesResponse{Err: err}, nil } diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 33e07a0465..273f4ec513 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -900,6 +900,7 @@ func (s *integrationTestSuite) TestVulnerableSoftware() { func (s *integrationTestSuite) TestGlobalPolicies() { t := s.T() + // create 3 hosts for i := 0; i < 3; i++ { _, err := s.ds.NewHost(context.Background(), &fleet.Host{ DetailUpdatedAt: time.Now(), @@ -923,6 +924,7 @@ func (s *integrationTestSuite) TestGlobalPolicies() { }) require.NoError(t, err) + // create a global policy gpParams := globalPolicyRequest{ QueryID: &qr.ID, Resolution: "some global resolution", @@ -936,6 +938,7 @@ func (s *integrationTestSuite) TestGlobalPolicies() { require.NotNil(t, gpResp.Policy.Resolution) assert.Equal(t, "some global resolution", *gpResp.Policy.Resolution) + // list global policies policiesResponse := listGlobalPoliciesResponse{} s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, &policiesResponse) require.Len(t, policiesResponse.Policies, 1) @@ -974,6 +977,27 @@ func (s *integrationTestSuite) TestGlobalPolicies() { s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp) require.Len(t, listHostsResp.Hosts, 1) + // count global policies + cGPRes := countGlobalPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &cGPRes) + assert.Equal(t, 1, cGPRes.Count) + + // count global policies with matching search query + cGPRes = countGlobalPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &cGPRes, "query", "estQue") + assert.Equal(t, 1, cGPRes.Count) + + // count global policies with matching search query containing leading/trailing whitespace + cGPRes = countGlobalPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &cGPRes, "query", " estQue ") + assert.Equal(t, 1, cGPRes.Count) + + // count global policies with non-matching search query + cGPRes = countGlobalPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &cGPRes, "query", "Query4") + assert.Equal(t, 0, cGPRes.Count) + + // delete the policy deletePolicyParams := deleteGlobalPoliciesRequest{IDs: []uint{policiesResponse.Policies[0].ID}} deletePolicyResp := deleteGlobalPoliciesResponse{} s.DoJSON("POST", "/api/latest/fleet/policies/delete", deletePolicyParams, http.StatusOK, &deletePolicyResp) @@ -1213,6 +1237,62 @@ func (s *integrationTestSuite) TestHostsCount() { ) assert.Equal(t, 1, resp.Count) + // there are 3 hosts, whos names end with ...local0, ...local1, ...local2 + // query by host name + + req = countHostsRequest{} + resp = countHostsResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp, + "query", "local0", + ) + assert.Equal(t, 1, resp.Count) + + req = countHostsRequest{} + resp = countHostsResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp, + "query", "local", + ) + assert.Equal(t, 3, resp.Count) + + // query by host name with leading/trailing whitespace + req = countHostsRequest{} + resp = countHostsResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp, + "query", " local0 ", + ) + assert.Equal(t, 1, resp.Count) + + req = countHostsRequest{} + resp = countHostsResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp, + "query", " local ", + ) + assert.Equal(t, 3, resp.Count) + + // query by host name leading/trailing whitespace and label + req = countHostsRequest{} + resp = countHostsResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp, + "label_id", fmt.Sprint(label.ID), + "query", " local0 ", + ) + assert.Equal(t, 1, resp.Count) + + req = countHostsRequest{} + resp = countHostsResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp, + "label_id", fmt.Sprint(label.ID), + // only host 0 has the label + "query", " local1 ", + ) + assert.Equal(t, 0, resp.Count) + // filter by low_disk_space criteria is ignored (premium-only filter) s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &resp, "low_disk_space", "32") require.Equal(t, len(hosts), resp.Count) @@ -1778,6 +1858,17 @@ func (s *integrationTestSuite) TestListHosts() { assert.Equal(t, "pass", policies[1].Response) } } + + // there are 3 hosts, whos names end with ...local0, ...local1, ...local2 + resp = listHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "query", "local0") + require.Len(t, resp.Hosts, 1) + require.Contains(t, resp.Hosts[0].Hostname, "local0") + resp = listHostsResponse{} + // now with leading/trailing whitespace + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "query", " local0 ") + require.Len(t, resp.Hosts, 1) + require.Contains(t, resp.Hosts[0].Hostname, "local0") } func (s *integrationTestSuite) TestInvites() { @@ -1854,6 +1945,26 @@ func (s *integrationTestSuite) TestInvites() { require.Len(t, listResp.Invites, 1) require.Equal(t, validInvite.ID, listResp.Invites[0].ID) + // list invites filtered by search query with leading/trailing whitespace + // matches name + listResp = listInvitesResponse{} + s.DoJSON("GET", "/api/latest/fleet/invites", nil, http.StatusOK, &listResp, "query", " some name ") + require.Len(t, listResp.Invites, 1) + require.Equal(t, validInvite.ID, listResp.Invites[0].ID) + + // list invites filtered by search query with leading/trailing whitespace + // matches email + listResp = listInvitesResponse{} + s.DoJSON("GET", "/api/latest/fleet/invites", nil, http.StatusOK, &listResp, "query", " some email ") + require.Len(t, listResp.Invites, 1) + require.Equal(t, validInvite.ID, listResp.Invites[0].ID) + + // list invites filtered by search query with leading/trailing whitespace + // matches nothing + listResp = listInvitesResponse{} + s.DoJSON("GET", "/api/latest/fleet/invites", nil, http.StatusOK, &listResp, "query", " no match ") + require.Len(t, listResp.Invites, 0) + // list invites, next page is empty listResp = listInvitesResponse{} s.DoJSON("GET", "/api/latest/fleet/invites", nil, http.StatusOK, &listResp, "page", "1", "per_page", "2") @@ -2354,8 +2465,9 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() { err = s.ds.AddHostsToTeam(context.Background(), &team1.ID, hosts) require.NoError(t, err) + tpName := "TestPolicy3" tpParams := teamPolicyRequest{ - Name: "TestQuery3", + Name: tpName, Query: "select * from osquery;", Description: "Some description", Resolution: "some team resolution", @@ -2365,7 +2477,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() { s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), tpParams, http.StatusOK, &tpResp) require.NotNil(t, tpResp.Policy) require.NotEmpty(t, tpResp.Policy.ID) - assert.Equal(t, "TestQuery3", tpResp.Policy.Name) + assert.Equal(t, tpName, tpResp.Policy.Name) assert.Equal(t, "select * from osquery;", tpResp.Policy.Query) assert.Equal(t, "Some description", tpResp.Policy.Description) require.NotNil(t, tpResp.Policy.Resolution) @@ -2374,9 +2486,10 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() { assert.Equal(t, "Test Name admin1@example.com", tpResp.Policy.AuthorName) assert.Equal(t, "admin1@example.com", tpResp.Policy.AuthorEmail) + tpNameNew := "TestPolicy4" mtpParams := modifyTeamPolicyRequest{ ModifyPolicyPayload: fleet.ModifyPolicyPayload{ - Name: ptr.String("TestQuery4"), + Name: ptr.String(tpNameNew), Query: ptr.String("select * from osquery_info;"), Description: ptr.String("Some description updated"), Resolution: ptr.String("some team resolution updated"), @@ -2385,7 +2498,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() { mtpResp := modifyTeamPolicyResponse{} s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, tpResp.Policy.ID), mtpParams, http.StatusOK, &mtpResp) require.NotNil(t, mtpResp.Policy) - assert.Equal(t, "TestQuery4", mtpResp.Policy.Name) + assert.Equal(t, tpNameNew, mtpResp.Policy.Name) assert.Equal(t, "select * from osquery_info;", mtpResp.Policy.Query) assert.Equal(t, "Some description updated", mtpResp.Policy.Description) require.NotNil(t, mtpResp.Policy.Resolution) @@ -2395,7 +2508,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() { gtpResp := getPolicyByIDResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, tpResp.Policy.ID), getPolicyByIDRequest{}, http.StatusOK, >pResp) require.NotNil(t, gtpResp.Policy) - assert.Equal(t, "TestQuery4", gtpResp.Policy.Name) + assert.Equal(t, tpNameNew, gtpResp.Policy.Name) assert.Equal(t, "select * from osquery_info;", gtpResp.Policy.Query) assert.Equal(t, "Some description updated", gtpResp.Policy.Description) require.NotNil(t, gtpResp.Policy.Resolution) @@ -2405,7 +2518,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() { policiesResponse := listTeamPoliciesResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), nil, http.StatusOK, &policiesResponse) require.Len(t, policiesResponse.Policies, 1) - assert.Equal(t, "TestQuery4", policiesResponse.Policies[0].Name) + assert.Equal(t, tpNameNew, policiesResponse.Policies[0].Name) assert.Equal(t, "select * from osquery_info;", policiesResponse.Policies[0].Query) assert.Equal(t, "Some description updated", policiesResponse.Policies[0].Description) require.NotNil(t, policiesResponse.Policies[0].Resolution) @@ -2413,6 +2526,23 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() { assert.Equal(t, "darwin", policiesResponse.Policies[0].Platform) require.Len(t, policiesResponse.InheritedPolicies, 0) + // test team policy count endpoint + tpCountResp := countTeamPoliciesResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tpCountResp) + assert.Equal(t, 1, tpCountResp.Count) + + tpCountResp = countTeamPoliciesResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tpCountResp, "query", tpNameNew) + assert.Equal(t, 1, tpCountResp.Count) + + tpCountResp = countTeamPoliciesResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tpCountResp, "query", " "+tpNameNew+" ") + assert.Equal(t, 1, tpCountResp.Count) + + tpCountResp = countTeamPoliciesResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tpCountResp, "query", " nomatch") + assert.Equal(t, 0, tpCountResp.Count) + listHostsURL := fmt.Sprintf("/api/latest/fleet/hosts?policy_id=%d", policiesResponse.Policies[0].ID) listHostsResp := listHostsResponse{} s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp) @@ -2935,6 +3065,20 @@ func (s *integrationTestSuite) TestScheduledQueries() { require.Len(t, listQryResp.Queries, 1) assert.Equal(t, query.Name, listQryResp.Queries[0].Name) + // listing with matching name returns that query + s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp, "query", query.Name) + require.Len(t, listQryResp.Queries, 1) + assert.Equal(t, query.Name, listQryResp.Queries[0].Name) + + // listing with matching name plus whitespace returns that query + s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp, "query", " "+query.Name+" ") + require.Len(t, listQryResp.Queries, 1) + assert.Equal(t, query.Name, listQryResp.Queries[0].Name) + + // listing with non-matching name returns nothing + s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp, "query", " nomatch") + require.Len(t, listQryResp.Queries, 0) + // Return that query by name s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries?query=%s", query.Name), nil, http.StatusOK, &listQryResp) require.Len(t, listQryResp.Queries, 1) @@ -3654,9 +3798,13 @@ func (s *integrationTestSuite) TestLabels() { assert.Len(t, summaryResp.Labels, builtInsCount+1) // next page is empty - s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "per_page", strconv.Itoa(builtInsCount+1), "page", "1", "query", t.Name()) + s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "per_page", strconv.Itoa(builtInsCount+1), "page", "1") assert.Len(t, listResp.Labels, 0) + // list labels with invalid query params + s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusBadRequest, &listResp, "per_page", strconv.Itoa(builtInsCount+1), "order_key", "id", "after", "1") + s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusBadRequest, &listResp, "per_page", strconv.Itoa(builtInsCount+1), "query", "no match query for this endpoint") + // create another label s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String(strings.ReplaceAll(t.Name(), "/", "_")), Query: ptr.String("select 1")}, http.StatusOK, &createResp) assert.NotZero(t, createResp.Label.ID) @@ -3679,7 +3827,7 @@ func (s *integrationTestSuite) TestLabels() { assert.Equal(t, hosts[1].ID, listHostsResp.Hosts[0].ID) assert.Equal(t, hosts[2].ID, listHostsResp.Hosts[1].ID) - // list hosts in label searching by display_name + // list hosts in label ordered by display_name s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusOK, &listHostsResp, "order_key", "display_name", "order_direction", "desc") assert.Len(t, listHostsResp.Hosts, len(hosts)) // first in the list is the last one, as the names are ordered with the index @@ -3701,6 +3849,11 @@ func (s *integrationTestSuite) TestLabels() { assert.Len(t, listHostsResp.Hosts, 1) assert.Equal(t, hosts[0].ID, listHostsResp.Hosts[0].ID) + // list hosts in label searching by email address with leading/trailing whitespace + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusOK, &listHostsResp, "query", " a@b.c ") + assert.Len(t, listHostsResp.Hosts, 1) + assert.Equal(t, hosts[0].ID, listHostsResp.Hosts[0].ID) + // count hosts in label order by display_name var countResp countHostsResponse s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "label_id", fmt.Sprint(lbl2.ID), "order_key", "display_name", "order_direction", "desc") @@ -4025,11 +4178,26 @@ func (s *integrationTestSuite) TestUsers() { t := s.T() + // existing users: + // {ID: 1, Name: "Test Name admin1@example.com", Email: "admin1@example.com", ...} + // {ID: 2, Name: "Test Name user1@example.com", Email: "user1@example.com", ...} + // {ID: 3, Name: "Test Name user2@example.com", Email: "user2@example.com", ...} + // list existing users var listResp listUsersResponse s.DoJSON("GET", "/api/latest/fleet/users", nil, http.StatusOK, &listResp) assert.Len(t, listResp.Users, len(s.users)) + // with non-matching query + s.DoJSON("GET", "/api/latest/fleet/users", nil, http.StatusOK, &listResp, "query", "noone") + assert.Len(t, listResp.Users, 0) + + // with matching query containing leading/trailing whitespaces + s.DoJSON("GET", "/api/latest/fleet/users", nil, http.StatusOK, &listResp, "query", " user ") + assert.Len(t, listResp.Users, 2) + assert.Equal(t, uint(2), listResp.Users[0].ID) + assert.Equal(t, uint(3), listResp.Users[1].ID) + // test available teams returned by `/me` endpoint for existing user var getMeResp getUserResponse ssn := createSession(t, 1, s.ds) @@ -6249,10 +6417,11 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() { require.NoError(t, s.ds.LoadHostSoftware(context.Background(), hosts[0], false)) // add CVEs for the first 10 software, which are the least used (lower hosts_count) + testCvePrefix := "cve-123-123" for i, sw := range hosts[0].Software[:10] { inserted, err := s.ds.InsertSoftwareVulnerability(context.Background(), fleet.SoftwareVulnerability{ SoftwareID: sw.ID, - CVE: fmt.Sprintf("cve-123-123-%03d", i), + CVE: fmt.Sprintf(testCvePrefix+"-%03d", i), }, fleet.NVDSource) require.NoError(t, err) require.True(t, inserted) @@ -6466,6 +6635,35 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() { s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "vulnerable", "true", "per_page", "5", "page", "2", "order_key", "hosts_count", "order_direction", "desc") assertVersionsResp(versResp, nil, time.Time{}, "", expectedVulnVersionsCount) + // /software/versions filtered by name, version, cve (`/software` is deprecated) + // TODO(jacob) use `assertVersionsResp` + versionsResp := listSoftwareVersionsResponse{} + s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", sws[0].Name) + assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1) + // with whitespace + s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", " "+sws[0].Name+"\n") + assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1) + + s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", sws[0].Version) + assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1) + // with whitespace + s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", "\n"+sws[0].Version+" ") + assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1) + + // All 10 CVEs added to the first 10 software have the same cvePrefix, so should return all + // 10 vulnerable software versions + s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", testCvePrefix) + require.Len(t, versionsResp.Software, 10) + require.Equal(t, 10, versionsResp.Count) + // TODO(jacob) use `assertVersionsResp` + // assertVersionsResp(versionsResp, sws[:10], hostsCountTs, "", 10, 1) + // with whitespace + s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", " "+testCvePrefix+"\n") + require.Len(t, versionsResp.Software, 10) + require.Equal(t, 10, versionsResp.Count) + // TODO(jacob) use `assertVersionsResp` + // assertVersionsResp(versionsResp, sws[:10], hostsCountTs, "", 10, 1) + // filter by the team, 2 by page lsResp = listSoftwareResponse{} s.DoJSON( @@ -7403,6 +7601,7 @@ func (s *integrationTestSuite) TestHostsReportDownload() { t := s.T() ctx := context.Background() + // create 3 hosts (deb, rhel, linux) hosts := s.createHosts(t) err := s.ds.ApplyLabelSpecs(context.Background(), []*fleet.LabelSpec{ {Name: t.Name(), LabelMembershipType: fleet.LabelMembershipTypeManual, Query: "select 1", Hosts: []string{hosts[2].Hostname}}, @@ -7523,6 +7722,14 @@ func (s *integrationTestSuite) TestHostsReportDownload() { require.Len(t, rows, 2) // headers + matching host require.Contains(t, rows[1], hosts[0].Hostname) + // search criteria including search query with leading/trailing whitespace are applied + res = s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv", "query", " local0 ", "columns", "hostname") + rows, err = csv.NewReader(res.Body).ReadAll() + res.Body.Close() + require.NoError(t, err) + require.Len(t, rows, 2) // headers + matching host + require.Contains(t, rows[1], hosts[0].Hostname) + // with device mapping results res = s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv", "columns", "id,hostname,device_mapping") rawCSV, err := io.ReadAll(res.Body) @@ -7548,6 +7755,15 @@ func (s *integrationTestSuite) TestHostsReportDownload() { require.Len(t, rows, 2) // headers + member host require.Contains(t, rows[1], hosts[2].Hostname) + // with a label id and a search query with leading/trailing whitespace + res = s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv", "columns", "hostname", "label_id", fmt.Sprintf("%d", customLabelID), "query", " local2 ") + rows, err = csv.NewReader(res.Body).ReadAll() + res.Body.Close() + require.NoError(t, err) + require.Len(t, rows, 2) // headers + member host + // hosts[2] is both matched by the trimmed query and in the provided label + require.Contains(t, rows[1], hosts[2].Hostname) + // with a software version id res = s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv", "columns", "hostname", "software_version_id", fmt.Sprint(fooV1ID)) rows, err = csv.NewReader(res.Body).ReadAll() @@ -7929,7 +8145,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() { // insert software vuln outside of host scope _, err = s.ds.InsertSoftwareVulnerability(context.Background(), fleet.SoftwareVulnerability{ SoftwareID: sw2.ID, - CVE: "CVE-2021-1236", + CVE: "CVE-2021-1246", }, fleet.NVDSource) require.NoError(t, err) @@ -7953,12 +8169,12 @@ func (s *integrationTestSuite) TestListVulnerabilities() { Description: "Test CVE 2021-1235", }, { - CVE: "CVE-2021-1236", + CVE: "CVE-2021-1246", CVSSScore: ptr.Float64(5.4), EPSSProbability: ptr.Float64(0.6), CISAKnownExploit: ptr.Bool(false), Published: ptr.Time(mockTime), - Description: "Test CVE 2021-1236", + Description: "Test CVE 2021-1246", }, }) require.NoError(t, err) @@ -7966,6 +8182,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() { err = s.ds.UpdateVulnerabilityHostCounts(context.Background()) require.NoError(t, err) + // test list s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp) require.Empty(t, resp.Err) require.Len(s.T(), resp.Vulnerabilities, 3) @@ -7987,9 +8204,9 @@ func (s *integrationTestSuite) TestListVulnerabilities() { HostCount: 1, DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1235", }, - "CVE-2021-1236": { + "CVE-2021-1246": { HostCount: 1, - DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1236", + DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1246", }, } @@ -8001,6 +8218,48 @@ func (s *integrationTestSuite) TestListVulnerabilities() { require.Empty(t, vuln.CVSSScore) } + // test list with matching query containing leading/trailing whitespace + // TODO(jacob) - this may be another parsing bug + s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "query", " 123 ") + require.Empty(t, resp.Err) + require.Len(s.T(), resp.Vulnerabilities, 2) + require.Equal(t, resp.Count, uint(2)) + require.False(t, resp.Meta.HasPreviousResults) + require.False(t, resp.Meta.HasNextResults) + + expected = map[string]struct { + fleet.CVEMeta + HostCount uint + DetailsLink string + Source fleet.VulnerabilitySource + }{ + "CVE-2021-1234": { + HostCount: 1, + DetailsLink: "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-1234", + }, + "CVE-2021-1235": { + HostCount: 1, + DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1235", + }, + // ...1246 should not match the query + } + + for _, vuln := range resp.Vulnerabilities { + expectedVuln, ok := expected[vuln.CVE.CVE] + require.True(t, ok) + require.Equal(t, expectedVuln.HostCount, vuln.HostsCount) + require.Equal(t, expectedVuln.DetailsLink, vuln.DetailsLink) + require.Empty(t, vuln.CVSSScore) + } + + // test list with non-matching query + s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "query", "CVB") + require.Empty(t, resp.Err) + require.Len(s.T(), resp.Vulnerabilities, 0) + require.Equal(t, resp.Count, uint(0)) + require.False(t, resp.Meta.HasPreviousResults) + require.False(t, resp.Meta.HasNextResults) + // Test Team Filter s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "team_id", "1") require.Len(s.T(), resp.Vulnerabilities, 0) @@ -8033,7 +8292,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() { s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/foobar", nil, http.StatusNotFound, &gResp) // Valid CVE but not in team scope - s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1236", nil, http.StatusNotFound, &gResp, "team_id", fmt.Sprintf("%d", team.ID)) + s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1246", nil, http.StatusNotFound, &gResp, "team_id", fmt.Sprintf("%d", team.ID)) // Invalid TeamID s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1234", nil, http.StatusForbidden, &gResp, "team_id", "100") diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 2b53cff98b..7504218bb4 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -1009,7 +1009,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { } s.DoJSON("POST", "/api/latest/fleet/teams", team4, http.StatusUnprocessableEntity, &tmResp) - // list teams + // list teams matching name of one team var listResp listTeamsResponse s.DoJSON("GET", "/api/latest/fleet/teams", nil, http.StatusOK, &listResp, "query", name, "per_page", "2") require.Len(t, listResp.Teams, 1) @@ -1017,6 +1017,16 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { assert.NotNil(t, listResp.Teams[0].Config.AgentOptions) tm1ID := listResp.Teams[0].ID + // same as above, with leading/trailing whitespace + s.DoJSON("GET", "/api/latest/fleet/teams", nil, http.StatusOK, &listResp, "query", " "+name+" ", "per_page", "2") + require.Len(t, listResp.Teams, 1) + assert.Equal(t, team.Name, listResp.Teams[0].Name) + assert.NotNil(t, listResp.Teams[0].Config.AgentOptions) + + // same as above, no match + s.DoJSON("GET", "/api/latest/fleet/teams", nil, http.StatusOK, &listResp, "query", " nope ", "per_page", "2") + require.Len(t, listResp.Teams, 0) + // get team var getResp getTeamResponse s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", tm1ID), nil, http.StatusOK, &getResp) @@ -3666,6 +3676,7 @@ func (s *integrationEnterpriseTestSuite) TestMDMNotConfiguredEndpoints() { } func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() { + t := s.T() fields := []string{"Query", "Name", "Description", "Resolution", "Platform", "Critical"} createPol1 := &globalPolicyResponse{} @@ -3678,7 +3689,7 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() { Critical: true, } s.DoJSON("POST", "/api/latest/fleet/policies", createPol1Req, http.StatusOK, &createPol1) - allEqual(s.T(), createPol1Req, createPol1.Policy, fields...) + allEqual(t, createPol1Req, createPol1.Policy, fields...) createPol2 := &globalPolicyResponse{} createPol2Req := &globalPolicyRequest{ @@ -3690,16 +3701,22 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() { Critical: false, } s.DoJSON("POST", "/api/latest/fleet/policies", createPol2Req, http.StatusOK, &createPol2) - allEqual(s.T(), createPol2Req, createPol2.Policy, fields...) + allEqual(t, createPol2Req, createPol2.Policy, fields...) listPol := &listGlobalPoliciesResponse{} s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, listPol) - require.Len(s.T(), listPol.Policies, 2) + require.Len(t, listPol.Policies, 2) sort.Slice(listPol.Policies, func(i, j int) bool { return listPol.Policies[i].Name < listPol.Policies[j].Name }) - require.Equal(s.T(), createPol1.Policy, listPol.Policies[0]) - require.Equal(s.T(), createPol2.Policy, listPol.Policies[1]) + require.Equal(t, createPol1.Policy, listPol.Policies[0]) + require.Equal(t, createPol2.Policy, listPol.Policies[1]) + + // match policy by name with leading/trailing whitespace + listPolByName := &listGlobalPoliciesResponse{} + s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, listPolByName, "query", " name1 ") + require.Len(t, listPolByName.Policies, 1) + require.Equal(t, listPolByName.Policies[0].Name, "name1") patchPol1Req := &modifyGlobalPolicyRequest{ ModifyPolicyPayload: fleet.ModifyPolicyPayload{ @@ -3713,7 +3730,7 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() { } patchPol1 := &modifyGlobalPolicyResponse{} s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", createPol1.Policy.ID), patchPol1Req, http.StatusOK, patchPol1) - allEqual(s.T(), patchPol1Req, patchPol1.Policy, fields...) + allEqual(t, patchPol1Req, patchPol1.Policy, fields...) patchPol2Req := &modifyGlobalPolicyRequest{ ModifyPolicyPayload: fleet.ModifyPolicyPayload{ @@ -3727,21 +3744,21 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() { } patchPol2 := &modifyGlobalPolicyResponse{} s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", createPol2.Policy.ID), patchPol2Req, http.StatusOK, patchPol2) - allEqual(s.T(), patchPol2Req, patchPol2.Policy, fields...) + allEqual(t, patchPol2Req, patchPol2.Policy, fields...) listPol = &listGlobalPoliciesResponse{} s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, listPol) - require.Len(s.T(), listPol.Policies, 2) + require.Len(t, listPol.Policies, 2) sort.Slice(listPol.Policies, func(i, j int) bool { return listPol.Policies[i].Name < listPol.Policies[j].Name }) // not using require.Equal because "PATCH policies" returns the wrong updated timestamp. - allEqual(s.T(), patchPol1.Policy, listPol.Policies[0], fields...) - allEqual(s.T(), patchPol2.Policy, listPol.Policies[1], fields...) + allEqual(t, patchPol1.Policy, listPol.Policies[0], fields...) + allEqual(t, patchPol2.Policy, listPol.Policies[1], fields...) getPol2 := &getPolicyByIDResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/policies/%d", createPol2.Policy.ID), nil, http.StatusOK, getPol2) - require.Equal(s.T(), listPol.Policies[1], getPol2.Policy) + require.Equal(t, listPol.Policies[1], getPol2.Policy) } func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() { @@ -7069,6 +7086,99 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() { }, }, resp.SoftwareTitles) + // match software title by name with leading whitespace + resp = listSoftwareTitlesResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/software/titles", + listSoftwareTitlesRequest{}, + http.StatusOK, &resp, + "query", " ba", + ) + require.Equal(t, 2, resp.Count) + require.NotEmpty(t, resp.CountsUpdatedAt) + softwareTitlesMatch([]fleet.SoftwareTitle{ + { + Name: "bar", + Source: "apps", + VersionsCount: 1, + HostsCount: 1, + Versions: []fleet.SoftwareVersion{ + {Version: "0.0.4", Vulnerabilities: &fleet.SliceString{"cve-123-123-132"}}, + }, + }, + { + Name: "baz", + Source: "deb_packages", + VersionsCount: 1, + HostsCount: 1, + Versions: []fleet.SoftwareVersion{ + {Version: "0.0.5", Vulnerabilities: nil}, + }, + }, + }, resp.SoftwareTitles) + + // match software title by name with trailing whitespace + resp = listSoftwareTitlesResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/software/titles", + listSoftwareTitlesRequest{}, + http.StatusOK, &resp, + "query", "ba ", + ) + require.Equal(t, 2, resp.Count) + require.NotEmpty(t, resp.CountsUpdatedAt) + softwareTitlesMatch([]fleet.SoftwareTitle{ + { + Name: "bar", + Source: "apps", + VersionsCount: 1, + HostsCount: 1, + Versions: []fleet.SoftwareVersion{ + {Version: "0.0.4", Vulnerabilities: &fleet.SliceString{"cve-123-123-132"}}, + }, + }, + { + Name: "baz", + Source: "deb_packages", + VersionsCount: 1, + HostsCount: 1, + Versions: []fleet.SoftwareVersion{ + {Version: "0.0.5", Vulnerabilities: nil}, + }, + }, + }, resp.SoftwareTitles) + + // match software title by name with leading and trailing whitespace + resp = listSoftwareTitlesResponse{} + s.DoJSON( + "GET", "/api/latest/fleet/software/titles", + listSoftwareTitlesRequest{}, + http.StatusOK, &resp, + "query", " ba ", + ) + require.Equal(t, 2, resp.Count) + require.NotEmpty(t, resp.CountsUpdatedAt) + softwareTitlesMatch([]fleet.SoftwareTitle{ + { + Name: "bar", + Source: "apps", + VersionsCount: 1, + HostsCount: 1, + Versions: []fleet.SoftwareVersion{ + {Version: "0.0.4", Vulnerabilities: &fleet.SliceString{"cve-123-123-132"}}, + }, + }, + { + Name: "baz", + Source: "deb_packages", + VersionsCount: 1, + HostsCount: 1, + Versions: []fleet.SoftwareVersion{ + {Version: "0.0.5", Vulnerabilities: nil}, + }, + }, + }, resp.SoftwareTitles) + // find the ID of "foo" s.DoJSON( "GET", "/api/latest/fleet/software/titles", diff --git a/server/service/team_policies.go b/server/service/team_policies.go index 15a7a90acd..7786c7fe68 100644 --- a/server/service/team_policies.go +++ b/server/service/team_policies.go @@ -154,8 +154,8 @@ func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts flee ///////////////////////////////////////////////////////////////////////////////// type countTeamPoliciesRequest struct { - fleet.ListOptions `url:"list_options"` - TeamID uint `url:"team_id"` + ListOptions fleet.ListOptions `url:"list_options"` + TeamID uint `url:"team_id"` } type countTeamPoliciesResponse struct { @@ -167,7 +167,7 @@ func (r countTeamPoliciesResponse) error() error { return r.Err } func countTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*countTeamPoliciesRequest) - resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.MatchQuery) + resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.ListOptions.MatchQuery) if err != nil { return countTeamPoliciesResponse{Err: err}, nil } diff --git a/server/service/transport.go b/server/service/transport.go index e7b2ece2e1..8db2a4b976 100644 --- a/server/service/transport.go +++ b/server/service/transport.go @@ -186,7 +186,7 @@ func listOptionsFromRequest(r *http.Request) (fleet.ListOptions, error) { PerPage: uint(perPage), OrderKey: orderKey, OrderDirection: orderDirection, - MatchQuery: query, + MatchQuery: strings.TrimSpace(query), After: afterString, }, nil } diff --git a/server/service/vulnerabilities_test.go b/server/service/vulnerabilities_test.go index 0e5ee6aa53..fd0a7e2eec 100644 --- a/server/service/vulnerabilities_test.go +++ b/server/service/vulnerabilities_test.go @@ -46,7 +46,7 @@ func TestListVulnerabilities(t *testing.T) { require.Contains(t, err.Error(), "invalid order key") // valid order key - opts.OrderKey = "cve" + opts.ListOptions.OrderKey = "cve" _, _, err = svc.ListVulnerabilities(ctx, opts) require.NoError(t, err) }) From 419dc84108309c6f64fc73bc54021e5573baac7c Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:20:28 -0500 Subject: [PATCH 46/83] Add Communicate Fleet's potential energy (#18195) --- handbook/digital-experience/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md index 4463438ce7..b75210bbc7 100644 --- a/handbook/digital-experience/README.md +++ b/handbook/digital-experience/README.md @@ -167,6 +167,31 @@ If the action fails, please complete the following steps: 3. Head to the fleetdm/fleet GitHub repository and re-run the Deploy Fleet Website action. +### 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: +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." + + +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. + + ### Refresh event calendar Fleet's public relations firm is directly responsible for the accuracy of event locations, attendance dates, and CFP deadlines in the event strategy workbook. At the end of every quarter, the PR firm updates every event in the ["Event strategy workbook"](https://docs.google.com/spreadsheets/d/1YQXAX2Q_WnGkAwMYjMbQpV3nbCj7gOBbv7Y0u4twxzQ/edit) (private Google doc) by following these steps: 1. Visit the latest website for each event. From 87a73682e381f6090c05ce21e3329bfaaa94919f Mon Sep 17 00:00:00 2001 From: Benjamin Edwards Date: Wed, 10 Apr 2024 16:10:03 -0400 Subject: [PATCH 47/83] firehose logging module -- add compression format variable (#18202) --- .../.terraform.lock.hcl | 24 +++++++++++++++++++ .../logging-destination-firehose/README.md | 3 ++- .../logging-destination-firehose/main.tf | 10 ++++---- .../logging-destination-firehose/variables.tf | 4 ++++ 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 terraform/addons/logging-destination-firehose/.terraform.lock.hcl diff --git a/terraform/addons/logging-destination-firehose/.terraform.lock.hcl b/terraform/addons/logging-destination-firehose/.terraform.lock.hcl new file mode 100644 index 0000000000..3d971a357d --- /dev/null +++ b/terraform/addons/logging-destination-firehose/.terraform.lock.hcl @@ -0,0 +1,24 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.44.0" + hashes = [ + "h1:QqMTKuyylmJ633mwNheXdFupfd5sozqCUTTSj89pnm8=", + "zh:1224a42bb04574785549b89815d98bda11f6e9992352fc6c36c5622f3aea91c0", + "zh:2a8d1095a2f1ab097f516d9e7e0d289337849eebb3fcc34f075070c65063f4fa", + "zh:46cce11150eb4934196d9bff693b72d0494c85917ceb3c2914d5ff4a785af861", + "zh:4a7c15d585ee747d17f4b3904851cd95cfbb920fa197aed3df78e8d7ef9609b6", + "zh:508f1a85a0b0f93bf26341207d809bd55b60c8fdeede40097d91f30111fc6f5d", + "zh:52f968ffc21240213110378d0ffb298cbd23e9157a6d01dfac5a4360492d69c2", + "zh:5e9846b48ef03eb59541049e81b15cae8bc7696a3779ae4a5412fdce60bb24e0", + "zh:850398aecaf7dc0231fc320fdd6dffe41836e07a54c8c7b40eb28e7525d3c0a9", + "zh:8f87eeb05bdd1b873b6cfb3898dfad6402ac180dfa3c8f9754df8f85dcf92ca6", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:c726b87cd6ed111536f875dccedecff21abc802a4087264515ffab113cac36dc", + "zh:d57ea706d2f98b93c7b05b0c6bc3420de8e8cf2d0b6703085dc15ed239b2cc49", + "zh:d5d1a21246e68c2a7a04c5619eb0ad5a81644f644c432cb690537b816a156de2", + "zh:e869904cac41114b7e4ee66bcd2ce4585ed15ca842040a60cb47119f69472c91", + "zh:f1a09f2f3ea72cbe795b865cf31ad9b1866a536a8050cf0bb93d3fa51069582e", + ] +} diff --git a/terraform/addons/logging-destination-firehose/README.md b/terraform/addons/logging-destination-firehose/README.md index c7ce437a30..054cc69b3a 100644 --- a/terraform/addons/logging-destination-firehose/README.md +++ b/terraform/addons/logging-destination-firehose/README.md @@ -9,7 +9,7 @@ No requirements. | Name | Version | |------|---------| -| [aws](#provider\_aws) | 5.25.0 | +| [aws](#provider\_aws) | 5.44.0 | ## Modules @@ -46,6 +46,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [compression\_format](#input\_compression\_format) | n/a | `string` | `"UNCOMPRESSED"` | no | | [osquery\_results\_s3\_bucket](#input\_osquery\_results\_s3\_bucket) | n/a |
object({
name = optional(string, "fleet-osquery-results-archive")
expires_days = optional(number, 1)
})
|
{
"expires_days": 1,
"name": "fleet-osquery-results-archive"
}
| no | | [osquery\_status\_s3\_bucket](#input\_osquery\_status\_s3\_bucket) | n/a |
object({
name = optional(string, "fleet-osquery-status-archive")
expires_days = optional(number, 1)
})
|
{
"expires_days": 1,
"name": "fleet-osquery-status-archive"
}
| no | diff --git a/terraform/addons/logging-destination-firehose/main.tf b/terraform/addons/logging-destination-firehose/main.tf index 8f8bd958e6..fcefd188de 100644 --- a/terraform/addons/logging-destination-firehose/main.tf +++ b/terraform/addons/logging-destination-firehose/main.tf @@ -151,8 +151,9 @@ resource "aws_kinesis_firehose_delivery_stream" "osquery_results" { destination = "extended_s3" extended_s3_configuration { - role_arn = aws_iam_role.firehose-results.arn - bucket_arn = aws_s3_bucket.osquery-results.arn + compression_format = var.compression_format + role_arn = aws_iam_role.firehose-results.arn + bucket_arn = aws_s3_bucket.osquery-results.arn } } @@ -161,8 +162,9 @@ resource "aws_kinesis_firehose_delivery_stream" "osquery_status" { destination = "extended_s3" extended_s3_configuration { - role_arn = aws_iam_role.firehose-status.arn - bucket_arn = aws_s3_bucket.osquery-status.arn + compression_format = var.compression_format + role_arn = aws_iam_role.firehose-status.arn + bucket_arn = aws_s3_bucket.osquery-status.arn } } diff --git a/terraform/addons/logging-destination-firehose/variables.tf b/terraform/addons/logging-destination-firehose/variables.tf index 3b3ca4524a..c97e2c36f6 100644 --- a/terraform/addons/logging-destination-firehose/variables.tf +++ b/terraform/addons/logging-destination-firehose/variables.tf @@ -19,3 +19,7 @@ variable "osquery_status_s3_bucket" { expires_days = 1 } } + +variable "compression_format" { + default = "UNCOMPRESSED" +} From 63ca8f44be4c39c11113267650c05ddbc2d4c1b3 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:19:18 -0400 Subject: [PATCH 48/83] Update types to config and remove unneeded keys being sent to API (#18183) --- .../admin/OrgSettingsPage/OrgSettingsPage.tsx | 3 ++- .../cards/Advanced/Advanced.tsx | 12 ------------ .../OrgSettingsPage/cards/Agents/Agents.tsx | 18 +++++++++++++----- .../cards/FleetDesktop/FleetDesktop.tsx | 2 +- .../admin/OrgSettingsPage/cards/Smtp/Smtp.tsx | 3 --- .../cards/Statistics/Statistics.tsx | 3 --- .../cards/WebAddress/WebAddress.tsx | 2 -- .../admin/OrgSettingsPage/cards/constants.ts | 8 +++++++- 8 files changed, 23 insertions(+), 28 deletions(-) diff --git a/frontend/pages/admin/OrgSettingsPage/OrgSettingsPage.tsx b/frontend/pages/admin/OrgSettingsPage/OrgSettingsPage.tsx index e2f1bdc575..f0afb86a75 100644 --- a/frontend/pages/admin/OrgSettingsPage/OrgSettingsPage.tsx +++ b/frontend/pages/admin/OrgSettingsPage/OrgSettingsPage.tsx @@ -14,6 +14,7 @@ import paths from "router/paths"; import SideNav from "../components/SideNav"; import ORG_SETTINGS_NAV_ITEMS from "./OrgSettingsNavItems"; +import { DeepPartial } from "./cards/constants"; interface IOrgSettingsPageProps { params: Params; @@ -50,7 +51,7 @@ const OrgSettingsPage = ({ params, router }: IOrgSettingsPageProps) => { }); const onFormSubmit = useCallback( - (formUpdates: Partial) => { + (formUpdates: DeepPartial) => { if (!appConfig) { return false; } diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx index bd0704ce43..dc882ba0be 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx @@ -67,23 +67,11 @@ const Advanced = ({ // Formatting of API not UI const formDataToSubmit = { server_settings: { - server_url: appConfig.server_settings.server_url || "", live_query_disabled: disableLiveQuery, - enable_analytics: appConfig.server_settings.enable_analytics, query_reports_disabled: disableQueryReports, scripts_disabled: disableScripts, }, smtp_settings: { - enable_smtp: appConfig.smtp_settings?.enable_smtp || false, - sender_address: appConfig.smtp_settings?.sender_address || "", - server: appConfig.smtp_settings?.server || "", - port: Number(appConfig.smtp_settings?.port), - authentication_type: appConfig.smtp_settings?.authentication_type || "", - user_name: appConfig.smtp_settings?.user_name || "", - password: appConfig.smtp_settings?.password || "", - enable_ssl_tls: appConfig.smtp_settings?.enable_ssl_tls || false, - authentication_method: - appConfig.smtp_settings?.authentication_method || "", domain, verify_ssl_certs: verifySSLCerts, enable_start_tls: enableStartTLS, diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Agents/Agents.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Agents/Agents.tsx index dea3fc70ac..c0acbfc680 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Agents/Agents.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Agents/Agents.tsx @@ -13,7 +13,15 @@ import YamlAce from "components/YamlAce"; import CustomLink from "components/CustomLink"; import SectionHeader from "components/SectionHeader"; -import { IAppConfigFormProps, IAppConfigFormErrors } from "../constants"; +import { IAppConfigFormProps } from "../constants"; + +interface IAgentOptionsFormData { + agentOptions?: string; +} + +interface IAgentOptionsFormErrors { + agent_options?: string | null; +} const baseClass = "app-config-form"; @@ -25,10 +33,10 @@ const Agents = ({ }: IAppConfigFormProps): JSX.Element => { const { ADMIN_TEAMS } = paths; - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ agentOptions: agentOptionsToYaml(appConfig.agent_options), }); - const [formErrors, setFormErrors] = useState({}); + const [formErrors, setFormErrors] = useState({}); const { agentOptions } = formData; @@ -37,7 +45,7 @@ const Agents = ({ }; const validateForm = () => { - const errors: IAppConfigFormErrors = {}; + const errors: IAgentOptionsFormErrors = {}; if (agentOptions) { const { error: yamlError, valid: yamlValid } = validateYaml(agentOptions); @@ -58,7 +66,7 @@ const Agents = ({ evt.preventDefault(); // Formatting of API not UI and allows empty agent options - const formDataToSubmit = agentOptions + const formDataToSubmit: any = agentOptions ? { agent_options: yaml.load(agentOptions), } diff --git a/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx b/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx index 53e237d878..890da8a713 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx @@ -52,7 +52,7 @@ const FleetDesktop = ({ const onFormSubmit = (evt: React.MouseEvent) => { evt.preventDefault(); - const formDataForAPI: Pick = { + const formDataForAPI = { fleet_desktop: { transparency_url: formData.transparencyUrl, }, diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Smtp/Smtp.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Smtp/Smtp.tsx index 45af193395..2d24f7a44c 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Smtp/Smtp.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Smtp/Smtp.tsx @@ -116,9 +116,6 @@ const Smtp = ({ password: smtpPassword, enable_ssl_tls: smtpEnableSSLTLS, authentication_method: smtpAuthenticationMethod, - domain: appConfig.smtp_settings?.domain || "", - verify_ssl_certs: appConfig.smtp_settings?.verify_ssl_certs || false, - enable_start_tls: appConfig.smtp_settings?.enable_start_tls, }, }; diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Statistics/Statistics.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Statistics/Statistics.tsx index c79b688ae2..9537594ac2 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Statistics/Statistics.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Statistics/Statistics.tsx @@ -31,9 +31,6 @@ const Statistics = ({ // Formatting of API not UI const formDataToSubmit = { server_settings: { - server_url: appConfig.server_settings.server_url || "", - live_query_disabled: - appConfig.server_settings.live_query_disabled || false, enable_analytics: enableUsageStatistics, }, }; diff --git a/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx b/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx index eca866581e..2709f67d2a 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx @@ -50,8 +50,6 @@ const WebAddress = ({ const formDataToSubmit = { server_settings: { server_url: serverURL, - live_query_disabled: appConfig.server_settings.live_query_disabled, - enable_analytics: appConfig.server_settings.enable_analytics, }, }; diff --git a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts index 33b3d6a0d3..3e314baa83 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts +++ b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts @@ -2,11 +2,17 @@ import { IConfig } from "interfaces/config"; export const DEFAULT_TRANSPARENCY_URL = "https://fleetdm.com/transparency"; +export type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; + export interface IAppConfigFormProps { appConfig: IConfig; isPremiumTier?: boolean; isUpdatingSettings?: boolean; - handleSubmit: any; + handleSubmit: (formUpdates: DeepPartial) => false | undefined; } export interface IFormField { From 57a28ed18df1be21aabdf82a9e52daef60f9b4d8 Mon Sep 17 00:00:00 2001 From: Rachael Shaw Date: Wed, 10 Apr 2024 15:28:17 -0500 Subject: [PATCH 49/83] #14916 Configuration change: Host status webhook per-team (#15612) Adds `webhook_settings.host_status_webhook` options to team config for #14916. Also updated conceptual docs that reference this config (and cut down some content to make room). --- docs/Configuration/configuration-files/README.md | 6 ++++++ docs/REST API/rest-api.md | 5 +++++ docs/Using Fleet/Automations.md | 10 ++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/Configuration/configuration-files/README.md b/docs/Configuration/configuration-files/README.md index a5f8ced68a..fa02bcb76c 100644 --- a/docs/Configuration/configuration-files/README.md +++ b/docs/Configuration/configuration-files/README.md @@ -234,6 +234,12 @@ spec: secrets: - secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff - secret: JZ/C/Z7ucq22dt/zjx2kEuDBN0iLjqfz + webhook_settings: + host_status_webhook: + days_count: 0 + destination_url: "" + enable_host_status_webhook: false + host_percentage: 0 host_expiry_settings: host_expiry_enabled: true host_expiry_window: 14 diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 4d7de79712..4e1f057990 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -8578,6 +8578,11 @@ _Available in Fleet Premium_ |     enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. | |     destination_url | string | body | The URL to deliver the webhook requests to. | |     policy_ids | array | body | List of policy IDs to enable failing policies webhook. | +|   host_status_webhook | object | body | Host status webhook settings. | +|     enable_host_status_webhook | boolean | body | Whether or not the host status webhook is enabled. | +|     destination_url | string | body | The URL to deliver the webhook request to. | +|     host_percentage | integer | body | The minimum percentage of hosts that must fail to check in to Fleet in order to trigger the webhook request. | +|     days_count | integer | body | The minimum number of days that the configured `host_percentage` must fail to check in to Fleet in order to trigger the webhook request. | |     host_batch_size | integer | body | Maximum number of hosts to batch on failing policy webhook requests. The default, 0, means no batching (all hosts failing a policy are sent on one request). | | integrations | object | body | Integrations settings for the team. Note that integrations referenced here must already exist globally, created by a call to [Modify configuration](#modify-configuration). | |   jira | array | body | Jira integrations configuration. | diff --git a/docs/Using Fleet/Automations.md b/docs/Using Fleet/Automations.md index 2a3808b678..c34d884d14 100644 --- a/docs/Using Fleet/Automations.md +++ b/docs/Using Fleet/Automations.md @@ -130,9 +130,9 @@ Follow the steps below to configure Jira or Zendesk as a ticket destination: ## Host status automations -Host status automations send a webhook request if a configured percentage of hosts have not checked in to Fleet for a configured number of days. +Host status automations send a webhook request if a configured percentage of hosts have not checked in to Fleet for a configured number of days. This can be customized [globally](https://fleetdm.com/docs/configuration/configuration-files#organization-settingss) or [per-team](https://fleetdm.com/docs/configuration/configuration-files#teams). -Fleet sends these webhook requests once per day by default. This interval can be updated with the `webhook_settings.interval` configuration option using the [`config` YAML document](https://fleetdm.com/docs/using-fleet/configuration-files#organization-settings) and the `fleetctl apply` command. Note that this interval currently configures both host status and failing policy automations. +Fleet sends these webhook requests once per day by default. This interval can be updated with the `webhook_settings.interval` [configuration option](https://fleetdm.com/docs/configuration/configuration-files#organization-settings). Note that this interval currently configures both host status and failing policy automations. Example webhook payload: @@ -147,9 +147,11 @@ POST https://server.com/example because the Host status webhook is enabeld in your Fleet instance.", "data": { - "unseen_hosts": 1, - "total_hosts": 2, + "unseen_hosts": 3, + "total_hosts": 12, "days_unseen": 3, + "team_id": 123, + "host_ids": [1, 2, 3] } } ``` From 966cf3fda57a048e4336d0af2809c21909d293f5 Mon Sep 17 00:00:00 2001 From: Rachael Shaw Date: Wed, 10 Apr 2024 15:47:44 -0500 Subject: [PATCH 50/83] Add redirect for broken link to downgrading docs (#18164) For https://github.com/fleetdm/fleet/issues/17860 (the current link that banner points to is broken, this will prevent that happening if things move around again). --- website/config/routes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/website/config/routes.js b/website/config/routes.js index 4646289e21..f9d6a47500 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -485,6 +485,7 @@ module.exports.routes = { 'GET /learn-more-about/google-workspace-domains': 'https://admin.google.com/ac/domains/manage', 'GET /learn-more-about/domain-wide-delegation': 'https://admin.google.com/ac/owl/domainwidedelegation', 'GET /learn-more-about/enabling-calendar-api': 'https://console.cloud.google.com/apis/library/calendar-json.googleapis.com', + 'GET /learn-more-about/downgrading': '/docs/using-fleet/downgrading-fleet', // Sitemap // ============================================================================================================= From 01f9963856a49800b00091c0636018098e408e97 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 10 Apr 2024 18:04:26 -0300 Subject: [PATCH 51/83] Add summary to test-go.yml Slack message when it fails (#18188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is to clearly see what is failing. (Looking through the thousands of log lines via the URL is tedious.) ![Screenshot 2024-04-10 at 1 25 31 PM](https://github.com/fleetdm/fleet/assets/2073526/b64edc4b-6c88-4385-80e2-7babb1d4f3e5) --- .../config/slack_payload_template.json | 19 +++++++++ .github/workflows/test-go.yaml | 39 ++++++++++++------- 2 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/config/slack_payload_template.json diff --git a/.github/workflows/config/slack_payload_template.json b/.github/workflows/config/slack_payload_template.json new file mode 100644 index 0000000000..c4a475c414 --- /dev/null +++ b/.github/workflows/config/slack_payload_template.json @@ -0,0 +1,19 @@ +{ + "text": "${{ env.JOB_STATUS }}\n${{ env.EVENT_URL }}", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Go tests result: ${{ env.JOB_STATUS }}\n${{ env.RUN_URL }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Summary:\n```${GO_FAIL_SUMMARY}```" + } + } + ] +} \ No newline at end of file diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index a5c02b14cd..eb82af7e21 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -99,7 +99,6 @@ jobs: done echo "mysql is ready" - - name: Run Go Tests run: | GO_TEST_EXTRA_FLAGS="-v -race=$RACE_ENABLED -timeout=$GO_TEST_TIMEOUT" \ @@ -119,24 +118,29 @@ jobs: files: coverage.txt flags: backend + - name: Generate summary of errors + if: github.event.schedule == '0 4 * * *' && failure() + run: | + c1grep() { grep "$@" || test $? = 1; } + c1grep -oP 'FAIL: .*$' /tmp/gotest.log > /tmp/summary.txt + c1grep 'test timed out after' /tmp/gotest.log >> /tmp/summary.txt + c1grep 'fatal error:' /tmp/gotest.log >> /tmp/summary.txt + GO_FAIL_SUMMARY=$(head -n 5 /tmp/summary.txt | sed ':a;N;$!ba;s/\n/\\n/g') + echo "GO_FAIL_SUMMARY=$GO_FAIL_SUMMARY" + if [[ -z "$GO_FAIL_SUMMARY" ]]; then + GO_FAIL_SUMMARY="unknown, please check the build URL" + fi + GO_FAIL_SUMMARY=$GO_FAIL_SUMMARY envsubst < .github/workflows/config/slack_payload_template.json > ./payload.json + - name: Slack Notification if: github.event.schedule == '0 4 * * *' && failure() uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0 with: - payload: | - { - "text": "${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }}", - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "Go tests result: ${{ job.status }}\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }}" - } - } - ] - } + payload-file-path: ./payload.json env: + JOB_STATUS: ${{ job.status }} + EVENT_URL: ${{ github.event.pull_request.html_url || github.event.head.html_url }} + RUN_URL: https://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }} SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK @@ -147,3 +151,10 @@ jobs: name: test-log path: /tmp/gotest.log if-no-files-found: error + + - name: Upload summary test log + if: always() + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2 + with: + name: summary-test-log + path: /tmp/summary.txt \ No newline at end of file From 565e45c0417ca57d9072ec48c3c6ae99c6cbb85c Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:26:16 -0400 Subject: [PATCH 52/83] Fleet UI: Pass teamId through all query flows (#18204) --- .../QueryDetailsPage/QueryDetailsPage.tsx | 5 ++++- .../queries/live/LiveQueryPage/LiveQueryPage.tsx | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx index d8fdb10d7a..9811794433 100644 --- a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx +++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx @@ -270,7 +270,10 @@ const QueryDetailsPage = ({ className={`${baseClass}__run`} variant="blue-green" onClick={() => { - queryId && router.push(PATHS.LIVE_QUERY(queryId)); + queryId && + router.push( + PATHS.LIVE_QUERY(queryId, currentTeamId) + ); }} disabled={disabledLiveQuery} > diff --git a/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx index 43f368876f..b5336c1436 100644 --- a/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx +++ b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx @@ -3,6 +3,7 @@ import { useQuery } from "react-query"; import { useErrorHandler } from "react-error-boundary"; import { InjectedRouter, Params } from "react-router/lib/Router"; import PATHS from "router/paths"; +import useTeamIdParam from "hooks/useTeamIdParam"; import { AppContext } from "context/app"; import { QueryContext } from "context/query"; @@ -41,6 +42,13 @@ const RunQueryPage = ({ }: IRunQueryPageProps): JSX.Element => { const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null; + const { currentTeamId } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); + const handlePageError = useErrorHandler(); const { config } = useContext(AppContext); const { @@ -78,8 +86,8 @@ const RunQueryPage = ({ // Reroute users out of live flow when live queries are globally disabled if (disabledLiveQuery) { queryId - ? router.push(PATHS.QUERY_DETAILS(queryId)) - : router.push(PATHS.NEW_QUERY()); + ? router.push(PATHS.QUERY_DETAILS(queryId, currentTeamId)) + : router.push(PATHS.NEW_QUERY(currentTeamId)); } // disabled on page load so we can control the number of renders @@ -150,8 +158,8 @@ const RunQueryPage = ({ const goToQueryEditor = useCallback( () => queryId - ? router.push(PATHS.EDIT_QUERY(queryId)) - : router.push(PATHS.NEW_QUERY()), + ? router.push(PATHS.EDIT_QUERY(queryId, currentTeamId)) + : router.push(PATHS.NEW_QUERY(currentTeamId)), [] ); From cfdce429708c08d8b23c5a14dee921ae6f346888 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 10 Apr 2024 18:34:33 -0300 Subject: [PATCH 53/83] Fixes for releasing to stable channel (#18200) Bug fixes found while releasing fleetd `1.23.0` to `stable`. --- tools/tuf/README.md | 4 ---- tools/tuf/releaser.sh | 9 +++++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tools/tuf/README.md b/tools/tuf/README.md index 126b2610bc..857859809d 100644 --- a/tools/tuf/README.md +++ b/tools/tuf/README.md @@ -175,8 +175,6 @@ KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \ TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \ SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \ TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \ -GITHUB_USERNAME=foobar \ -GITHUB_TOKEN_1PASSWORD_PATH="Private/Github Token/password" \ PUSH_TO_REMOTE=1 \ ./tools/tuf/releaser.sh ``` @@ -193,8 +191,6 @@ KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \ TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \ SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \ TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \ -GITHUB_USERNAME=foobar \ -GITHUB_TOKEN_1PASSWORD_PATH="Private/Github Token/password" \ PUSH_TO_REMOTE=1 \ ./tools/tuf/releaser.sh ``` diff --git a/tools/tuf/releaser.sh b/tools/tuf/releaser.sh index 527314bd01..9acf254f18 100755 --- a/tools/tuf/releaser.sh +++ b/tools/tuf/releaser.sh @@ -62,7 +62,10 @@ setup () { prompt "AWS SSO login was successful, press any key to continue..." fi - GITHUB_TOKEN=$(op read "op://$GITHUB_TOKEN_1PASSWORD_PATH") + # GITHUB_TOKEN is only necessary when releasing to edge. + if [[ -n $GITHUB_TOKEN_1PASSWORD_PATH ]]; then + GITHUB_TOKEN=$(op read "op://$GITHUB_TOKEN_1PASSWORD_PATH") + fi # These need to be exported for use by `fleetctl updates` commands. FLEET_TARGETS_PASSPHRASE=$(op read "op://$TARGETS_PASSPHRASE_1PASSWORD_PATH") @@ -87,10 +90,11 @@ promote_component_edge_to_stable () { component_name=$1 component_version=$2 - version_parts=("${component_version//./ }") + IFS='.' read -r -a version_parts <<< "$component_version" major=${version_parts[0]} minor=${version_parts[1]} + pushd "$TUF_DIRECTORY" case $component_name in orbit) fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/macos/edge/orbit" --platform macos --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable @@ -112,6 +116,7 @@ promote_component_edge_to_stable () { exit 1 ;; esac + popd } promote_edge_to_stable () { From 8159c2b44fcbe54dfb07baf023fa50004e42260b Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 10 Apr 2024 19:50:13 -0500 Subject: [PATCH 54/83] Website: Update rituals script (#18211) Closes: #17991 Changes: - Updated the create-issues-for-todays-rituals script to calculate the number of miliseconds until ritual issues should be created in whole milliseconds. --- website/scripts/create-issues-for-todays-rituals.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/scripts/create-issues-for-todays-rituals.js b/website/scripts/create-issues-for-todays-rituals.js index 9faf00bca1..1a3be90ffd 100644 --- a/website/scripts/create-issues-for-todays-rituals.js +++ b/website/scripts/create-issues-for-todays-rituals.js @@ -69,11 +69,11 @@ module.exports = { isItTimeToCreateANewIssue = true; } else { // Otherwise, get the number of milliseconds until the next issue for this ritual will be created. - let timeToNextRitualInMs = amountOfCycleRemainingTillNextRitual * ritualsFrequencyInMs; + let timeToNextRitualInMs = Math.floor(amountOfCycleRemainingTillNextRitual * ritualsFrequencyInMs); // Since this script runs once a day at the same time, we'll create issues we'll create issues for if(_.startsWith(ritual.frequency, 'Daily')) {// Using _.startsWith() to handle frequencies with emoji ("Daily ⏰") and with out ("Daily") isItTimeToCreateANewIssue = true; - } else if(timeToNextRitualInMs < 86400000) { + } else if(timeToNextRitualInMs <= 86400000) { // If the next occurance of this ritual is in less than 24 hours (before this script runs again), we'll create an issue for it. isItTimeToCreateANewIssue = true; } From 02563ffef944062f956626d313893f61fc0e2528 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Wed, 10 Apr 2024 18:38:41 -0700 Subject: [PATCH 55/83] Convert all values to string where possible, even if not derived from a table (#18210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #17946 results from querying chrome extension on macOS Chrome browser: ![Screenshot 2024-04-10 at 4 39 13 PM](https://github.com/fleetdm/fleet/assets/61553566/d67901f3-6e20-4190-8dbb-26e93361555b) - [x] Changes file added for user-visible changes in `changes/` - [x] Updated tests - [x] Manual QA for all new/changed functionality - [ ] TODO - Manual QA on actual Chromebook --------- Co-authored-by: Jacob Shandling --- changes/17946-fleetd-chrome-numbers | 2 ++ ee/fleetd-chrome/src/db.test.ts | 2 +- ee/fleetd-chrome/src/db.ts | 20 +++++++++++++++++--- 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 changes/17946-fleetd-chrome-numbers diff --git a/changes/17946-fleetd-chrome-numbers b/changes/17946-fleetd-chrome-numbers new file mode 100644 index 0000000000..c26bffdd51 --- /dev/null +++ b/changes/17946-fleetd-chrome-numbers @@ -0,0 +1,2 @@ +- Fix a bug where values not derived from "actual" fleetd-chrome tables were not being displayed + correctly (e.g., `SELECT 1` gets its value from the query itself, not a table) diff --git a/ee/fleetd-chrome/src/db.test.ts b/ee/fleetd-chrome/src/db.test.ts index 41493348fb..6bf7526005 100644 --- a/ee/fleetd-chrome/src/db.test.ts +++ b/ee/fleetd-chrome/src/db.test.ts @@ -3,5 +3,5 @@ import VirtualDatabase from "./db"; test("Simple query", async () => { const db = await VirtualDatabase.init(); const res = await db.query("select 1"); - expect(res).toEqual({"data": [{ "1": 1 }], "warnings": null}); + expect(res).toEqual({ data: [{ "1": "1" }], warnings: null }); }); diff --git a/ee/fleetd-chrome/src/db.ts b/ee/fleetd-chrome/src/db.ts index b50f186df9..c309b2cdab 100644 --- a/ee/fleetd-chrome/src/db.ts +++ b/ee/fleetd-chrome/src/db.ts @@ -1,7 +1,6 @@ import SQLiteAsyncESMFactory from "wa-sqlite/dist/wa-sqlite-async.mjs"; import * as SQLite from "wa-sqlite"; -// Alphabetical order import Table from "./tables/Table"; import TableChromeExtensions from "./tables/chrome_extensions"; import TableDiskInfo from "./tables/disk_info"; @@ -34,7 +33,6 @@ export default class VirtualDatabase { this.sqlite3 = sqlite3; this.db = db; - // Alphabetical order VirtualDatabase.register( sqlite3, db, @@ -81,7 +79,23 @@ export default class VirtualDatabase { await this.sqlite3.exec(this.db, sql, (row, columns) => { // map each row to object rows.push( - Object.fromEntries(columns.map((_, i) => [columns[i], row[i]])) + Object.fromEntries( + columns.map((_, i) => { + let [colName, val] = [columns[i], row[i]]; + if (typeof val !== "string") { + if (val.toString) { + val = val.toString(); + } else { + this.warnings.push({ + column: colName, + error_message: `Value is not a string and doesn't have a toString method: ${val}`, + }); + val = null; + } + } + return [colName, val]; + }) + ) ); }); return { data: rows, warnings: this.warnings }; From 265b6122e19ccfb304c44bcc88214bb4f864b3a3 Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Wed, 10 Apr 2024 22:19:21 -0500 Subject: [PATCH 56/83] Handbook: Update leadership.md (#18158) Co-authored-by: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> --- handbook/company/leadership.md | 45 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/handbook/company/leadership.md b/handbook/company/leadership.md index b76d42083b..a0397c1d9b 100644 --- a/handbook/company/leadership.md +++ b/handbook/company/leadership.md @@ -166,23 +166,25 @@ If the consultant is international, you will also provide: > To update a consultant's fee, [submit an issue to BizOps](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-business-operations&projects=&title=Update%20consultant%20fee) with the consultant's name and new hourly rate. -### Advisor +### Adding an advisor -#### Adding an advisor -Advisor agreements are sent through [DocuSign](https://www.docusign.com/), using the "Advisor Agreement" -template. -- Send the advisor agreement. To send a new advisor agreement, you'll need the new advisor's name and the number of shares they are offered. -- Once you send the agreement, locate an existing empty row and available ID in ["Advisors"](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) and enter the new advisor's information. - >**_Note:_** *Be sure to mark any columns that haven't been completed yet as "TODO"* +First: -#### Finalizing a new advisor -- Update the ["Advisors"](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) to show that the agreement has been signed, and ask the new advisor to add us on [LinkedIn](https://www.linkedin.com/company/71111416), [Crunchbase](https://www.crunchbase.com/organization/fleet-device-management), and [Angellist](https://angel.co/company/fleetdm). -- Update "Equity plan" to reflect updated status and equity grant for this advisor, and to ensure the advisor's equity is queued up for the next quarterly equity grant ritual. +Advisor agreements are sent through [DocuSign](https://www.docusign.com/), using the "Advisor Agreement" template. +- Update the ["Advisors" sheet](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) + >**_Note:_** *Be sure to mark any columns that haven't been completed yet as "TODO"* +- Update the "Equity plan" sheet (which should have been automatically updated after updating "Advisors" thanks to the global unique IDs next to each row which are used to connect the spreadsheets) to reflect the default number of shares for advisor equity grants. +- Send the advisor agreement [through Docusign](https://apps.docusign.com/send/templates?view=shared&folder=0482b0fd-a752-41be-93a0-185e2fb7ef54) using the CEO's account, pulling the advisor's email address from a recent calendar event on the CEO's calendar. +- Complete the first step of signing, which involves filling in the number of shares. +- Then wait for the advisor to sign. (Fleet's CEO will sign after that.) -### Core team member -This section is about creating a core team member role, and the hiring process for a new core team member, or Fleetie. +Then, to finalize a new advisor after signing is complete: +- Schedule quarterly recurring 1h meeting between the CEO and the advisor, with 30m of recurring prep scheduled back to back ahead of the meeting. +- Update the status columns in the ["Advisors" sheet](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) to show that the agreement has been signed, and ask the new advisor to add us on [LinkedIn](https://www.linkedin.com/company/71111416), [Crunchbase](https://www.crunchbase.com/organization/fleet-device-management), and [Angellist](https://angel.co/company/fleetdm). +- Update "Equity plan" status columns to reflect updated status for this advisor, and to ensure the advisor's equity is queued up for the next quarterly equity grant ritual. -#### Creating a new position + +### Creating a new position Want to hire? Use these steps to hire a [fleetie, not a consultant](https://fleetdm.com/handbook/company/leadership#who-isnt-a-consultant). Here's how to open up a new position on the core team: @@ -236,7 +238,7 @@ A completed open position entry should look something like this: - _**Why bother with approvals?** We avoid cancelling or significantly changing a role after opening it. It hurts candidates too much. Instead, get the position approved first, before you start recruiting and interviewing. This gives you a sounding board and avoids misunderstandings._ -#### Approving a new position +### Approving a new position When review is requested on a proposal to open a new position, the 🐈‍⬛ CEO will complete the following steps when reviewing the pull request: 1. **Consider role and reporting structure:** Confirm the new row in "Fleeties" has a manager, job title, and department, that it doesn't have any corrupted spreadsheet formulas or formatting, and that the start date is set to the first Monday of the next month. @@ -257,7 +259,7 @@ When review is requested on a proposal to open a new position, the 🐈‍⬛ CE > _**Note:** Most columns of the "Equity plan" are updated automatically when "Fleeties" is, based on the unique identifier of each row, like `🧑‍🚀890`. (Advisors have their own flavor of unique IDs, such as `🦉755`, which are defined in ["Advisors and investors"](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit).)_ -#### Recruiting +### Recruiting Fleet accepts job applications, but the company does not list positions on general purpose job boards. This prevents us being overwhelmed with candidates so we can fulfill our goal of responding promptly to every applicant. This means that outbound recruiting, 3rd party recruiters, and references from team members are important aspect of the company's hiring strategy. Fleet's CEO is happy to assist with outreach, intros, and recruiting strategy for candidates. @@ -272,16 +274,17 @@ When a candidate clicks applies for a job at Fleet, they are taken to a generic #### Candidate correspondence email templates Fleet uses [certain email templates](https://docs.google.com/document/d/1E_gTunZBMNF4AhsOFuDVi9EnvsIGbAYrmmEzdGmnc9U) when responding to candidates. This helps us live our value of [🔴 empathy](https://fleetdm.com/handbook/company#empathy) and helps the company meet the aspiration of replying to all applications within one business day. -#### Hiring restrictions +### Hiring restrictions -##### Incompatible former employers +#### Incompatible former employers Fleet maintains a list of companies with whom Fleet has do-not-solicit terms that prevents us from making offers to employees of these companies. The list is in the Do Not Solicit tab of the [BizOps spreadsheet](https://docs.google.com/spreadsheets/d/1lp3OugxfPfMjAgQWRi_rbyL_3opILq-duHmlng_pwyo/edit#gid=0). -##### Incompatible locations +#### Incompatible locations Fleet is unable to hire team members in some countries. See [this internal document](https://docs.google.com/document/d/1jHHJqShIyvlVwzx1C-FB9GC74Di_Rfdgmhpai1SPC0g/edit) for the list. -#### Interviewing - + +### Interviewing +> TODO: Rewrite this section for the hiring manager as our audience. We're glad you're interested in joining the team! Here are some of the things you can anticipate throughout this process: @@ -314,7 +317,7 @@ Here are the steps hiring managers follow to get an offer out to a candidate: - Share only _one, single Google Doc, please_; with a short, formulaic name that's easy to understand in an instant from just an email subject line (e.g. "_Why hire Jane Doe ("Train Conductor") - 2023-03-21_"). - When the Head of Business Operations receives this doc shared doc in their email with the compiled feedback about the candidate, they will understand that to mean that it is time for Fleet to make an offer to the candidate. -#### Making an offer +### Making an offer After receiving the interview packet, the Head of Business Operations uses the following steps to make an offer: From b101e290a107df65517ba4c67d68d700bcd442f3 Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Wed, 10 Apr 2024 23:30:27 -0400 Subject: [PATCH 57/83] Update customer-success.rituals.yml (#18205) Changed DRI to @nonpunctual for tasks: "Prepare customer requests for feature fest" "Present customer requests at feature fest" --- handbook/customer-success/customer-success.rituals.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handbook/customer-success/customer-success.rituals.yml b/handbook/customer-success/customer-success.rituals.yml index 27046f38f2..017063ae96 100644 --- a/handbook/customer-success/customer-success.rituals.yml +++ b/handbook/customer-success/customer-success.rituals.yml @@ -49,14 +49,14 @@ frequency: "Triweekly" description: "Check-in before the 🗣️ Product Feature Requests meeting to make sure that all information necessary has been gathered before presenting customer requests and feedback to the Product team." moreInfoUrl: "" # TODO: add responsibility on customer-success readme starting point == "Prepare and review the health and latest updates from Fleet's key customers and active proof of concepts (POCs), plus other active support items related to community support, community engagement efforts, contact form or chat requests, self-service customers, outages, and more." - dri: "patagonia121" + dri: "nonpunctual" - task: "Present customer requests at feature fest" startedOn: "2024-02-15" frequency: "Triweekly" description: "Present and advocate for requests and ideas brought to Fleet's attention by customers that are interesting from a product perspective." moreInfoUrl: "" # TODO: add responsibility on customer-success readme starting point == "Prepare and review the health and latest updates from Fleet's key customers and active proof of concepts (POCs), plus other active support items related to community support, community engagement efforts, contact form or chat requests, self-service customers, outages, and more." - dri: "patagonia121" + dri: "nonpunctual" - task: "Communicate release notes to stakeholders" startedOn: "2024-02-21" From b98b050078e35a0a1bb769c9e6147425c0cc7233 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 11 Apr 2024 00:34:13 -0500 Subject: [PATCH 58/83] Website: Fix conditional logic in create-issues-from-todays-rituals script. (#18214) Closes: #17991 Changes: - Updated the logic in the create-issues-from-todays-rituals script that determines if an issue for a ritual should be created. --- .../create-issues-for-todays-rituals.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/website/scripts/create-issues-for-todays-rituals.js b/website/scripts/create-issues-for-todays-rituals.js index 1a3be90ffd..5e1303c736 100644 --- a/website/scripts/create-issues-for-todays-rituals.js +++ b/website/scripts/create-issues-for-todays-rituals.js @@ -64,19 +64,14 @@ module.exports = { let nextIssueShouldBeCreatedAt = ritualStartedAt + ((Math.floor(howManyRitualsCycles) + 1) * ritualsFrequencyInMs); // Get the amount of this ritual's cycle remaining. let amountOfCycleRemainingTillNextRitual = (Math.floor(howManyRitualsCycles) - howManyRitualsCycles) + 1; - // If amountOfCycleRemainingTillNextRitual is 0, then it is time to create a new issue for this ritual (Note: This will probably never happen) - if(amountOfCycleRemainingTillNextRitual === 0 || amountOfCycleRemainingTillNextRitual === -0){ + // Get the number of milliseconds until the next issue for this ritual will be created. + let timeToNextRitualInMs = amountOfCycleRemainingTillNextRitual * ritualsFrequencyInMs; + if(_.startsWith(ritual.frequency, 'Daily')) {// Using _.startsWith() to handle frequencies with emoji ("Daily ⏰") and with out ("Daily") + // Since this script runs once a day, we'll always create issues for daily rituals. + isItTimeToCreateANewIssue = true; + } else if(timeToNextRitualInMs === ritualsFrequencyInMs) { + // For any other frequency, we'll check to see if the calculated timeToNextRitualInMs is the same as the rituals frequency. isItTimeToCreateANewIssue = true; - } else { - // Otherwise, get the number of milliseconds until the next issue for this ritual will be created. - let timeToNextRitualInMs = Math.floor(amountOfCycleRemainingTillNextRitual * ritualsFrequencyInMs); - // Since this script runs once a day at the same time, we'll create issues we'll create issues for - if(_.startsWith(ritual.frequency, 'Daily')) {// Using _.startsWith() to handle frequencies with emoji ("Daily ⏰") and with out ("Daily") - isItTimeToCreateANewIssue = true; - } else if(timeToNextRitualInMs <= 86400000) { - // If the next occurance of this ritual is in less than 24 hours (before this script runs again), we'll create an issue for it. - isItTimeToCreateANewIssue = true; - } } // Skip to the next ritual if it isn't time yet. if (!isItTimeToCreateANewIssue) { From 51464202949e9d62e75d6b495097641e8b2b8c79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:13:33 -0300 Subject: [PATCH 59/83] Update versions of fleetd components in Fleet's TUF [automated] (#18213) Automated change from [GitHub action](https://github.com/fleetdm/fleet/actions/workflows/fleetd-tuf.yml). Co-authored-by: lucasmrod --- orbit/TUF.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/orbit/TUF.md b/orbit/TUF.md index 4e4c48fec4..11c44d4184 100644 --- a/orbit/TUF.md +++ b/orbit/TUF.md @@ -7,8 +7,8 @@ Following are the currently deployed versions of fleetd components on the `stabl | Component\OS | macOS | Linux | Windows | |--------------|--------------|--------|---------| -| orbit | 1.22.0 | 1.22.0 | 1.22.0 | -| desktop | 1.22.0 | 1.22.0 | 1.22.0 | +| orbit | 1.23.0 | 1.23.0 | 1.23.0 | +| desktop | 1.23.0 | 1.23.0 | 1.23.0 | | osqueryd | 5.11.0 | 5.11.0 | 5.11.0 | | nudge | 1.1.10.81462 | - | - | | swiftDialog | 2.1.0 | - | - | From c917ac30267c2c2f7db0f870b020e0a58bb90e77 Mon Sep 17 00:00:00 2001 From: JD Date: Thu, 11 Apr 2024 11:48:33 -0700 Subject: [PATCH 60/83] Handbook: Adds swag ordering (#18167) Moving these details out of the event issue template and into the handbook. --------- Co-authored-by: Joanne Stableford <59930035+JoStableford@users.noreply.github.com> Co-authored-by: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> --- handbook/business-operations/README.md | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/handbook/business-operations/README.md b/handbook/business-operations/README.md index 112388e5ab..eeebe9fbc7 100644 --- a/handbook/business-operations/README.md +++ b/handbook/business-operations/README.md @@ -377,6 +377,36 @@ Article creation begins with creation of an issue using the "Article request" te Check the "📃 Planned articles" column in [#g-demand board](https://app.zenhub.com/workspaces/g-demand-64e6c8e2d35c7f001a457b7f/board) and continue to work through steps in each event's issue. --> +### Order SWAG + +**To order T-shirts:** + + - Check [Postal](https://app.postal.io/items/postals) first and see if the warehouse has enough shirts. + - Navigate to the [approved items page](https://app.postal.io/items/postals). + - Hover over the shirt design and click on the airplane. + - Click bulk send and choose one shirt size and the expected quantity of that particular shirt size. + - Make sure the address matches the expected receiving address. + - If the Postal warehouse can't fulfill the order or To order swag quickly: + - Login to [https://www.rushordertees.com/my-account/login/) (saved in 1Password). + - Choose Fleet logo design t-shirt under [my designs](https://www.rushordertees.com/my-account/designs/). + - Order shirts based on the pre-determined number (~5% of total event attendees). + - Submit the order. Ensure the address matches the expected receiving address. + +**To order stickers:** + + - Login to [StickerMule](https://www.stickermule.com/) (saved in 1Password). + - Find the [brand kit](https://www.stickermule.com/studio/brand-kits) after logging in. + - Click on the "Fleet Device Management" brand kit and order preapproved stickers from the templates. + - Total sticker quantity should be ~10% of total event attendees. + - Complete the checkout process. Ensure the address matches the expected receiving address. + +**To order pens and sticky note pads** + + - Pens and sticky note pads are ordered through Everything Branded. + - Email our sales representative Jake William (saved in 1Password) to order any of the following: + - [Javalina™ Metallic Stylus Pen](https://www.everythingbranded.com/product/javalina-metallic-stylus-pen-us-pat-8847930-9092077-350220) + - [Sharpie Fine Point Markers](https://www.everythingbranded.com/product/sharpie-fine-point-332908) + - [Custom sticky note pads](https://www.everythingbranded.com/product/custom-sticky-notes-585601) (design is in the StickerMule [brand kit](https://www.stickermule.com/studio/brand-kits)) ## Rituals From 598dfa3061c702f60a586c0f9c7f00a0d3fc7e4d Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Thu, 11 Apr 2024 16:23:00 -0300 Subject: [PATCH 61/83] Move and fix `oncall.sh` script (#18217) In the future we can create automation to send reminders to a Slack channel. Or send the list every time there's a oncall changeover. --- tools/oncall/README.md | 8 ++++++++ scripts/on-call => tools/oncall/oncall.sh | 15 ++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 tools/oncall/README.md rename scripts/on-call => tools/oncall/oncall.sh (78%) diff --git a/tools/oncall/README.md b/tools/oncall/README.md new file mode 100644 index 0000000000..5ada653c50 --- /dev/null +++ b/tools/oncall/README.md @@ -0,0 +1,8 @@ +# Oncall + +You can use the `oncall.sh` script to find out if there are any open issues or PRs from the community: +```sh +gh auth login +./tools/oncall/oncall.sh issues +./tools/oncall/oncall.sh prs +``` diff --git a/scripts/on-call b/tools/oncall/oncall.sh similarity index 78% rename from scripts/on-call rename to tools/oncall/oncall.sh index 13cfdeed0b..7b8c3f1e0d 100755 --- a/scripts/on-call +++ b/tools/oncall/oncall.sh @@ -9,7 +9,7 @@ usage() { Contains useful commands for on-call. Usage: - $(basename $0) + $(basename "$0") Commands: issues List open issues from outside contributors. @@ -18,7 +18,7 @@ EOF } require() { - type $1 >/dev/null 2>&1 || { + type "$1" >/dev/null 2>&1 || { echo "$1 is required but not installed. Aborting." >&2 exit 1 } @@ -29,12 +29,12 @@ issues() { require jq auth_status="$(gh auth status -t 2>&1)" - username="$(echo "${auth_status}" | sed -n -r 's/^.* Logged in to [^[:space:]]+ as ([^[:space:]]+).*/\1/p')" + username="$(echo "${auth_status}" | sed -n -r 's/^.* Logged in to github.com account ([^[:space:]]+).*/\1/p')" token="$(echo "${auth_status}" | sed -n -r 's/^.*Token: ([a-zA-Z0-9_]*)/\1/p')" - members="$(curl -s -u "${username}:${token}" https://api.github.com/orgs/fleetdm/members | jq -r 'map(.login)')" + members="$(curl -s -u "${username}:${token}" https://api.github.com/orgs/fleetdm/members?per_page=100 | jq -r 'map(.login)')" - gh pr list --repo fleetdm/fleet --label "bug" --label ":reproduce" --json id,title,author,url,createdAt | + gh issue list --repo fleetdm/fleet --json id,title,author,url,createdAt,labels --limit 100 | jq -r --argjson members "$members" \ 'map(select(.author.login as $in | $members | index($in) | not)) | sort_by(.createdAt) | reverse' } @@ -44,10 +44,11 @@ prs() { require jq auth_status="$(gh auth status -t 2>&1)" - username="$(echo "${auth_status}" | sed -n -r 's/^.* Logged in to [^[:space:]]+ as ([^[:space:]]+).*/\1/p')" + username="$(echo "${auth_status}" | sed -n -r 's/^.* Logged in to github.com account ([^[:space:]]+).*/\1/p')" token="$(echo "${auth_status}" | sed -n -r 's/^.*Token: ([a-zA-Z0-9_]*)/\1/p')" - members="$(curl -s -u "${username}:${token}" https://api.github.com/orgs/fleetdm/members | jq -r 'map(.login)')" + members="$(curl -s -u "${username}:${token}" https://api.github.com/orgs/fleetdm/members?per_page=100 | jq -r 'map(.login)' | jq '. += ["app/dependabot"]')" + # defaults to listing open prs gh pr list --repo fleetdm/fleet --json id,title,author,url,createdAt | jq -r --argjson members "$members" \ From 9eaaaf8af312f44c03584c7b61121271354213da Mon Sep 17 00:00:00 2001 From: Joanne Stableford <59930035+JoStableford@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:36:09 -0400 Subject: [PATCH 62/83] Update macos-device-health.policies.yml to add check macOS version (#18218) Adding new policy to the macos-device-health.policies.yml that checks if the device meets minimum macOS (currently set to 14.4.1) and enables it for calendar events. Reference https://github.com/fleetdm/confidential/issues/6015 --------- Co-authored-by: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> --- .../lib/macos-device-health.policies.yml | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/it-and-security/lib/macos-device-health.policies.yml b/it-and-security/lib/macos-device-health.policies.yml index b706cd80c1..350b1f530f 100644 --- a/it-and-security/lib/macos-device-health.policies.yml +++ b/it-and-security/lib/macos-device-health.policies.yml @@ -44,7 +44,7 @@ username = '' ) AND NOT EXISTS ( - SELECT 1 FROM managed_policies WHERE + SELECT 1 FROM managed_policies WHERE domain='com.apple.screensaver' AND name='idleTime' AND CAST(value AS INT) > 1200 @@ -54,8 +54,24 @@ 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/%')); + 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." + 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 WHERE + EXISTS ( + SELECT major, concat_ws(".", major, minor, patch) AS "macOS Version" FROM os_version --Sonoma WHERE + (major = "14" AND "macOS Version" < "14.4.1") + ); + critical: false + description: This policy check if macOS version is most recent version available. + resolution: From the Apple menu, select System Settings. Navigate to General > Software Update. + platform: darwin + calendar_events_enabled: true From 5eb8cf618e0c321bde3b2883398c422eaaefd513 Mon Sep 17 00:00:00 2001 From: Joanne Stableford <59930035+JoStableford@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:36:40 -0400 Subject: [PATCH 63/83] Add calendar integration settings into workstation canary team (#18219) Adding integration settings and linking to the webhook_url secret set for calendar webhook --- it-and-security/teams/workstations-canary.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/it-and-security/teams/workstations-canary.yml b/it-and-security/teams/workstations-canary.yml index 1ace8e43cf..7ec46d1f36 100644 --- a/it-and-security/teams/workstations-canary.yml +++ b/it-and-security/teams/workstations-canary.yml @@ -8,6 +8,10 @@ team_settings: host_expiry_window: 0 secrets: - secret: $DOGFOOD_WORKSTATIONS_CANARY_ENROLL_SECRET + integrations: + google_calendar: + enable_calendar_events: true + webhook_url: $DOGFOOD_WORKSTATIONS_CANARY_CALENDAR_WEBHOOK_URL agent_options: config: decorators: From 6a70f2ab7854937fd6abc00b58079f069728d665 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:28:42 -0400 Subject: [PATCH 64/83] [unreleased bug] Fleet UI: Fix UI to expect null values instead of undefined values for orbit and fleetd versions (#18208) --- frontend/interfaces/host.ts | 4 ++-- .../pages/hosts/details/cards/HostSummary/HostSummary.tsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts index 2e92da6490..623bf0b66f 100644 --- a/frontend/interfaces/host.ts +++ b/frontend/interfaces/host.ts @@ -269,8 +269,8 @@ export interface IHost { uuid: string; platform: string; osquery_version: string; - orbit_version?: string; - fleet_desktop_version?: string; + orbit_version: string | null; + fleet_desktop_version: string | null; os_version: string; build: string; platform_like: string; // TODO: replace with more specific union type diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index f52c8b06bc..97a513198b 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -321,7 +321,7 @@ const HostSummary = ({ if (platform === "chrome") { return ; } - if (summaryData.orbit_version) { + if (summaryData.orbit_version !== DEFAULT_EMPTY_CELL_VALUE) { return ( Orbit: {summaryData.orbit_version} - {summaryData.fleet_desktop_version && ( + {summaryData.fleet_desktop_version !== + DEFAULT_EMPTY_CELL_VALUE && ( <>
Fleet Desktop: {summaryData.fleet_desktop_version} From 8fd807be553bfe286ec61ebcf6e91e5412742b88 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:30:12 -0400 Subject: [PATCH 65/83] Fleet UI: Adding a policy modal can filter by platform (#18006) --- changes/12292-policies-filter-by-platform | 1 + .../AddPolicyModal/AddPolicyModal.tsx | 79 +++++++++++++++++-- .../components/AddPolicyModal/_styles.scss | 21 +++-- 3 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 changes/12292-policies-filter-by-platform diff --git a/changes/12292-policies-filter-by-platform b/changes/12292-policies-filter-by-platform new file mode 100644 index 0000000000..dbc31fab33 --- /dev/null +++ b/changes/12292-policies-filter-by-platform @@ -0,0 +1 @@ +* Add filters by platform to select a new policy modal \ No newline at end of file diff --git a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/AddPolicyModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/AddPolicyModal.tsx index 1aa53f3346..d369f79370 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/AddPolicyModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/AddPolicyModal.tsx @@ -1,15 +1,19 @@ -import React, { useCallback, useContext } from "react"; +import React, { useCallback, useContext, useState } from "react"; import PATHS from "router/paths"; import { InjectedRouter } from "react-router/lib/Router"; import { DEFAULT_POLICY, DEFAULT_POLICIES } from "pages/policies/constants"; import { IPolicyNew } from "interfaces/policy"; +import { SelectedPlatform } from "interfaces/platform"; import { PolicyContext } from "context/policy"; import Button from "components/buttons/Button"; import Modal from "components/Modal"; +// @ts-ignore +import Dropdown from "components/forms/fields/Dropdown"; +import CustomLink from "components/CustomLink"; export interface IAddPolicyModalProps { onCancel: () => void; @@ -18,6 +22,32 @@ export interface IAddPolicyModalProps { teamName?: string; } +const CONTRIBUTE_TO_POLICIES_DOCS_URL = + "https://www.fleetdm.com/contribute-to/policies"; + +const PLATFORM_FILTER_OPTIONS = [ + { + label: "All platforms", + value: "all", + }, + { + label: "macOS", + value: "darwin", + }, + { + label: "Windows", + value: "windows", + }, + { + label: "Linux", + value: "linux", + }, + { + label: "ChromeOS", + value: "chrome", + }, +]; + const baseClass = "add-policy-modal"; const AddPolicyModal = ({ @@ -38,6 +68,9 @@ const AddPolicyModal = ({ setDefaultPolicy, } = useContext(PolicyContext); + const [filteredPolicies, setFilteredPolicies] = useState(DEFAULT_POLICIES); + const [platform, setPlatform] = useState("all"); + const onAddPolicy = (selectedPolicy: IPolicyNew) => { setDefaultPolicy(true); teamName @@ -70,7 +103,22 @@ const AddPolicyModal = ({ teamId, ]); - const policiesAvailable = DEFAULT_POLICIES.map((policy: IPolicyNew) => { + const onPlatformFilterChange = (platformSelected: SelectedPlatform) => { + if (platformSelected === "all") { + setFilteredPolicies(DEFAULT_POLICIES); + } else { + // Note: Default policies currently map to a single platform + const policiesFilteredByPlatform = DEFAULT_POLICIES.filter((policy) => { + return policy.platform === platformSelected; + }); + setFilteredPolicies(policiesFilteredByPlatform); + } + setPlatform(platformSelected); + }; + + const filteredPoliciesCount = filteredPolicies.length; + + const filteredPoliciesList = filteredPolicies.map((policy: IPolicyNew) => { return ( .
+
- {policiesAvailable} + {filteredPoliciesCount > 0 ? filteredPoliciesList : renderNoResults()}
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss index 376aacc859..924a16b0d8 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss @@ -1,17 +1,22 @@ .add-policy-modal { - height: 80%; + height: 90%; overflow: hidden; + min-height: 460px; + max-height: fit-content; + .modal__content { - height: 100%; + height: 90%; overflow: scroll; - margin-top: 0; + display: flex; + flex-direction: column; + gap: $pad-large; } - &__create-policy { - padding-top: 1.5rem; + + .Select-multi-value-wrapper { + display: flex; } - &__policy-selection { - padding: $pad-large 0; - height: 100%; + .Select-menu-outer { + max-height: 220px; } &__policy-name { From c5b2b250911ea05d74a2cc7230cfecffe38d7eff Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:10:19 -0400 Subject: [PATCH 66/83] Fleet UI: Make disabled dropdown tooltip a part of reusable dropdown component and implement (#18054) --- frontend/__mocks__/hostMock.ts | 1 + .../DisabledOptionTooltipWrapper.tsx | 67 +++++++++++++++++++ .../DisabledOptionTooltipWrapper/_styles.scss | 37 ++++++++++ .../DisabledOptionTooltipWrapper/index.tsx | 1 + .../forms/fields/Dropdown/Dropdown.jsx | 17 +++++ .../forms/fields/Dropdown/_styles.scss | 1 + frontend/interfaces/dropdownOption.ts | 1 + frontend/interfaces/host.ts | 1 + .../HostActionsDropdown.tests.tsx | 25 +++++++ .../HostActionsDropdown.tsx | 3 + .../HostActionsDropdown/helpers.tsx | 37 +++++++++- .../HostDetailsPage/HostDetailsPage.tsx | 1 + .../ManagePoliciesPage/ManagePoliciesPage.tsx | 51 +++----------- .../policies/ManagePoliciesPage/_styles.scss | 28 -------- 14 files changed, 201 insertions(+), 70 deletions(-) create mode 100644 frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/DisabledOptionTooltipWrapper.tsx create mode 100644 frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/_styles.scss create mode 100644 frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/index.tsx diff --git a/frontend/__mocks__/hostMock.ts b/frontend/__mocks__/hostMock.ts index 0aaf080bba..b9b36b248e 100644 --- a/frontend/__mocks__/hostMock.ts +++ b/frontend/__mocks__/hostMock.ts @@ -91,6 +91,7 @@ const DEFAULT_HOST_MOCK: IHost = { failing_policies_count: 0, }, status: "offline", + scripts_enabled: false, labels: [], packs: [], software: [], diff --git a/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/DisabledOptionTooltipWrapper.tsx b/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/DisabledOptionTooltipWrapper.tsx new file mode 100644 index 0000000000..c6779a83cc --- /dev/null +++ b/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/DisabledOptionTooltipWrapper.tsx @@ -0,0 +1,67 @@ +import classnames from "classnames"; +import React from "react"; +import { Tooltip as ReactTooltip5 } from "react-tooltip-5"; + +import { uniqueId } from "lodash"; + +interface IDisabledOptionTooltipWrapper { + children: React.ReactNode; + isDelayed?: boolean; + className?: string; + tooltipClass?: string; + clickable?: boolean; + tipContent: React.ReactNode; + /** Location defaults to left */ + place?: "left" | "right" | "top" | "bottom"; + offset?: number; +} + +const baseClass = "disabled-option-tooltip-wrapper"; + +const DisabledOptionTooltipWrapper = ({ + children, + tipContent, + isDelayed, + className, + tooltipClass, + clickable = true, + place = "left", + offset = 24, +}: IDisabledOptionTooltipWrapper) => { + const wrapperClassNames = classnames(baseClass, className); + + const elementClassNames = classnames(`${baseClass}__element`); + + const tipClassNames = classnames( + `${baseClass}__tip-text`, + `${baseClass}__dropdown-tooltip-arrow`, + tooltipClass + ); + + const tipId = uniqueId(); + + return ( + +
+ {children} +
+ + {tipContent} + +
+ ); +}; + +export default DisabledOptionTooltipWrapper; diff --git a/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/_styles.scss b/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/_styles.scss new file mode 100644 index 0000000000..a2f963ed66 --- /dev/null +++ b/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/_styles.scss @@ -0,0 +1,37 @@ +.Select > .Select-menu-outer { + .is-disabled * { + color: $ui-fleet-black-50; + .disabled-option-tooltip-wrapper { + width: 100%; + } + .disabled-option-tooltip-wrapper__element { + // for broader tooltip activation area, equally increase padding and decrease margin + padding: 8px; + margin: -8px; + width: 100%; + } + .react-tooltip { + @include tooltip-text; + font-style: normal; + text-align: center; + } + + // arrow styles directly from react-tooltip-5 css + .tooltip-arrow { + width: 8px; + height: 8px; + } + [class*="react-tooltip__place-top"] > .styles-module_arrow__K0L3T { + transform: rotate(45deg); + } + [class*="react-tooltip__place-right"] > .styles-module_arrow__K0L3T { + transform: rotate(135deg); + } + [class*="react-tooltip__place-bottom"] > .styles-module_arrow__K0L3T { + transform: rotate(225deg); + } + [class*="react-tooltip__place-left"] > .styles-module_arrow__K0L3T { + transform: rotate(315deg); + } + } +} diff --git a/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/index.tsx b/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/index.tsx new file mode 100644 index 0000000000..2a5818905b --- /dev/null +++ b/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/index.tsx @@ -0,0 +1 @@ +export { default } from "./DisabledOptionTooltipWrapper"; diff --git a/frontend/components/forms/fields/Dropdown/Dropdown.jsx b/frontend/components/forms/fields/Dropdown/Dropdown.jsx index 848adceb0f..0a9d657820 100644 --- a/frontend/components/forms/fields/Dropdown/Dropdown.jsx +++ b/frontend/components/forms/fields/Dropdown/Dropdown.jsx @@ -7,6 +7,7 @@ import Select from "react-select"; import dropdownOptionInterface from "interfaces/dropdownOption"; import FormField from "components/forms/FormField"; import Icon from "components/Icon"; +import DisabledOptionTooltipWrapper from "./DisabledOptionTooltipWrapper"; const baseClass = "dropdown"; @@ -109,6 +110,22 @@ class Dropdown extends Component { }; renderOption = (option) => { + if (option.disabledTooltipContent) { + return ( + +
+ {option.label} + {option.helpText && ( + + {option.helpText} + + )} +
+
+ ); + } return (
{option.label} diff --git a/frontend/components/forms/fields/Dropdown/_styles.scss b/frontend/components/forms/fields/Dropdown/_styles.scss index 5523158b09..b7466ae888 100644 --- a/frontend/components/forms/fields/Dropdown/_styles.scss +++ b/frontend/components/forms/fields/Dropdown/_styles.scss @@ -51,6 +51,7 @@ &__option { display: flex; flex-direction: column; + width: 100%; } &__help-text { diff --git a/frontend/interfaces/dropdownOption.ts b/frontend/interfaces/dropdownOption.ts index 75a2124679..c592b4882c 100644 --- a/frontend/interfaces/dropdownOption.ts +++ b/frontend/interfaces/dropdownOption.ts @@ -11,4 +11,5 @@ export interface IDropdownOption { label: string | JSX.Element; value: string | number; premiumOnly?: boolean; + disabledTooltipContent?: string | JSX.Element; } diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts index 623bf0b66f..f0d1ae2b3d 100644 --- a/frontend/interfaces/host.ts +++ b/frontend/interfaces/host.ts @@ -307,6 +307,7 @@ export interface IHost { display_text: string; display_name: string; target_type?: string; + scripts_enabled: boolean | null; users: IHostUser[]; device_users?: IDeviceUser[]; munki?: IMunkiData; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx index 57426f660c..cea478dbd7 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx @@ -28,6 +28,7 @@ describe("Host Actions Dropdown", () => { hostStatus="online" hostMdmEnrollmentStatus={null} hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -54,6 +55,7 @@ describe("Host Actions Dropdown", () => { hostStatus="online" hostMdmEnrollmentStatus={null} hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -81,6 +83,7 @@ describe("Host Actions Dropdown", () => { hostMdmEnrollmentStatus={null} doesStoreEncryptionKey hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -110,6 +113,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -138,6 +142,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -167,6 +172,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -196,6 +202,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -223,6 +230,7 @@ describe("Host Actions Dropdown", () => { mdmName="Non Fleet MDM" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -251,6 +259,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -283,6 +292,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="windows" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -310,6 +320,7 @@ describe("Host Actions Dropdown", () => { hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -335,6 +346,7 @@ describe("Host Actions Dropdown", () => { hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -361,6 +373,7 @@ describe("Host Actions Dropdown", () => { hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -387,6 +400,7 @@ describe("Host Actions Dropdown", () => { hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -418,6 +432,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -447,6 +462,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -476,6 +492,7 @@ describe("Host Actions Dropdown", () => { mdmName="Non Fleet MDM" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -507,6 +524,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="locked" + hostScriptsEnabled /> ); @@ -536,6 +554,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocking" + hostScriptsEnabled /> ); @@ -565,6 +584,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="locked" + hostScriptsEnabled /> ); @@ -594,6 +614,7 @@ describe("Host Actions Dropdown", () => { mdmName="Non Fleet MDM" hostPlatform="darwin" hostMdmDeviceStatus="locked" + hostScriptsEnabled /> ); @@ -624,6 +645,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="locked" + hostScriptsEnabled /> ); @@ -655,6 +677,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -685,6 +708,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="windows" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); @@ -715,6 +739,7 @@ describe("Host Actions Dropdown", () => { mdmName="Fleet" hostPlatform="darwin" hostMdmDeviceStatus="unlocked" + hostScriptsEnabled /> ); diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx index 988986012b..b7fb56c05b 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx @@ -22,6 +22,7 @@ interface IHostActionsDropdownProps { mdmName?: string; hostPlatform?: string; onSelect: (value: string) => void; + hostScriptsEnabled: boolean | null; } const HostActionsDropdown = ({ @@ -32,6 +33,7 @@ const HostActionsDropdown = ({ doesStoreEncryptionKey, mdmName, hostPlatform = "", + hostScriptsEnabled = false, onSelect, }: IHostActionsDropdownProps) => { const { @@ -73,6 +75,7 @@ const HostActionsDropdown = ({ doesStoreEncryptionKey: doesStoreEncryptionKey ?? false, isSandboxMode, hostMdmDeviceStatus, + hostScriptsEnabled, }); // No options to render. Exit early diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index 5310ea7ba9..313e3e8032 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -79,6 +79,7 @@ interface IHostActionConfigOptions { doesStoreEncryptionKey: boolean; isSandboxMode: boolean; hostMdmDeviceStatus: HostMdmDeviceStatusUIState; + hostScriptsEnabled: boolean | null; } const canTransferTeam = (config: IHostActionConfigOptions) => { @@ -284,11 +285,39 @@ const filterOutOptions = ( const setOptionsAsDisabled = ( options: IDropdownOption[], - { isHostOnline, isSandboxMode, hostMdmDeviceStatus }: IHostActionConfigOptions + { + isHostOnline, + isSandboxMode, + hostMdmDeviceStatus, + hostScriptsEnabled, + }: IHostActionConfigOptions ) => { + // Available tooltips for disabled options + const disabledTooltipContent = (value: string | number) => { + const tooltipAction: Record = { + runScript: "run scripts on", + wipe: "wipe", + lock: "lock", + unlock: "unlock", + }; + if (tooltipAction[value]) { + return ( + <> + To {tooltipAction[value]} this host, deploy the +
+ fleetd agent with --enable-scripts + + ); + } + if (!isHostOnline && value === "query") { + return <>You can't query an offline host.; + } + }; + const disableOptions = (optionsToDisable: IDropdownOption[]) => { optionsToDisable.forEach((option) => { option.disabled = true; + option.disabledTooltipContent = disabledTooltipContent(option.value); }); }; @@ -305,6 +334,12 @@ const setOptionsAsDisabled = ( ) ); } + + if (!hostScriptsEnabled) { + optionsToDisable = optionsToDisable.concat( + options.filter((option) => option.value === "runScript") + ); + } if (isSandboxMode) { optionsToDisable = optionsToDisable.concat( options.filter((option) => option.value === "transfer") diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 9d07971117..6a2839035e 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -675,6 +675,7 @@ const HostDetailsPage = ({ hostMdmEnrollmentStatus={host.mdm.enrollment_status} doesStoreEncryptionKey={host.mdm.encryption_key_available} mdmName={mdm?.name} + hostScriptsEnabled={host.scripts_enabled} /> ); }; diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index 494b26cd2d..2f848e5f13 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -775,57 +775,26 @@ const ManagePolicyPage = ({ const getAutomationsDropdownOptions = () => { const isAllTeams = teamIdForApi === undefined || teamIdForApi === -1; - let calEventsLabel: React.ReactNode = "Calendar events"; + let disabledTooltipContent: React.ReactNode; if (!isPremiumTier) { - const tipId = uniqueId(); - calEventsLabel = ( - -
- Calendar events -
- - Available in Fleet Premium - -
- ); + disabledTooltipContent = "Available in Fleet Premium."; } else if (isAllTeams) { - const tipId = uniqueId(); - calEventsLabel = ( - -
- Calendar events -
- - Select a team to manage -
- calendar events. -
-
+ disabledTooltipContent = ( + <> + Select a team to manage +
+ calendar events. + ); } return [ { - label: calEventsLabel, + label: "Calendar events", value: "calendar_events", disabled: !isPremiumTier || isAllTeams, helpText: "Automatically reserve time to resolve failing policies.", + disabledTooltipContent, }, { label: "Other workflows", diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss index 1453d7e5ee..da3af40f77 100644 --- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss @@ -24,34 +24,6 @@ .dropdown__help-text { color: $ui-fleet-black-50; } - .is-disabled * { - color: $ui-fleet-black-25; - .label-text { - font-style: normal; - // increase height to allow for broader tooltip activation area - position: absolute; - height: 34px; - width: 100%; - } - .dropdown__help-text { - // compensate for absolute label-text height - margin-top: 20px; - } - .react-tooltip { - @include tooltip-text; - font-style: normal; - text-align: center; - } - - // arrow styles directly from react-tooltip-5 css - .tooltip-arrow { - width: 8px; - height: 8px; - } - [class*="react-tooltip__place-left"] > .tooltip-arrow { - transform: rotate(315deg); - } - } } .Select-control { margin-top: 0; From 80c906aa69c3cb9888071b259112f32c6a563be6 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:00:46 -0400 Subject: [PATCH 67/83] Windows careful bitlocker selection (#18189) #17796 Fixes an issue in windows server where selecting from `bitlocker_info` will cause the query to abort. Bitlocker is not available by default on some version of windows server, so we first check if the optional component is enabled before making our query --- changes/17796-bitlocker-server | 1 + server/service/osquery_utils/queries.go | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 changes/17796-bitlocker-server diff --git a/changes/17796-bitlocker-server b/changes/17796-bitlocker-server new file mode 100644 index 0000000000..ed28f25c31 --- /dev/null +++ b/changes/17796-bitlocker-server @@ -0,0 +1 @@ +* Fix bug where query retrieving bitlocker info from windows server wouldn't return diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 7c6db09356..f9a6af2bcd 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -630,11 +630,26 @@ var extraDetailQueries = map[string]DetailQuery{ // osquery table on darwin and linux, it is always present. }, "disk_encryption_windows": { - Query: `SELECT 1 FROM bitlocker_info WHERE drive_letter = 'C:' AND protection_status = 1;`, + // Bitlocker is an optional component on Windows Server and + // isn't guaranteed to be installed. If we try to query the + // bitlocker_info table when the bitlocker component isn't + // present, the query will crash and fail to report back to + // the server. Before querying bitlocke_info, we check if it's + // either: + // 1. both an optional component, and installed. + // OR + // 2. not optional, meaning it's built into the OS + Query: ` + WITH encrypted(enabled) AS ( + SELECT CASE WHEN + NOT EXISTS(SELECT 1 FROM windows_optional_features WHERE name = 'BitLocker') + OR + (SELECT 1 FROM windows_optional_features WHERE name = 'BitLocker' AND state = 1) + THEN (SELECT 1 FROM bitlocker_info WHERE drive_letter = 'C:' AND protection_status = 1) + END) + SELECT 1 FROM encrypted WHERE enabled IS NOT NULL`, Platforms: []string{"windows"}, DirectIngestFunc: directIngestDiskEncryption, - // the "bitlocker_info" table doesn't need a Discovery query as it is an official - // osquery table on windows, it is always present. }, } From 82806d07ef167274245959132a8144cc8f50f9db Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:34:42 -0500 Subject: [PATCH 68/83] Update README.md (#18235) --- handbook/digital-experience/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md index b75210bbc7..132d0ffba7 100644 --- a/handbook/digital-experience/README.md +++ b/handbook/digital-experience/README.md @@ -416,7 +416,7 @@ Every two weeks, our CEO Mike has a meeting with Sid Sijbrandij. The CEO uses de Follow these steps to process and backup the E-group agenda: 1. [Archive the E-group agenda](https://fleetdm.com/handbook/digital-experience#archive-a-document) after each meeting, moving it to the ["¶¶ E-group archive"](https://drive.google.com/drive/u/0/folders/1IsSGMgbt4pDcP8gSnLj8Z8NGY7_6UTt6) folder in Google Drive. 2. **In the backup copy**, leave Google Doc comments assigning all TODOs to the correct DRI. -3. If the "All hands" meeting has happened today +3. If the "All hands" meeting has happened today remove any spotlights covered in the current "All hands" presentation. ### Check LinkedIn for unread messages Once a day the Apprentice will confirm check LinkedIn for unread messages. From de7f4b09c66b49b6c5fcdd6f541e1d30ea6d5429 Mon Sep 17 00:00:00 2001 From: Rachael Shaw Date: Fri, 12 Apr 2024 12:25:00 -0500 Subject: [PATCH 69/83] Add redirect for fleetd doc links in the product (#18237) The old one was pointing to a section that doesn't exist anymore. (Part of changes for https://github.com/fleetdm/fleet/pull/18138/) --- website/config/routes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/website/config/routes.js b/website/config/routes.js index f9d6a47500..27dd2b5a3f 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -486,6 +486,7 @@ module.exports.routes = { 'GET /learn-more-about/domain-wide-delegation': 'https://admin.google.com/ac/owl/domainwidedelegation', 'GET /learn-more-about/enabling-calendar-api': 'https://console.cloud.google.com/apis/library/calendar-json.googleapis.com', 'GET /learn-more-about/downgrading': '/docs/using-fleet/downgrading-fleet', + 'GET /learn-more-about/fleetd': '/docs/get-started/anatomy#fleetd', // Sitemap // ============================================================================================================= From 3b96743932333f44ccce2860743c8e7e538e3145 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Fri, 12 Apr 2024 17:16:13 -0300 Subject: [PATCH 70/83] document research done to extract name and version from installers (#18241) copying what's described here https://github.com/fleetdm/fleet/issues/17984#issuecomment-2051911744 for future reference. --- .../research/mdm/software-version-extract.md | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 docs/Contributing/research/mdm/software-version-extract.md diff --git a/docs/Contributing/research/mdm/software-version-extract.md b/docs/Contributing/research/mdm/software-version-extract.md new file mode 100644 index 0000000000..e23ed7dd73 --- /dev/null +++ b/docs/Contributing/research/mdm/software-version-extract.md @@ -0,0 +1,174 @@ +### Research: extracting name and version from installer packages + +> [!WARNING] +> This document is about extracting name and version from the installers, not +> about actually installing them on the device. +> +> For example, extracting info from `.dmg` files is hard for us, but installing +> those files should be a low effort task. + +| Type | Eng effort | Accuracy | UX notes | +| ------ | ---------- | -------- | ------------------------------------------- | +| `.dmg` | High | Medium | - | +| `.msi` | Medium | Medium | - | +| `.app` | Low | High | It's a folder, needs compression to upload. | +| `.pkg` | Low | High | - | +| `.exe` | Low | High | - | +| `.deb` | Low | High | - | + +More details: + +- Draft PR with a PoC implementation for `.app`, `.exe`, `.pgk`, `.deb` and half of `.msi` in #18232 +- Research notes with more details for each type below +- Additional concerns at the end of this doc + +### Windows Installer (.msi) + +`.msi` files are a relational database (!) laid out in [CFB format](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b) + +Getting the database tables in binary format form the CBF file is +[possible](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/msi.go#L40-L41), but extracting the information from the tables is a challenge +because the DB format is closed source and Microsoft doesn't disclose any +details about the implementation. + +That's why this is labeled as a `Medium` engineering effort. + +The strategy to parse the DB files is to rely on two tables: `_StringData` and `_StringPool`, +that contain all unique strings in the DB: + +> there is a single stream in the MSI file that holds all the strings. This +> stream is called the string pool contains a single entry for each unique +> string. That way a string column in a table is just an integer offset into +> the string pool. +> +> Source: https://robmensching.com/blog/posts/2003/11/25/inside-the-msi-file-format/ + +One possibly, but very low accuracy strategy could be to regex the contents of +`_StringData` for anything that looks like an application name or version. + +A more sophisticated approach is taken by [this Python library](https://github.com/binref/refinery/blob/de99c87f6dedd6d42508a3d436b6df9181837e34/refinery/units/formats/msi.py#L131) that is able to reverse engineer some of the data based on both tables: + +``` +$ emit fleet-osquery.msi | ./pyenv/bin/xtmsi MsiTables.json | jq '.Property[] | select(.Property == "ProductName" or .Property == "ProductVersion")' +{ + "Property": "ProductName", + "Value": "Fleet osquery" +} +{ + "Property": "ProductVersion", + "Value": "1.22.0" +} +``` + +A partial implementation that reads the CFB format can be found [here](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/msi.go). + +### Apple Disk Image (.dmg) + +From Wikipedia: + +> A disk image is a compressed copy of the contents of a disk or folder. Disk +> images have .dmg at the end of their names. To see the contents of a disk +> image, you must first open the disk image so it appears on the desktop or in +> a Finder window. + +There are two challenges that make `.dmg` files a High engineering effort: + +#### Finding the software + +A good mental model would be to imagine `.dmg` files as an USB stick that you +plug in a computer: it can contain anything, there are no rules about the kind +of files or the structure of them. + +My proposal to fix this problem would be to go for the 80% of the cases and +extract the information from the first `.app` or `.pkg` file we find and fail +if we don't find anything. + +#### Accessing the contents on the server + +With the strategy to find the software in place, we still need to access the +dmg contents on the server, from Wikipedia: + +> Different file systems can be contained inside these disk images, and there +> is also support for creating hybrid optical media images that contain +> multiple file systems. Some of the file systems supported include +> Hierarchical File System (HFS), HFS Plus (HFS+), File Allocation Table (FAT), +> ISO9660, and Universal Disk Format (UDF). + +Becuse we can't mount a `dmg` image in the server, and unless we find a +creative way to hack around this, we'll need to implement the logic to in Go. + +The only [library I could find](https://github.com/blacktop/go-apfs) is a WIP, +and failed to open Google Chrome and Slack `dmg` files provided in their +websites, but it's a good starting point if we decide to go this route. + +### Application Bundle (.app) + +[Application Bundles](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW5) can be thought as a file directory with a defined structure and file extension +that macOS treats as a single item. + +This folder contains all resources necessary for the app to run. As an example, +this is how the Firefox bundle is structured: + +``` +/Applications $ tree Firefox.app/ -L 2 +Firefox.app/ +└── Contents + ├── CodeResources + ├── Info.plist + ├── Library + ├── MacOS + ├── PkgInfo + ├── Resources + ├── _CodeSignature + └── embedded.provisionprofile +``` + +The `Info.plist` file is a required file that contains metadata about the app. +We can read the app version and the display name from there. + +Because a bundle is a folder, we'll need to ask the IT admin to upload the +bundle compressed (eg: zip, tar). + +Here's how different browsers behave when you try to upload an `.app` using a +file input: + +- Firefox treats it as a folder, and won't let you select it as a unit (screenshot) +- Safari and Chrome automatically compresses the folder in zip format (screenshot) + +A full implementation that reads the name and version from `Info.plist` can be found [here](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/app.go). + +### PKG installers (.pkg) + +Under the hood, `.pkg` installers are compressed files in `xar` format. + +PKG installers are required to have a [Distribution](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html) file from which we can extract the name and version. + +A full implementation that reads the name and version from the `Distribution` file +can be found [here](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/xar.go). + +### Portable Executable (.exe) + +The PE format is well documented in [here](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) + +The Go standard library provides a `"debug/pe"` package that we could use as a starting point, but it's not really tailored to our use case. + +The file is composed by different sections, and the name and version can be found in the [`.rsrc` section](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-rsrc-section) + +For the PoC, I used a Go library that's a bit heavy but does the heavy lifting for us ([link](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/pe.go)) + +### .deb + +Deb files are `ar` archives that contain a `control.tar` archive with +meta-information, including name and version. + +Code that extracts the values can be found [here](https://github.com/sassoftware/relic/blob/6c510a666832163a5d02587bda8be970d5e29b8c/lib/signdeb/control.go#L38-L39) + +## Additional considerations + +### Security + +In many cases, we'll have to write custom parsing logic or rely on third party libraries outside of the standard lib. + +Keeping that in mind we should take special care and consider any installer as untrusted input, common attacks for Go servers rely on malformed files that make the server OOM or panic. + + From 8859fe3ad7491b71794cf024b8f4333ea790cb67 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:36:37 -0400 Subject: [PATCH 71/83] [FE unit tests] Test agent information shown in host summary of host details page (#18246) --- frontend/__mocks__/hostMock.ts | 14 +- .../cards/HostSummary/HostSummary.tests.tsx | 143 ++++++++++++++++++ 2 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx diff --git a/frontend/__mocks__/hostMock.ts b/frontend/__mocks__/hostMock.ts index b9b36b248e..37b5f826bd 100644 --- a/frontend/__mocks__/hostMock.ts +++ b/frontend/__mocks__/hostMock.ts @@ -1,5 +1,9 @@ -import { IHost, IHostResponse } from "interfaces/host"; +import { IHost } from "interfaces/host"; import { IHostMdmProfile } from "interfaces/mdm"; +import { pick } from "lodash"; + +import { normalizeEmptyValues } from "utilities/helpers"; +import { HOST_SUMMARY_DATA } from "utilities/constants"; const DEFAULT_HOST_PROFILE_MOCK: IHostMdmProfile = { profile_uuid: "123-abc", @@ -35,7 +39,7 @@ const DEFAULT_HOST_MOCK: IHost = { platform: "ubuntu", osquery_version: "4.9.0", orbit_version: "1.22.0", - fleet_desktop_version: "1.22.0", + fleet_desktop_version: "1.22.1", os_version: "Ubuntu 18.4.0", build: "", platform_like: "debian", @@ -106,4 +110,10 @@ const createMockHost = (overrides?: Partial): IHost => { export const createMockHostResponse = { host: createMockHost() }; +export const createMockHostSummary = (overrides?: Partial) => { + return normalizeEmptyValues( + pick(createMockHost(overrides), HOST_SUMMARY_DATA) + ); +}; + export default createMockHost; diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx new file mode 100644 index 0000000000..da1258a28e --- /dev/null +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx @@ -0,0 +1,143 @@ +import React from "react"; +import { noop } from "lodash"; +import { screen } from "@testing-library/react"; +import { createCustomRenderer } from "test/test-utils"; + +import createMockUser from "__mocks__/userMock"; +import { createMockHostSummary } from "__mocks__/hostMock"; + +import HostSummary from "./HostSummary"; + +describe("Host Actions Dropdown", () => { + describe("Agent data", () => { + it("with all info present, render Agent header with orbit_version and tooltip with all 3 data points", async () => { + const render = createCustomRenderer({ + context: { + app: { + isPremiumTier: true, + isGlobalAdmin: true, + currentUser: createMockUser(), + }, + }, + }); + const summaryData = createMockHostSummary(); + const orbitVersion = summaryData.orbit_version as string; + const osqueryVersion = summaryData.osquery_version as string; + const fleetdVersion = summaryData.fleet_desktop_version as string; + + const { user } = render( + null} + /> + ); + + expect(screen.getByText("Agent")).toBeInTheDocument(); + await user.hover(screen.getByText(new RegExp(orbitVersion, "i"))); + + expect( + screen.getByText(new RegExp(osqueryVersion, "i")) + ).toBeInTheDocument(); + expect( + screen.getByText(new RegExp(fleetdVersion, "i")) + ).toBeInTheDocument(); + }); + + it("omit fleet desktop from tooltip if no fleet desktop version", async () => { + const render = createCustomRenderer({ + context: { + app: { + isPremiumTier: true, + isGlobalAdmin: true, + currentUser: createMockUser(), + }, + }, + }); + const summaryData = createMockHostSummary({ + fleet_desktop_version: null, + }); + const orbitVersion = summaryData.orbit_version as string; + const osqueryVersion = summaryData.osquery_version as string; + + const { user } = render( + null} + /> + ); + + expect(screen.getByText("Agent")).toBeInTheDocument(); + await user.hover(screen.getByText(new RegExp(orbitVersion, "i"))); + + expect( + screen.getByText(new RegExp(osqueryVersion, "i")) + ).toBeInTheDocument(); + expect(screen.queryByText(/Fleet desktop:/i)).not.toBeInTheDocument(); + }); + + it("for Chromebooks, render Agent header with osquery_version that is the fleetd chrome version and no tooltip", async () => { + const render = createCustomRenderer({ + context: { + app: { + isPremiumTier: true, + isGlobalAdmin: true, + currentUser: createMockUser(), + }, + }, + }); + const summaryData = createMockHostSummary({ + platform: "chrome", + osquery_version: "fleetd-chrome 1.2.0", + }); + + const fleetdChromeVersion = summaryData.osquery_version as string; + + const { user } = render( + null} + /> + ); + + expect(screen.getByText("Agent")).toBeInTheDocument(); + await user.hover(screen.getByText(new RegExp(fleetdChromeVersion, "i"))); + expect(screen.queryByText("Osquery")).not.toBeInTheDocument(); + }); + it("for non-Chromebooks with no orbit_version, render Osquery header with osquery_version and no tooltip", async () => { + const render = createCustomRenderer({ + context: { + app: { + isPremiumTier: true, + isGlobalAdmin: true, + currentUser: createMockUser(), + }, + }, + }); + const summaryData = createMockHostSummary({ + orbit_version: null, + }); + + const osqueryVersion = summaryData.osquery_version as string; + + const { user } = render( + null} + /> + ); + + expect(screen.getByText("Osquery")).toBeInTheDocument(); + await user.hover(screen.getByText(new RegExp(osqueryVersion, "i"))); + expect(screen.queryByText(/Orbit/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/Fleet desktop/i)).not.toBeInTheDocument(); + }); + }); +}); From 419634d368b6bff8e90d046afa9003e67a5904b0 Mon Sep 17 00:00:00 2001 From: Joanne Stableford <59930035+JoStableford@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:54:41 -0400 Subject: [PATCH 72/83] Configure google calendar integration in dogfood with API key (#18220) Related: https://github.com/fleetdm/confidential/issues/6015 --------- Co-authored-by: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> --- .github/workflows/dogfood-gitops.yml | 1 + it-and-security/default.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/dogfood-gitops.yml b/.github/workflows/dogfood-gitops.yml index 43761a5fbb..95f55254a3 100644 --- a/.github/workflows/dogfood-gitops.yml +++ b/.github/workflows/dogfood-gitops.yml @@ -64,3 +64,4 @@ jobs: DOGFOOD_SERVERS_ENROLL_SECRET: ${{ secrets.DOGFOOD_SERVERS_ENROLL_SECRET }} DOGFOOD_SERVERS_CANARY_ENROLL_SECRET: ${{ secrets.DOGFOOD_SERVERS_CANARY_ENROLL_SECRET }} DOGFOOD_EXPLORE_DATA_ENROLL_SECRET: ${{ secrets.DOGFOOD_EXPLORE_DATA_ENROLL_SECRET }} + DOGFOOD_CALENDAR_API_KEY: ${{ secrets.DOGFOOD_CALENDAR_API_KEY }} diff --git a/it-and-security/default.yml b/it-and-security/default.yml index a30c72224b..36edde9c74 100644 --- a/it-and-security/default.yml +++ b/it-and-security/default.yml @@ -31,6 +31,9 @@ org_settings: host_expiry_settings: host_expiry_enabled: false integrations: + google_calendar: + - api_key_json: $DOGFOOD_CALENDAR_API_KEY + domain: fleetdm.com jira: [ ] zendesk: [ ] mdm: From b032ddf06eb8eb0671d9c5bd250f302982854a83 Mon Sep 17 00:00:00 2001 From: Marko Lisica <83164494+marko-lisica@users.noreply.github.com> Date: Fri, 12 Apr 2024 23:34:38 +0200 Subject: [PATCH 73/83] Add missing searchable fields to docs (#17825) Docs are missing some of the searchable fields. --- docs/Contributing/API-for-contributors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index f46c32cdde..e7937927ed 100644 --- a/docs/Contributing/API-for-contributors.md +++ b/docs/Contributing/API-for-contributors.md @@ -1654,7 +1654,7 @@ for which the user has an observer role. | Name | Type | In | Description | | ----------------- | ------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| query | string | body | The query used to identify hosts to target. Searchable items include a host's hostname or IPv4 address. | +| query | string | body | The query used to identify hosts to target. Searchable items include a `display_name`, `hostname`, `hardware_serial`, `uuid` or `primary_ip`. | | query_id | integer | body | The saved query (if any) that will be run. The `observer_can_run` property on the query and the user's roles affect which targets are included. | | excluded_host_ids | array | body | The list of host ids to omit from the search results. | From 0888cdb8de583a6321e55ed3a71cb98772322195 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:39:56 -0400 Subject: [PATCH 74/83] Update end user auth and SSO docs (#18127) Docs improvements uncovered during [dogfooding](https://github.com/fleetdm/confidential/issues/2506): - Update end user auth docs to link to SSO docs. So the user knows what do to get the necessary info from their IdP (create an Okta/GW app) - Cut content from SSO docs and move Okta and Google Workspace to top level headers - "IDP" => "IdP" - Use **bold** styling to indicate UI elements in docs (instead of _italics_) --------- Co-authored-by: Rachael Shaw --- docs/Deploy/single-sign-on-sso.md | 189 +++++++----------- .../Using Fleet/MDM-macOS-setup-experience.md | 105 +--------- website/views/pages/integrations.ejs | 6 +- 3 files changed, 83 insertions(+), 217 deletions(-) diff --git a/docs/Deploy/single-sign-on-sso.md b/docs/Deploy/single-sign-on-sso.md index 38be78dfd1..fbc90ec779 100644 --- a/docs/Deploy/single-sign-on-sso.md +++ b/docs/Deploy/single-sign-on-sso.md @@ -1,76 +1,99 @@ # Single sign-on (SSO) -Learn how to configure single sign-on (SSO) and just-in-time (JIT) user provisioning. +Fleet supports SSO and just-in-time (JIT) user provisioning using any identity provider (IdP) that supports SAML. -## Overview +Fleet supports both service (SP) initiated login and IdP initiated login. -Fleet supports SAML single sign-on capability. +To configure SSO, follow steps for your IdP and then complete [Fleet configuration](#fleet-configuration). -Fleet supports both SP-initiated SAML login and IDP-initiated login. However, IDP-initiated login must be enabled in the web interface's SAML single sign-on options. +## Okta -Fleet supports the SAML Web Browser SSO Profile using the HTTP Redirect Binding. +Create a new SAML app in Okta: -> Note: The email used in the SAML Assertion must match a user that already exists in Fleet unless you enable [JIT provisioning](#just-in-time-jit-user-provisioning).** +![Example Okta IdP Configuration](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/okta-idp-setup.png) -## Identity provider (IDP) configuration +If you're configuring [end user authentication](../Using%20Fleet/MDM-macOS-setup-experience.md#end-user-authentication-and-eula), use `https:///api/v1/fleet/mdm/sso/callback` for the **Single sign on URL** instead. -Setting up the service provider (Fleet) with an identity provider generally requires the following information: +Once configured, you will need to retrieve the issuer URI from **View Setup Instructions** and metadata URL from the **Identity Provider metadata** link within the application **Sign on** settings. See below for where to find them: -- _Assertion Consumer Service_ - This is the call-back URL that the identity provider - will use to send security assertions to Fleet. In Okta, this field is called _single sign-on URL_. On Google, it is "ACS URL." The value you supply will be a fully qualified URL consisting of your Fleet web address and the call-back path `/api/v1/fleet/sso/callback`. For example, if your Fleet web address is https://fleet.example.com, then the value you would use in the identity provider configuration would be: - ```text - https://fleet.example.com/api/v1/fleet/sso/callback - ``` +![Where to find SSO links for Fleet](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/okta-retrieve-links.png) -- _Entity ID_ - This value is an identifier that you choose. It identifies your Fleet instance as the service provider that issues authorization requests. The value must match the Entity ID that you define in the Fleet SSO configuration. +> The Provider Sign-on URL within **View Setup Instructions** has a similar format as the Provider SAML Metadata URL, but this link provides a redirect to _sign into_ the application, not the metadata necessary for dynamic configuration. -- _Name ID Format_ - The value should be `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`. This may be shortened in the IDP setup to something like `email` or `EmailAddress`. +## Google Workspace -- _Subject Type (Application username in Okta)_ - `email`. +Create a new SAML app in Google Workspace: -After supplying the above information, the IDP will generate an issuer URI and metadata that will be used to configure Fleet as a service provider. +1. Navigate to the [Web and Mobile Apps](https://admin.google.com/ac/apps/unified) section of the Google Workspace dashboard. Click **Add App -> Add custom SAML app**. -## Fleet SSO configuration + ![The Google Workspace admin dashboard](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-1.png) -A Fleet user must be assigned the Admin role to configure Fleet for SSO. In Fleet, SSO configuration settings are located in **Settings > Organization settings > SAML single sign-on options**. +2. Enter "Fleet" for the **App name** and click **Continue**. -If your IDP supports dynamic configuration, like Okta, you only need to provide an _identity provider name_ and _entity ID_, then paste a link in the metadata URL field. Make sure you create the SSO application within your IDP before configuring it in Fleet. + ![Adding a new app to Google Workspace admin dashboard](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-2.png) -Otherwise, the following values are required: +3. Click **Download Metadata**, saving the metadata to your computer. Click **Continue**. -- _Identity provider name_ - A human-readable name of the IDP. This is rendered on the login page. + ![Download metadata](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-3.png) -- _Entity ID_ - A URI that identifies your Fleet instance as the issuer of authorization - requests (e.g., `fleet.example.com`). This must match the _Entity ID_ configured with the IDP. +5. Configure the **Service provider details**: -- _Metadata URL_ - Obtain this value from the IDP and is used by Fleet to - issue authorization requests to the IDP. + - For **ACS URL**, use `https:///api/v1/fleet/sso/callback`. If you're configuring [end user authentication](../Using%20Fleet/MDM-macOS-setup-experience.md#end-user-authentication-and-eula), use `https:///api/v1/fleet/mdm/sso/callback` instead. + - For Entity ID, use **the same unique identifier from step four** (e.g., "fleet.example.com"). + - For **Name ID format**, choose `EMAIL`. + - For **Name ID**, choose `Basic Information > Primary email`. + - All other fields can be left blank. -- _Metadata_ - If the IDP does not provide a metadata URL, the metadata must - be obtained from the IDP and entered. Note that the metadata URL is preferred if - the IDP provides metadata in both forms. + Click **Continue** at the bottom of the page. -### Example Fleet SSO configuration + ![Configuring the service provider details in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-5.png) + +6. Click **Finish**. + + ![Finish configuring the new SAML app in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-6.png) + +7. Click the down arrow on the **User access** section of the app details page. + + ![The new SAML app's details page in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-7.png) + +8. Check **ON for everyone**. Click **Save**. + + ![The new SAML app's service status page in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-8.png) + +9. Enable SSO for a test user and try logging in. Note that Google sometimes takes a long time to propagate the SSO configuration, and it can help to try logging in to Fleet with an Incognito/Private window in the browser. + +## Other IdPs + +IdPs generally requires the following information: + +- Assertion Consumer Service - This is the call-back URL that the identity provider will use to send security assertions to Fleet. Use `https:///api/v1/fleet/sso/callback`. If you're configuring end user authentication, use `https:///api/v1/fleet/mdm/sso/callback` instead. + +- Entity ID - This value is an identifier that you choose. It identifies your Fleet instance as the service provider that issues authorization requests. The value must match the Entity ID that you define in the Fleet SSO configuration. + +- Name ID Format - The value should be `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`. This may be shortened in the IdP setup to something like `email` or `EmailAddress`. + +- Subject Type - `email`. + +After supplying the above information, your IdP will generate an issuer URI and metadata that will be used to configure Fleet as a service provider. + +## Fleet configuration + +To configure SSO in Fleet head to **Settings > Organization settings > Single sign-on options**. + +If you're configuring end user authentication head to **Settings > Integrations > Automatic enrollment > End user authentication**. + +- **Identity provider name** - A human-readable name of the IdP. This is rendered on the login page. + +- **Entity ID** - A URI that identifies your Fleet instance as the issuer of authorization requests (e.g., `fleet.example.com`). This must match the Entity ID configured with the IdP. + +- **Metadata URL** - Obtain this value from your IdP. and is used by Fleet to + issue authorization requests to the IdP. + +- **Metadata** - If the IdP does not provide a metadata URL, the metadata must + be obtained from the IdP and entered. Coming soon to end user authentication. ![Example SSO Configuration](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/sso-setup.png) -## Creating SSO users in Fleet - -When an admin creates a new user in Fleet, they may select the `Enable single sign on` option. The -SSO-enabled users will not be able to sign in with a regular user ID and password. - -It is strongly recommended that at least one admin user is set up to use the traditional password-based login so that there is a fallback method for logging into Fleet in the event of SSO -configuration problems. - -> Individual users must also be set up on the IDP before signing in to Fleet. - -## Enabling SSO for existing users in Fleet -As an admin, you can enable SSO for existing users in Fleet. To do this, go to the Settings page, -then click on the Users tab. Locate the user you want to enable SSO for, and in the Actions dropdown -menu for that user, click on "Edit." In the dialogue that opens, check the box labeled "Enable -single sign-on," then click "Save." If you are unable to check that box, you must first [configure -and enable SSO for the organization](https://fleetdm.com/docs/deploying/configuration#configuring-single-sign-on-sso). - ## Just-in-time (JIT) user provisioning `Applies only to Fleet Premium` @@ -85,8 +108,8 @@ To enable this option, go to **Settings > Organization settings > Single sign-on For this to work correctly make sure that: -- Your IDP is configured to send the user email as the Name ID (instructions for configuring different providers are detailed below) -- Your IDP sends the full name of the user as an attribute with any of the following names (if this value is not provided Fleet will fallback to the user email) +- Your IdP is configured to send the user email as the Name ID (instructions for configuring different providers are detailed below) +- Your IdP sends the full name of the user as an attribute with any of the following names (if this value is not provided Fleet will fallback to the user email) - `name` - `displayname` - `cn` @@ -167,73 +190,7 @@ Here's a `SAMLResponse` sample to set the role of SSO users to `observer` in tea Each IdP will have its own way of setting these SAML custom attributes, here are instructions for how to set it for Okta: https://support.okta.com/help/s/article/How-to-define-and-configure-a-custom-SAML-attribute-statement?language=en_US. -### Okta IDP configuration - -![Example Okta IDP Configuration](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/okta-idp-setup.png) - -Once configured, you will need to retrieve the Issuer URI from the `View Setup Instructions` and metadata URL from the `Identity Provider metadata` link within the application `Sign on` settings. See below for where to find them: - -![Where to find SSO links for Fleet](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/okta-retrieve-links.png) - -> The Provider Sign-on URL within the `View Setup Instructions` has a similar format as the Provider SAML Metadata URL, but this link provides a redirect to _sign into_ the application, not the metadata necessary for dynamic configuration. - -> The names of the items required to configure an identity provider may vary from provider to provider and may not conform to the SAML spec. - -### Google Workspace IDP Configuration - -Follow these steps to configure Fleet SSO with Google Workspace. This will require administrator permissions in Google Workspace. - -1. Navigate to the [Web and Mobile Apps](https://admin.google.com/ac/apps/unified) section of the Google Workspace dashboard. Click _Add App -> Add custom SAML app_. - - ![The Google Workspace admin dashboard](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-1.png) - -2. Enter `Fleet` for the _App name_ and click _Continue_. - - ![Adding a new app to Google workspace admin dashboard](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-2.png) - -3. Click _Download Metadata_, saving the metadata to your computer. Click _Continue_. - - ![Download metadata](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-3.png) - -4. In Fleet, navigate to the _Organization Settings_ page. Configure the _SAML single sign-on options_ section. - - - Check the _Enable single sign-on_ checkbox. - - For _Identity provider name_, use `Google`. - - For _Entity ID_, use a unique identifier such as `fleet.example.com`. Note that Google seems to error when the provided ID includes `https://`. - - For _Metadata_, paste the contents of the downloaded metadata XML from step three. - - All other fields can be left blank. - - Click _Update settings_ at the bottom of the page. - - ![Fleet's SAML single sign on options page](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-4.png) - -5. In Google Workspace, configure the _Service provider details_. - - - For _ACS URL_, use `https:///api/v1/fleet/sso/callback` (e.g., `https://fleet.example.com/api/v1/fleet/sso/callback`). - - For Entity ID, use **the same unique identifier from step four** (e.g., `fleet.example.com`). - - For _Name ID format_, choose `EMAIL`. - - For _Name ID_, choose `Basic Information > Primary email`. - - All other fields can be left blank. - - Click _Continue_ at the bottom of the page. - - ![Configuring the service provider details in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-5.png) - -6. Click _Finish_. - - ![Finish configuring the new SAML app in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-6.png) - -7. Click the down arrow on the _User access_ section of the app details page. - - ![The new SAML app's details page in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-7.png) - -8. Check _ON for everyone_. Click _Save_. - - ![The new SAML app's service status page in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-8.png) - -9. Enable SSO for a test user and try logging in. Note that Google sometimes takes a long time to propagate the SSO configuration, and it can help to try logging in to Fleet with an Incognito/Private window in the browser. - - \ No newline at end of file + diff --git a/docs/Using Fleet/MDM-macOS-setup-experience.md b/docs/Using Fleet/MDM-macOS-setup-experience.md index d440667761..3f89f24941 100644 --- a/docs/Using Fleet/MDM-macOS-setup-experience.md +++ b/docs/Using Fleet/MDM-macOS-setup-experience.md @@ -18,110 +18,19 @@ MacOS setup features require connecting Fleet to Apple Business Manager (ABM). L Using Fleet, you can require end users to authenticate with your identity provider (IdP) and agree to an end user license agreement (EULA) before they can use their new Mac. -To require end user authentication, we will do the following steps: +### End user authentication -1. Connect Fleet to your IdP -2. Upload a EULA to Fleet (optional) -3. Enable end user authentication +To require end user authentication, first [configure single sign-on (SSO)](../Deploy/single-sign-on-sso.md). Next, enable end user authentication by heading to to **Controls > Setup experience End user authentication** or use [Fleet's GitOps workflow](https://github.com/fleetdm/fleet-gitops). -### Step 1: connect Fleet to your IdP +If you've already configured SSO in Fleet, create a new SAML app in your IdP. In your new app, use `https:///api/v1/fleet/mdm/sso/callback` for the SSO URL. -Fleet UI: +In your IdP, make sure your end users' full names are set to one of the following attributes (depends on IdP): `name`, `displayname`, `cn`, `urn:oid:2.5.4.3`, or `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`. Fleet will automatically populate and lock the macOS local account **Full Name** with any of these. -1. Head to the **Settings > Integrations > Automatic enrollment** page. +In your IdP, set **Name ID** to email. Fleet will trim this email and use it to populate and lock the macOS local account **Account Name**. For example, a "johndoe@example.com" email turn into a "johndoe" account name. -2. Under **End user authentication**, enter your IdP credentials and select **Save**. +### EULA - > If you've already configured [single sign-on (SSO) for logging in to Fleet](https://fleetdm.com/docs/configuration/fleet-server-configuration#okta-idp-configuration), you'll need to create a separate app in your IdP so your end users can't log in to Fleet. In this separate app, use "https://fleetserver.com/api/v1/fleet/mdm/sso/callback" for the SSO URL. - -fleetctl CLI: - -1. Create a `fleet-config.yaml` file or add to your existing `config` YAML file: - - ```yaml - apiVersion: v1 - kind: config - spec: - mdm: - end_user_authentication: - identity_provider_name: "Okta" - entity_id: "https://fleetserver.com" - issuer_url: "https://okta-instance.okta.com/84598y345hjdsshsfg/sso/saml/metadata" - metadata_url: "https://okta-instance.okta.com/84598y345hjdsshsfg/sso/saml/metadata" - ... - ``` - -2. Fill in the relevant information from your IdP under the `mdm.end_user_authentication` key. - -3. Run the fleetctl `apply -f fleet-config.yml` command to add your IdP credentials. - -4. Confirm that your IdP credentials were saved by running `fleetctl get config`. - -### Step 2: upload a EULA to Fleet - -1. Head to the **Settings > Integrations > Automatic enrollment** page. - -2. Under **End user license agreement (EULA)**, select **Upload** and choose your EULA. - - > Uploading a EULA is optional. If you don't upload a EULA, the end user will skip this step and continue to the next step of the new Mac setup experience after they authenticate with your IdP. - -### Step 3: enable end user authentication - -You can enable end user authentication using the Fleet UI or fleetctl command-line tool. - -Fleet UI: - -1. Head to the **Controls > macOS settings > macOS setup > End user authentication** page. - -2. Choose which team you want to enable end user authentication for by selecting the desired team in the teams dropdown in the upper left corner. - -3. Select the **On** checkbox and select **Save**. - -fleetctl CLI: - -1. Choose which team you want to enable end user authentication on. - - In this example, we'll enable end user authentication on the "Workstations (canary)" team so that the authentication is only required for hosts that automatically enroll to this team. - -2. Create a `workstations-canary-config.yaml` file: - - ```yaml - apiVersion: v1 - kind: team - spec: - team: - name: Workstations (canary) - mdm: - macos_setup: - enable_end_user_authentication: true - ... - ``` - - Learn more about team configurations options [here](./configuration-files/README.md#teams). - - If you want to enable authentication on hosts that automatically enroll to "No team," we'll need to create a `fleet-config.yaml` file: - - ```yaml - apiVersion: v1 - kind: config - spec: - mdm: - macos_setup: - enable_end_user_authentication: true - ... - ``` - -Learn more about "No team" configuration options [here](./configuration-files/README.md#organization-settings). - -3. Add an `mdm.macos_setup.enable_end_user_authentication` key to your YAML document. This key accepts a boolean value. - -4. Run the `fleetctl apply -f workstations-canary-config.yml` command to enable authentication for this team. - -5. Confirm that end user authentication is enabled by running the `fleetctl get teams --name=Workstations --yaml` command. - - If you enabled authentication on "No team," run `fleetctl get config`. - - You should see a `true` value for `mdm.macos_setup.enable_end_user_authentication`. +To require a EULA, in Fleet, head to **Settings > Integrations > Automatic enrollment > End user license agreement (EULA)** or use the [Fleet API](https://fleetdm.com/docs/rest-api/rest-api#upload-an-eula-file). ## Bootstrap package diff --git a/website/views/pages/integrations.ejs b/website/views/pages/integrations.ejs index 8e779e172d..f6bd0c3f81 100644 --- a/website/views/pages/integrations.ejs +++ b/website/views/pages/integrations.ejs @@ -19,7 +19,7 @@

@@ -77,7 +77,7 @@

@@ -286,7 +286,7 @@ Make smart authorization decisions by Integrating your device fleet’s ground truth with any IDP.

- Docs + Docs
From 29b482fffa54b6e7c5059bfe6416d5b432fa3c59 Mon Sep 17 00:00:00 2001 From: Rachael Shaw Date: Fri, 12 Apr 2024 18:01:52 -0500 Subject: [PATCH 75/83] #17230 API design: Fleet in your calendar (#17552) REST API updates for #17230. --------- Co-authored-by: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> --- docs/REST API/rest-api.md | 54 ++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 4e1f057990..37e9aa6ea8 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -981,7 +981,25 @@ None. } }, "integrations": { - "jira": null + "jira": null, + "google_calendar": [ + { + "domain": "example.com", + "api_key_json": { + "type": "service_account", + "project_id": "fleet-in-your-calendar", + "private_key_id": "", + "private_key": "-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----\n", + "client_email": "fleet-calendar-events@fleet-in-your-calendar.iam.gserviceaccount.com", + "client_id": "", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/fleet-calendar-events%40fleet-in-your-calendar.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" + } + } + ] }, "logging": { "debug": false, @@ -1083,6 +1101,8 @@ Modifies the Fleet's configuration with the supplied information. | email | string | body | _integrations.zendesk[] settings_. The Zendesk user email to use for this Zendesk integration. | | api_token | string | body | _integrations.zendesk[] settings_. The Zendesk API token to use for this Zendesk integration. | | group_id | integer | body | _integrations.zendesk[] settings_. The Zendesk group id to use for this integration. Zendesk tickets will be created in this group. | +| domain | string | body | _integrations.google_calendar[] settings_. The domain for the Google Workspace service account to be used for this calendar integration. | +| api_key_json | object | body | _integrations.google_calendar[] settings_. The private key JSON downloaded when generating the service account API key to be used for this calendar integration. | | apple_bm_default_team | string | body | _mdm settings_. The default team to use with Apple Business Manager. **Requires Fleet Premium license** | | windows_enabled_and_configured | boolean | body | _mdm settings_. Enables Windows MDM support. | | minimum_version | string | body | _mdm.macos_updates settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will be nudged until their macOS is at or above this version. **Requires Fleet Premium license** | @@ -1272,6 +1292,12 @@ Note that when making changes to the `integrations` object, all integrations mus "project_key": "jira_project", "enable_software_vulnerabilities": false } + ], + "google_calendar": [ + { + "domain": "", + "api_key_json": null + } ] }, "logging": { @@ -6009,7 +6035,8 @@ Team policies work the same as policies, but at the team level. "updated_at": "2021-12-16T16:39:00Z", "passing_host_count": 2000, "failing_host_count": 300, - "host_count_updated_at": "2023-12-20T15:23:57Z" + "host_count_updated_at": "2023-12-20T15:23:57Z", + "calendar_events_enabled": true }, { "id": 2, @@ -6027,7 +6054,8 @@ Team policies work the same as policies, but at the team level. "updated_at": "2021-12-16T16:39:00Z", "passing_host_count": 2300, "failing_host_count": 0, - "host_count_updated_at": "2023-12-20T15:23:57Z" + "host_count_updated_at": "2023-12-20T15:23:57Z", + "calendar_events_enabled": false } ], "inherited_policies": [ @@ -6116,7 +6144,8 @@ Team policies work the same as policies, but at the team level. "updated_at": "2021-12-16T16:39:00Z", "passing_host_count": 0, "failing_host_count": 0, - "host_count_updated_at": null + "host_count_updated_at": null, + "calendar_events_enabled": true } } ``` @@ -6181,7 +6210,8 @@ Either `query` or `query_id` must be provided. "updated_at": "2021-12-16T16:39:00Z", "passing_host_count": 0, "failing_host_count": 0, - "host_count_updated_at": null + "host_count_updated_at": null, + "calendar_events_enabled": false } } ``` @@ -6235,6 +6265,7 @@ Either `query` or `query_id` must be provided. | resolution | string | body | The resolution steps for the policy. | | platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. | | critical | boolean | body | _Available in Fleet Premium_. Mark policy as critical/high impact. | +| calendar_events_enabled | boolean | body | _Available in Fleet Premium_. Whether to trigger calendar events when policy is failing. | #### Example @@ -6275,7 +6306,8 @@ Either `query` or `query_id` must be provided. "updated_at": "2021-12-16T16:39:00Z", "passing_host_count": 0, "failing_host_count": 0, - "host_count_updated_at": null + "host_count_updated_at": null, + "calendar_events_enabled": true } } ``` @@ -8463,6 +8495,12 @@ _Available in Fleet Premium_ "host_batch_size": 0 } }, + "integrations": { + "google_calendar": { + "enable_calendar_events": true, + "webhook_url": "https://server.com/example" + } + }, "mdm": { "macos_updates": { "minimum_version": "12.3.1", @@ -8607,6 +8645,10 @@ _Available in Fleet Premium_ |     custom_settings | list | body | The list of objects where each object includes XML file (configuration profile) and label name to apply to Windows hosts that belong to this team and are members of the specified label. | |   macos_setup | object | body | Setup for automatic MDM enrollment of macOS hosts. | |     enable_end_user_authentication | boolean | body | If set to true, end user authentication will be required during automatic MDM enrollment of new macOS hosts. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#end-user-authentication-and-eula). | +| integrations | object | body | Integration settings for this team. | +|   google_calendar | object | body | Google Calendar integration settings. | +|     enable_calendar_events | boolean | body | Whether or not calendar events are enabled for this team. | +|     webhook_url | string | body | The URL to send a request to during calendar events, to trigger auto-remediation. | | host_expiry_settings | object | body | Host expiry settings for the team. | |   host_expiry_enabled | boolean | body | When enabled, allows automatic cleanup of hosts that have not communicated with Fleet in some number of days. When disabled, defaults to the global setting. | |   host_expiry_window | integer | body | If a host has not communicated with Fleet in the specified number of days, it will be removed. | From 500f33c512ebdf774cf89711df67d2f29dd1e61d Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 12 Apr 2024 18:21:56 -0500 Subject: [PATCH 76/83] Website: Update personalization on /endpoint-ops page (#18252) Changes: - Updated the /endpoint-ops page to display different content in the hero based on a users `primaryBuyingSituation` --- website/assets/styles/pages/endpoint-ops.less | 3 + website/views/pages/endpoint-ops.ejs | 68 ++++++++++++++----- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/website/assets/styles/pages/endpoint-ops.less b/website/assets/styles/pages/endpoint-ops.less index 09aa410aa4..aa92b9d1f2 100644 --- a/website/assets/styles/pages/endpoint-ops.less +++ b/website/assets/styles/pages/endpoint-ops.less @@ -636,6 +636,9 @@ } } @media (max-width: 472px) { + h1 { + font-size: 36px; + } [purpose='testimonial-videos'] { flex-direction: column; } diff --git a/website/views/pages/endpoint-ops.ejs b/website/views/pages/endpoint-ops.ejs index 9576f3d175..e464db9c83 100644 --- a/website/views/pages/endpoint-ops.ejs +++ b/website/views/pages/endpoint-ops.ejs @@ -4,19 +4,35 @@

Endpoint operations

-

A consistent interface

+

Understand your computers

A device verifying compliance for every endpoint
- Pulse check anything -

Use a live connection to every endpoint to simplify audit, compliance, and reporting from workstations to data centers.

- Ship data to any platform -

Ship logs to any platform like Splunk, Snowflake, or any streaming infrastructure like AWS Kinesis and Apache Kafka.

- Osquery on easy mode -

You don’t need to be an osquery expert to get the answers you need from your devices, Fleet does some of that for you.

+ <% if(primaryBuyingSituation === 'eo-it') { %> + Pulse check anything +

Use a live connection to every endpoint to simplify audit, compliance, and reporting from workstations to data centers.

+ Ship data to any platform +

Ship logs to any platform like Splunk, Snowflake, or any streaming infrastructure like AWS Kinesis and Apache Kafka.

+ Automate anything +

Remotely run scripts and prompts to complete tasks on every kind of computer, including Linux.

+ <% } else if(primaryBuyingSituation === 'eo-security') { %> + Osquery on easy mode +

You don’t need to be an osquery expert to get the answers you need from your devices, Fleet does some of that for you.

+ Pulse check anything +

Use a live connection to every endpoint to simplify audit, compliance, and reporting from workstations to data centers.

+ Ship data to any platform +

Ship logs to any platform like Splunk, Snowflake, or any streaming infrastructure like AWS Kinesis and Apache Kafka.

+ <% } else { %> + Pulse check anything +

Use a live connection to every endpoint to simplify audit, compliance, and reporting from workstations to data centers.

+ Ship data to any platform +

Ship logs to any platform like Splunk, Snowflake, or any streaming infrastructure like AWS Kinesis and Apache Kafka.

+ Osquery on easy mode +

You don’t need to be an osquery expert to get the answers you need from your devices, Fleet does some of that for you.

+ <% }%>
Start now Talk to us @@ -25,19 +41,35 @@
-
-
- an opening quotation mark -

I love the steady and consistent delivery of features that help teams work how they want to work, not how your product dictates they work.

-
-
-
Daniel Grzelak
-
-

Daniel Grzelak

-

CISO

+ <% if(primaryBuyingSituation === 'eo-it') { %> +
+
+ an opening quotation mark +

“Fleet provides a way to surface device data to our other teams and partners.”

+
+
+
Nick Fohs
+
+

Nick Fohs

+

Systems and infrastructure manager

+
-
+ <% } else { %> +
+
+ an opening quotation mark +

"I love the steady and consistent delivery of features that help teams work how they want to work, not how your product dictates they work."

+
+
+
Daniel Grzelak
+
+

Daniel Grzelak

+

CISO

+
+
+
+ <% } %>
PlayPlay video From 383fa4f252ee9039e6d2a10a28f6662f1577626c Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Fri, 12 Apr 2024 20:27:09 -0500 Subject: [PATCH 77/83] Update README.md (#18259) situation agnostic --- handbook/company/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/handbook/company/README.md b/handbook/company/README.md index ed5b339d5f..05bbe2f238 100644 --- a/handbook/company/README.md +++ b/handbook/company/README.md @@ -2,7 +2,7 @@ ## Purpose -Fleet Device Management Inc is an [open-core company](https://fleetdm.com/handbook/company/why-this-way#why-open-source) that sells subscriptions that offer [more features and support](https://fleetdm.com/pricing) for Fleet and osquery, the leading open-source systems management platform and security agent. Today, Fleet enrolls millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community). +Fleet is an [open-core company](https://fleetdm.com/handbook/company/why-this-way#why-open-source) that sells subscriptions that offer [more features and support](https://fleetdm.com/pricing) for Fleet and osquery, the leading open-source systems management platform and security agent. Today, Fleet enrolls millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linkedin.com/feed/update/urn:li:activity:7120880290859728897/). Fleet is dedicated to a comprehensive strategy against [whatever this is](https://chat.openai.com/share/e44ba6f3-b3ed-488a-a15e-a5a723f20c98): @@ -113,13 +113,13 @@ Ever wonder why there are 6 circles in the Fleet logo, but only 5 values? Behol ## History ### 2014: Origins of osquery -In 2014, our Cofounder Zach Wasserman, together with [Mike Arpaia](https://twitter.com/mikearpaia/status/1357455391588839424) and the rest of their team at Facebook, created an open source project called [osquery](https://osquery.io). +In 2014, our cofounder Zach Wasserman, together with [Mike Arpaia](https://twitter.com/mikearpaia/status/1357455391588839424) and the rest of their team at Facebook, created an open source project called [osquery](https://osquery.io). ### 2016: Origins of Fleet v1.0 A few years later, Zach, Mike Arpaia, and [Jason Meller](https://honest.security) founded [Kolide](https://kolide.com) and created Fleet: an open source platform that made it easier and more productive to use osquery in an enterprise setting. ### 2019: The growing community -When Kolide's attention shifted away from Fleet, and towards their separate, user-focused SaaS offering, the Fleet community took over maintenance of the open source project. After his time at Kolide, Zach continued as lead maintainer of Fleet. He spent 2019 consulting and working with the growing open source community to support and extend the capabilities of the Fleet platform. +When Kolide's attention shifted away from Fleet, and towards their separate, user-focused SaaS offering, the Fleet community [took over maintenance](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community) of the open-source project. After his time at Kolide, Zach continued as lead maintainer of Fleet. He spent 2019 consulting and working with the growing open source community to support and extend the capabilities of the Fleet platform. ### 2020: Fleet was incorporated Zach partnered with our [CEO, Mike McNeil](https://fleetdm.com/handbook/company/leadership#ceo-flaws), to found a new, independent company: Fleet Device Management Inc. In November 2020, we [announced](https://medium.com/fleetdm/a-new-fleet-d4096c7de978) the transition and kicked off the logistics of moving the GitHub repository. @@ -127,8 +127,10 @@ Zach partnered with our [CEO, Mike McNeil](https://fleetdm.com/handbook/company/ ### 2022: Millions of hosts Fleet raised its Series A funding round. The world now has at least 1.65 million computers and virtual hosts enrolled in Fleet, including enterprises, governments, startups, families, and hobbyist racks all over the world. -### 2024: Your last MDM migration -Fleet announces [support for Windows and Linux devices](https://fleetdm.com/announcements/fleet-introduces-windows-mdm), enabling IT departments to consolidate tools and implement “zero trust” faster using a modern Mac-first MDM. Removing the need for proprietary alternatives like Jamf Pro, Jamf Protect, Microsoft Intune, Ivanti MobileIron, and Broadcom's recently acquired Workspace ONE (originally known as "Airwatch"). +### 2023: Your last MDM migration +Fleet added support for [scripting and management capabilities](https://fleetdm.com/announcements/fleet-introduces-windows-mdm) on macOS, Windows, _and_ Linux devices, allowing IT departments to manage devices more consistently using modern tooling and best practices. This allowed many customers to simplify their management practices. In several cases, Fleet was also able to save customers several hundreds of thousands of dollars (USD) by cutting tool overlap across platforms such as Jamf, Airwatch, Intune, MobileIron, Nexthink, Tanium, Uptycs, and Rapid7. + + > Still curious? Check out this [visualization of the Fleet repo over the years](https://www.linkedin.com/feed/update/urn:li:activity:7045068060168220672/) or listen to this [conversation between Zach and Mike Arpaia about the origin story of osquery](https://fleetdm.com/podcasts/the-future-of-device-management-ep1). From c60fea52c473c0ae56f5a811d5aa982eff917d41 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Sat, 13 Apr 2024 01:36:11 -0400 Subject: [PATCH 78/83] #help-product-design => #help-design (#18247) Handbook changes for moving product discussion to #g-mdm/#g-endpoint-ops: fleetdm/confidential#6056 - Replace #help-product design w/ #help-design or #g-mdm or #g-endpoint ops - Cut "Maintain current versions". It's documented in product rituals --- .github/ISSUE_TEMPLATE/release-qa.md | 2 +- handbook/company/product-groups.md | 2 +- handbook/demand/README.md | 2 +- handbook/product-design/README.md | 22 +++++-------------- .../product-design/product-design.rituals.yml | 2 +- 5 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release-qa.md b/.github/ISSUE_TEMPLATE/release-qa.md index 6e29de6bb3..22b20081c1 100644 --- a/.github/ISSUE_TEMPLATE/release-qa.md +++ b/.github/ISSUE_TEMPLATE/release-qa.md @@ -147,7 +147,7 @@ Run the instructions in [tools/percona/test/README.md](../../tools/percona/test/ Release blockersVerify there are no outstanding release blocking tickets. 1. Check [this](https://github.com/fleetdm/fleet/labels/~release%20blocker) filter to view all open `~release blocker` tickets. -2. If any are found raise an alarm in the `#help-engineering` and `#help-product-design` channels. +2. If any are found raise an alarm in the `#help-engineering` and `#g-mdm` (or `#g-endpoint-ops`) channels. pass/fail diff --git a/handbook/company/product-groups.md b/handbook/company/product-groups.md index 506831d769..d85d80eeb7 100644 --- a/handbook/company/product-groups.md +++ b/handbook/company/product-groups.md @@ -273,7 +273,7 @@ All unreleased bugs are addressed before publishing a release. Released bugs tha - Introduces a security vulnerability ### Notify the community about a critical bug -We inform customers and the community about critical bugs immediately so they don’t trigger it themselves. When a bug meeting the definition of critical is found, the bug finder is responsible for raising an alarm. Raising an alarm means pinging @here in the #help-product-design channel with the filed bug. +We inform customers and the community about critical bugs immediately so they don’t trigger it themselves. When a bug meeting the definition of critical is found, the bug finder is responsible for raising an alarm. Raising an alarm means pinging @here in the `#g-mdm` or `#g-endpoint-ops` channel with the filed bug. If the bug finder is not a Fleetie (e.g., a member of the community), then whoever sees the critical bug should raise the alarm. Note that the bug finder here is NOT necessarily the **first** person who sees the bug. If you come across a bug you think is critical, but it has not been escalated, raise the alarm! diff --git a/handbook/demand/README.md b/handbook/demand/README.md index 74744dbbf4..2360016984 100644 --- a/handbook/demand/README.md +++ b/handbook/demand/README.md @@ -23,7 +23,7 @@ The Demand department is directly responsible for growing awareness of Fleet and 1. Check the [_from-prospective-customers](https://fleetdm.slack.com/archives/C01HE9GQW6B) Slack channel for "Contact us" submissions. 2. Mark submission as seen with the "👀" emoji. 3. Within 4 business hours, use the [best practices template (private Google doc)](https://docs.google.com/document/d/1D02k0tc5v-sEJ4uahAouuqnvZ6phxA_gP-IqmkBdMTE/edit) to respond to general asks. -4. Answer any technical questions to the best of your ability. If you are unable to answer a technical/product question, ask a Solutions Consultant in [#help-solutions-consulting](https://fleetdm.slack.com/archives/C05HZ2LHEL8). If an SC is unavailable, post in [#help-product-design](https://fleetdm.slack.com/archives/C02A8BRABB5) or [#help-engineering](https://fleetdm.slack.com/archives/C019WG4GH0A) and notify @on-call. +4. Answer any technical questions to the best of your ability. If you are unable to answer a technical/product question, ask a Solutions Consultant in `#help-solutions-consulting`. If an SC is unavailable, post in `#g-mdm`or `#g-endpoint-ops`and notify @on-call. 5. log in to [Salesforce](https://fleetdm.lightning.force.com/lightning/o/Lead/list?filterName=00B4x00000DtaRDEAZ) and search the lead list by first name and match the corresponding email to find the right lead. 6. Enrich each lead with company information and buying situation. 7. If a lead is completed or out of ICP, update the lead status in Salesforce to "Closed" or "Disqualified". If within ICP at-mention the [Head of Revenue Operations](https://fleetdm.com/handbook/digital-experience#team) in the [#g-digital-experience](https://fleetdm.slack.com/archives/C058S8PFSK0) Slack channel and move lead to their name in SFDC. diff --git a/handbook/product-design/README.md b/handbook/product-design/README.md index 835982d970..a560a0764e 100644 --- a/handbook/product-design/README.md +++ b/handbook/product-design/README.md @@ -11,7 +11,7 @@ This handbook page details processes specific to working [with](#contact-us) and ## Contact us -- To **make a request** of this department, [create an issue](https://github.com/fleetdm/confidential/issues/new?labels=%3Aproduct&title=Product%20design%20request%C2%BB______________________&template=custom-request.md) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in [#help-product-design](https://fleetdm.slack.com/archives/C02A8BRABB5). +- To **make a request** of this department, [create an issue](https://github.com/fleetdm/confidential/issues/new?labels=%3Aproduct&title=Product%20design%20request%C2%BB______________________&template=custom-request.md) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in `#help-design`. - Please **use issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request. - Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/-g-digital-experience-6451748b4eb15200131d4bab/board?sprints=none) for this department, including pending tasks and the status of new requests. @@ -22,7 +22,7 @@ The Product Design department is responsible for reviewing and collecting feedba - Once your designs are reviewed and approved, change the status on the cover page of the relevant Figma file and move the issue to the "Settled" column. - After each release (every 3 weeks) make sure you change the status on the cover page of the relevant Figma files that you worked on during the sprint to "Released". ->**Questions and missing information:** Take a screenshot of the area in Figma and start a thread in the [#help-product-design](https://fleetdm.slack.com/archives/C02A8BRABB5) Slack channel and paste in the screenshot. Figma does have a commenting system, but it is not easy to search for outstanding concerns and is therefore not preferred. +>**Questions and missing information:** Take a screenshot of the area in Figma and add a comment in the story's GitHub issue. Figma does have a commenting system, but it is not easy to search for outstanding concerns and is therefore not preferred. > >For external contributors: please consider opening an issue with reference screenshots if you have a Figma related question you need to resolve. @@ -85,8 +85,8 @@ You'll know it's time for expedited drafting when: - A user story on the drafting board won't reach "Settled" by the last estimation session in the current sprint and cannot wait until the next sprint. This can also happen when we decide to bring a user story in mid-sprint. What happens during expedited drafting? -1. If the user story wasn't "Settled" by the last estimation session, the product group's engineering manager (EM), [release DRI](https://fleetdm.com/handbook/company/communications#directly-responsible-individuals-dris), and Head of Product Design are notified in [#help-product-design](https://fleetdm.slack.com/archives/C02A8BRABB5). Decision to allow the user story to make it into the sprint is up to the release DRI. -2. If the user story is already in the sprint, the EM, release DRI, and Head of Product Design are notified in #help-product-design. If there are significant changes to the requirements, then the user story might be pushed to the next sprint. Decision is up to the release DRI. +1. If the user story wasn't "Settled" by the last estimation session, the product group's engineering manager (EM), [release DRI](https://fleetdm.com/handbook/company/communications#directly-responsible-individuals-dris), and Head of Product Design are notified in `#g-mdm` or `#g-endpoint-ops`. Decision to allow the user story to make it into the sprint is up to the release DRI. +2. If the user story is already in the sprint, the EM, release DRI, and Head of Product Design are notified in `#g-mdm` or `#g-endpoint-ops`. If there are significant changes to the requirements, then the user story might be pushed to the next sprint. Decision is up to the release DRI. 3. If the release DRI decides the user story will be worked on this sprint, drafts are updated or finished. 4. UI changes [are approved](https://fleetdm.com/handbook/company/development-groups#drafting-process), and the UI changes are brought back into the sprint or are estimated. @@ -160,18 +160,6 @@ The following highlights should be considered when deciding if we promote a feat explains why the feature is advertised as "beta" and tracking the feature's progress towards advertising the feature as "stable." - The feature will be advertised as "beta" in the documentation on fleetdm.com/docs, release notes, release blog posts, and Twitter. -### Maintain current versions -Fleet's product depends on the capabilities of other platforms. - -Every week, a member of the product team looks up whether there is: -1. a new major or minor version of [macOS](https://support.apple.com/en-us/HT201260) -2. a new major or minor version of [CIS Benchmarks Windows 10 Enterprise](https://workbench.cisecurity.org/community/2/benchmarks?q=windows+10+enterprise&status=&sortBy=version&type=desc) -3. a new major or minor version of [CIS Benchmarks macOS 13 Ventura](https://workbench.cisecurity.org/community/20/benchmarks?q=macos+13.0+Ventura&status=&sortBy=version&type=desc) -4. a release of CIS Benchmarks for [macOS 14 Sonoma](https://workbench.cisecurity.org/community/20/benchmarks?q=sonoma&status=&sortBy=version&type=desc) -5. a new major or minor version of [ChromeOS](https://chromereleases.googleblog.com/search/label/Chrome%20OS) - -The DRI should record the latest versions in the [maintenance tracker](https://docs.google.com/spreadsheets/d/1IWfQtSkOQgm_JIQZ0i2y3A8aaK5vQW1ayWRk6-4FOp0/edit#gid=0). If there are any changes, the DRI sends an update in the [#help-product-design Slack channel](https://fleetdm.slack.com/archives/C02A8BRABB5). - ### View Fleet usage statistics In order to understand the usage of the Fleet product, we [collect statistics](https://fleetdm.com/docs/using-fleet/usage-statistics) from installations where this functionality is enabled. @@ -190,7 +178,7 @@ Some of the data is forwarded to [Datadog](https://us5.datadoghq.com/dashboard/7 The following stubs are included only to make links backward compatible. ##### Maintenance -Please see [handbook/product#maintain-current-versions](https://fleetdm.com/handbook/product#maintain-current-versions) +Please see [handbook/product-design#rituals](https://fleetdm.com/handbook/product-design#rituals) ##### New CIS benchmarks Please see [handbook/product#submit-a-new-cis-benchmark-set-for-certification](https://fleetdm.com/handbook/product#submit-a-new-cis-benchmark-set-for-certification) diff --git a/handbook/product-design/product-design.rituals.yml b/handbook/product-design/product-design.rituals.yml index bc710c5674..bea04e39bb 100644 --- a/handbook/product-design/product-design.rituals.yml +++ b/handbook/product-design/product-design.rituals.yml @@ -68,7 +68,7 @@ task: "Maintenance" # 2024-03-06 TODO: Link to responsibility or corresponding "how to" info e.g. https://fleetdm.com/handbook/company/product-groups#making-changes startedOn: "2024-03-01" frequency: "Weekly" - description: "Head of Product Design checks the latest versions of relevant platforms, updates the maintenance tracker, and notifies the #help-product-design Slack channel." + description: "Head of Product Design checks the latest versions of relevant platforms, updates the maintenance tracker, and notifies the #g-mdm and #g-endpoint-ops Slack channel." moreInfoUrl: dri: "noahtalerman" - From d39d42cbb9e250dbe18a0f3a5ac759d6e3fdaabe Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Sat, 13 Apr 2024 13:32:07 -0500 Subject: [PATCH 79/83] Update communications.md (#18262) --- handbook/company/communications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handbook/company/communications.md b/handbook/company/communications.md index 72df3299c7..3a416712de 100644 --- a/handbook/company/communications.md +++ b/handbook/company/communications.md @@ -118,7 +118,7 @@ All invitations to meetings are welcomed, and quickly considering them is a top > **Note:** Please do not add events to the CEO's calendar. **Events added directly to the CEO's calendar will be declined and removed.** Even if the CEO asks you to set up a meeting or add him to a call, please get scheduling help from the [Apprentice](https://www.fleetdm.com/handbook/digital-experience#team)). -To request time with the CEO, [submit an issue](https://github.com/fleetdm/confidential/issues/new?assignees=sampfluger88&labels=%23g-digital-experience&projects=&template=custom-request.md&title=%7BNAME%7D%C2%BB______________________). Internal meetings can sometimes be moved to make room. External meetings, blocked time, travel, and personal commitments can rarely be moved. +To request time with the CEO, [submit an issue](https://github.com/fleetdm/confidential/issues/new?assignees=sampfluger88&labels=%23g-digital-experience&projects=&template=custom-request.md&title=%7BMeeting%20request%3A%20) at-mentioning the [Head of Digital Experience](https://www.fleetdm.com/handbook/digital-experience#team). Internal meetings can sometimes be moved to make room. External meetings, blocked time, travel, and personal commitments can rarely be moved. - **Why the extra step?** There are not enough hours in the day for the CEO to accept every request to meet, so [we have to prioritize](https://www.fleetdm.com/handbook/digital-experience#process-the-ceos-calendar). - **Self-service scheduling:** Unlike other team members, who you can schedule with by simply dropping an event on their calendar unless requested directly from Mike, please do not directly schedule a meeting onto the CEO's calendar without using this process to confirm with the Apprentice first. From 5f6d7dbf851659e635cef2a87ccf704883589754 Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:03:56 -0500 Subject: [PATCH 80/83] Add schedule a press release to Digital Experience page (#18263) --- handbook/digital-experience/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md index 132d0ffba7..f974034833 100644 --- a/handbook/digital-experience/README.md +++ b/handbook/digital-experience/README.md @@ -198,6 +198,19 @@ Fleet's public relations firm is directly responsible for the accuracy of event 2. Update the workbook with the latest location, dates, and CFP deadlines from the website. +### Schedule press release +Fleet will occasionally release information to the press regarding upcoming initiatives before updating the functionality of the core product. This process sUse the following steps to schedule a press release: + +1. Add context for the next press release to the [e-group agenda](https://docs.google.com/document/d/13fjq3T0bZGOUah9cqHVxngckv0EB2R24A3gfl5cH7eo/edit) as a "DISCUSS:" to be reviewed by Fleet's executive team for alignment and finalization of date. +2. Once a release date is set, at-mention our public relations firm in the [#help-public-relations-firm--mindshare-pr--brand-marketing](https://fleetdm.slack.com/archives/C04PC9H34LF) and schedule a 30m call for our CEO and to communicate the press release. + +> The above must be completed 6 weeks before the press release date. + +3. Schedule a 1.5h discussion between the [Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team) and the CEO to review the first draft linked as "Agenda: LINK" to the calendar event description. +4. Schedule a 60m call with the CEO and public relations firm to review the first draft linked as above to the calendar event (first draft provided by the PR firm) +5. Schedule 2.5 hrs of async time for the CEO work on edits and a 60m followup postgame (solo) where CEO edits and then settles+sends final release. + + ### Archive a document Follow these steps to archive any document: 1. Create a copy of the document prefixed with the date using the format "`YYYY-MM-DD` Backup of `DOCUMENT_NAME`" (e.g. "2024-03-22 Backup of 🪂🗞️ Customer voice"). From 158931046e54377cefb1ec6c54dad9d59e6af4ab Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Sat, 13 Apr 2024 17:36:51 -0500 Subject: [PATCH 81/83] Handbook: shortlinks and personalization redirects (#18258) --- website/api/hooks/custom/index.js | 21 ++++++++------------- website/config/routes.js | 7 ++++++- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/website/api/hooks/custom/index.js b/website/api/hooks/custom/index.js index b4998f3a23..20a7884014 100644 --- a/website/api/hooks/custom/index.js +++ b/website/api/hooks/custom/index.js @@ -145,23 +145,18 @@ will be disabled and/or hidden in the UI. res.locals.me = undefined; }//fi - // Check for UTM parameters for website personalization. + // Check for website personalization parameter, and if valid, absorb it in the session. + // (This makes the experience simpler and less confusing for people, prioritizing showing things that matter for them) // [?] https://en.wikipedia.org/wiki/UTM_parameters // e.g. - // https://fleetdm.com/device-management?utm_source=linkedin&utm_campaign=evergreen+leadgen&utm_content=mdm - if (['eo-security', 'eo-it', 'mdm', 'vm'].includes(req.param('utm_content'))) { - // If this is set to something weird, then we silently ignore it. - // Modify the active session instance. (This will be persisted when the response is sent.) - req.session.primaryBuyingSituation = req.param('utm_content'); - // FUTURE: Auto-redirect without the querystring after absorbtion to make it prettier in the URL bar. - // (except this probably messes up analytics so before doing that, figure out how to solve that problem) + // https://fleetdm.com/device-management?utm_content=mdm + if (['clear','eo-security', 'eo-it', 'mdm', 'vm'].includes(req.param('utm_content'))) { + req.session.primaryBuyingSituation = req.param('utm_content') === 'clear' ? undefined : req.param('utm_content'); + return res.redirect(req.path);// « auto-redirect without querystring to make it prettier in the URL bar. }//fi - if(req.param('utm_content') === 'clear'){ - req.session.primaryBuyingSituation = undefined; - } + if (req.method === 'GET' || req.method === 'HEAD') { - // Include information about the primary buying situation - // If set in the session (e.g. from an ad), use the primary buying situation for personalization. + // Include information about the primary buying situation for use in the HTML layout, views, and page scripts. res.locals.primaryBuyingSituation = req.session.primaryBuyingSituation || undefined; }//fi diff --git a/website/config/routes.js b/website/config/routes.js index 27dd2b5a3f..daa4c7b4bc 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -464,10 +464,15 @@ module.exports.routes = { 'GET /try-fleet/sandbox-expired': '/try-fleet', 'GET /try-fleet/sandbox': '/try-fleet', 'GET /try-fleet/waitlist': '/try-fleet', - 'GET /mdm': '/device-management',// « alias for radio ad 'GET /endpoint-operations': '/endpoint-ops',// « just in case we type it the wrong way 'GET /example-dep-profile': 'https://github.com/fleetdm/fleet/blob/main/it-and-security/lib/automatic-enrollment.dep.json', + // Shortlinks for texting friends, radio ads, etc + 'GET /mdm': '/device-management?utm_content=mdm',// « alias for radio ad + 'GET /it': '/endpoint-ops?utm_content=eo-it', + 'GET /seceng': '/endpoint-ops?utm_content=eo-security', + 'GET /vm': '/vulnerability-management?utm_content=vm', + // Fleet UI // ============================================================================================================= // Redirects for external links from the Fleet UI & CLI, including to fleetdm.com and to external websites not From 364117bffd5f5f39575d2aa712dcc9d0b4d01e07 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Mon, 15 Apr 2024 04:49:58 -0500 Subject: [PATCH 82/83] Document new vulnerabilities repo (#18236) Note that as we transition to the new `vulnerabilities` repo we will archive the `nvd` repo, so ultimately we will end up with the same number of repos as we have currently. --- handbook/company/why-this-way.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/handbook/company/why-this-way.md b/handbook/company/why-this-way.md index 010680a7b3..5fe726ce66 100644 --- a/handbook/company/why-this-way.md +++ b/handbook/company/why-this-way.md @@ -117,6 +117,9 @@ The only exceptions are: - _Confidential:_ [`fleetdm/confidential`](https://github.com/fleetdm/confidential) - _Classified (¶¶):_ [`fleetdm/classified`](https://github.com/fleetdm/classified) 3. **GitHub Actions:** Since GitHub requires GitHub Actions to live in dedicated repositories in order to submit them to the marketplace, Fleet uses a separate repo for publishing [GitHub Actions designed for other people to deploy and use (and/or fork)](https://github.com/fleetdm/fleet-mdm-gitops). +4. **Software vulnerabilities:** Since GitHub only allows one latest release per repository, we currently maintain two repositories to host our CVE/CPE database releases: + - _vulnerabilities:_ [`fleetdm/vulnerabilities`](https://github.com/fleetdm/vulnerabilities) + - _nvd:_ [`fleetdm/nvd`](https://github.com/fleetdm/nvd) Besides the exceptions above, Fleet does not use any other repositories. Other GitHub repositories in `fleetdm` should be archived and made private. From bcba0efd55f7b95c2803b65deee343add1eede94 Mon Sep 17 00:00:00 2001 From: Award Malisi <45960385+Unearthlyglow@users.noreply.github.com> Date: Mon, 15 Apr 2024 04:57:21 -0500 Subject: [PATCH 83/83] Documentation Feature-Request(Issue# 17485): Details For Skip Level 1:1 Meetings For Leadership.MD (#17545) ## Description of Change The Fleet handbook is currently without specific details regarding Skip-Level 1:1s for non-managerial Fleeties. This change request is being made to help the multiple parties involved by, outlining the Skip Level 1:1 process, listing recommendations for non-managerial Fleeties, and describing actions for managers to take to support their direct reports and the CEO in this process. Closes #17485 --------- Co-authored-by: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> --- handbook/company/communications.md | 14 +++++++++++++- handbook/company/leadership.md | 13 ++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/handbook/company/communications.md b/handbook/company/communications.md index 3a416712de..0285a14c3a 100644 --- a/handbook/company/communications.md +++ b/handbook/company/communications.md @@ -130,16 +130,28 @@ This works because every Fleetie grants edit access to everyone else at Fleet as ### Shared calendars Team calendars are the primary source for sprint rituals; they facilitate the execution of each sprint. - Looking to add, change, or remove a shared calendar? [Create an issue for the CEO](https://fleetdm.com/handbook/digital-experience#contact-us) and the appropriate DRI will reply with feedback. +## Skip-level 1:1 meetings + +Fleet uses skip-level 1:1 meetings as a recurring pulse check to encourage [valuable personal and departmental feedback](https://fleetdm.com/handbook/company/communications#performance-feedback) across the org. This helps the leadership at Fleet run an effective company with a great team, good alignment, and quick decisions. To schedule a skip-Level 1:1: +1. Create a copy of the ["Skip-level 1:1 agenda template"](https://docs.google.com/document/d/191wiy-_a9XBMndLlM97iOwUF6a-0PtkbboQ2FCUIy6w/copy) and rename the document "🧑‍🚀 YOUR_GITHUB_USER_NAME : SUPERVISOR_GITHUB_USER_NAME". +2. [Schedule a meeting](https://fleetdm.com/handbook/company/communications#internal-meeting-scheduling) with your manager's supervisor and title the calendar event by copying your skip-level agenda title and appending "[no shadows]" to the end (this tells other team members that this is a private conversation). + +> **If you're scheduling with the CEO** please [get help](https://fleetdm.com/handbook/company/communications#schedule-time-with-the-ceo) before adding events to the calendar. + +3. Link the skip-level agenda in the calendar event description before saving. + + ### Zoom + We use [Zoom](https://zoom.us) for virtual meetings at Fleet, and it is important that every team member feels comfortable hosting, joining, and scheduling Zoom meetings. By default, Zoom settings are the same for all Fleet team members, but you can change your personal settings on your [profile settings](https://zoom.us/profile/setting) page. Settings that have a lock icon next to them have been locked by an administrator and cannot be changed. Zoom administrators can change settings for all team members on the [account settings page](https://zoom.us/account/setting) or for individual accounts on the [user management page](https://zoom.us/account/user#/). ### Recording meetings + Capturing video from meetings with customers, prospects, and community members outside the company is an important part of building world-class sales and customer success teams and is a widespread practice across the industry. At Fleet, we use Gong to capture Zoom meetings and share them company-wide. If a team member with a Gong license attends certain meetings, generally those with at least one person from outside of Fleet in attendance. - While some Fleeties may have a Gong seat that is necessary in their work, the typical use case at Fleet is for employees on the company's sales, customer success, or customer support teams. - You should be notified anytime you join a recorded call with an audio message announcing "this meeting is being recorded" or "recording in progress." To stop a recording, the host of the call can press "Stop." diff --git a/handbook/company/leadership.md b/handbook/company/leadership.md index a0397c1d9b..6daab45283 100644 --- a/handbook/company/leadership.md +++ b/handbook/company/leadership.md @@ -401,12 +401,19 @@ From time to time, someone's job title changes. To do this, Business Operations 2. If there is a compensation change, update "Equity plan". Use the first day of a month as the date, and enter this in the corresponding column. 3. If applicable, schedule the change in the appropriate payroll system. (Don't worry about updating job titles in the payroll system.) -## Performance feedback +## Delivering performance feedback When it comes to performance feedback, [speak freely](https://fleetdm.com/handbook/company#openness), sooner, and provide an explicit example of the behavior you observed and the impact it had. 1. Deliver negative feedback privately whenever possible, and be constructive not punitive. Celebrate positive feedback publicly. -2. Performance mangement is a part of every 1:1 document. Start each 1:1 by delivering performance feedback. -3. When you meet with your manager for your 1:1, periodically provide an update on how each of your direct reports is doing at the top of your own "Performance management" section in your 1:1 agenda doc. +2. Performance management is a part of every 1:1 document. Start each 1:1 by delivering performance feedback. +3. When you meet with your manager for your 1:1, periodically provide an update on how each of your direct reports is doing at the top of your own "Performance management" section in your 1:1 agenda doc. + + +#### Stubs + +##### Performance feedback +Please see 📖[handbook/company/leadership#delivering-performance-feedback](https://fleetdm.com/handbook/company/leadership#delivering-performance-feedback). +