diff --git a/changes/18897-shoe-zeroes b/changes/18897-shoe-zeroes new file mode 100644 index 0000000000..7faddd522d --- /dev/null +++ b/changes/18897-shoe-zeroes @@ -0,0 +1 @@ +Added "0 items" description on empty software tables for UI consistency diff --git a/changes/20757-profiles-batch-activity b/changes/20757-profiles-batch-activity new file mode 100644 index 0000000000..6b110b87c7 --- /dev/null +++ b/changes/20757-profiles-batch-activity @@ -0,0 +1 @@ +API endpoint `/api/v1/fleet/mdm/profiles/batch` will now not log an activity for profile types that did not change in the database (Apple configuration profiles, Windows configuration profiles, or Apple declarations). diff --git a/changes/21412-remove-node-key-from-server-logs b/changes/21412-remove-node-key-from-server-logs new file mode 100644 index 0000000000..c6555bd5bc --- /dev/null +++ b/changes/21412-remove-node-key-from-server-logs @@ -0,0 +1 @@ +* Removed invalid node keys from server logs. diff --git a/changes/21428-policy-automatic-install-software b/changes/21428-policy-automatic-install-software new file mode 100644 index 0000000000..e61dc2a9ea --- /dev/null +++ b/changes/21428-policy-automatic-install-software @@ -0,0 +1 @@ +* Added automatic installation of software packages using policy automations. diff --git a/changes/21428-prevent-install-when-already-pending b/changes/21428-prevent-install-when-already-pending new file mode 100644 index 0000000000..d01006d6f9 --- /dev/null +++ b/changes/21428-prevent-install-when-already-pending @@ -0,0 +1 @@ +* Added validation to `POST /api/_version_/fleet/hosts/{host_id}/software/install/{software_title_id}` to prevent installing on a host that already has a pending installation for that software title. diff --git a/changes/21683-apns-cert-validation-on-start b/changes/21683-apns-cert-validation-on-start new file mode 100644 index 0000000000..9f17143599 --- /dev/null +++ b/changes/21683-apns-cert-validation-on-start @@ -0,0 +1,2 @@ +- Removed validation of APNS certificate from server startup. This was no longer necessary because + we now allow for APNS certificates to be renewed in the UI. diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 5be28feedd..67ce51f7d0 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -22,7 +22,6 @@ import ( "github.com/e-dard/netbug" "github.com/fleetdm/fleet/v4/ee/server/licensing" eeservice "github.com/fleetdm/fleet/v4/ee/server/service" - "github.com/fleetdm/fleet/v4/pkg/certificate" "github.com/fleetdm/fleet/v4/pkg/scripts" "github.com/fleetdm/fleet/v4/server" configpkg "github.com/fleetdm/fleet/v4/server/config" @@ -75,8 +74,10 @@ import ( var allowedURLPrefixRegexp = regexp.MustCompile("^(?:/[a-zA-Z0-9_.~-]+)+$") -const softwareInstallerUploadTimeout = 4 * time.Minute -const liveQueryMemCacheDuration = 1 * time.Second +const ( + softwareInstallerUploadTimeout = 4 * time.Minute + liveQueryMemCacheDuration = 1 * time.Second +) type initializer interface { // Initialize is used to populate a datastore with @@ -126,6 +127,10 @@ the way that the Fleet server works. logger := initLogger(config) + if dev { + createTestBucketForInstallers(&config, logger) + } + // Init tracing if config.Logging.TracingEnabled { ctx := context.Background() @@ -506,7 +511,7 @@ the way that the Fleet server works. initFatal(errors.New("inserting APNs and SCEP assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") } - apnsCert, apnsCertPEM, apnsKeyPEM, err := config.MDM.AppleAPNs() + _, apnsCertPEM, apnsKeyPEM, err := config.MDM.AppleAPNs() if err != nil { initFatal(err, "validate Apple APNs certificate and key") } @@ -516,18 +521,6 @@ the way that the Fleet server works. initFatal(err, "validate Apple SCEP certificate and key") } - const ( - apnsConnectionTimeout = 10 * time.Second - apnsConnectionURL = "https://api.sandbox.push.apple.com" - ) - - // check that the Apple APNs certificate is valid to connect to the API - ctx, cancel := context.WithTimeout(context.Background(), apnsConnectionTimeout) - if err := certificate.ValidateClientAuthTLSConnection(ctx, apnsCert, apnsConnectionURL); err != nil { - initFatal(err, "validate authentication with Apple APNs certificate") - } - cancel() - err = ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{ {Name: fleet.MDMAssetAPNSCert, Value: apnsCertPEM}, {Name: fleet.MDMAssetAPNSKey, Value: apnsKeyPEM}, @@ -583,6 +576,8 @@ the way that the Fleet server works. // backfilled tok := &fleet.ABMToken{ EncryptedToken: appleBM.EncryptedToken, + // 2000-01-01 is our "zero value" for time + RenewAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), } _, err = ds.InsertABMToken(context.Background(), tok) if err != nil { @@ -1404,3 +1399,18 @@ var _ push.Pusher = nopPusher{} func (n nopPusher) Push(context.Context, []string) (map[string]*push.Response, error) { return nil, nil } + +func createTestBucketForInstallers(config *configpkg.FleetConfig, logger log.Logger) { + store, err := s3.NewSoftwareInstallerStore(config.S3) + if err != nil { + initFatal(err, "initializing S3 software installer store") + } + if err := store.CreateTestBucket(config.S3.SoftwareInstallersBucket); err != nil { + // Don't panic, allow devs to run Fleet without minio/S3 dependency. + level.Info(logger).Log( + "err", err, + "msg", "failed to create test bucket", + "name", config.S3.SoftwareInstallersBucket, + ) + } +} diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 04c6fe3b5b..3980504e48 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -167,12 +167,15 @@ func TestApplyTeamSpecs(t *testing.T) { return nil } - ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error { - return nil + ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, + winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.NewActivityFunc = func( @@ -627,8 +630,9 @@ func TestApplyAppConfig(t *testing.T) { return nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { @@ -1250,11 +1254,14 @@ func TestApplyAsGitOps(t *testing.T) { teamEnrollSecrets = secrets return nil } - ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error { - return nil + ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, + winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) { return nil, ¬FoundError{} diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 752d3a65d5..cc44450b11 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -2271,11 +2271,14 @@ func TestGetTeamsYAMLAndApply(t *testing.T) { } return nil, fmt.Errorf("team not found: %s", name) } - ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error { - return nil + ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, + winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 4caaebd00e..fc9acc58a5 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -49,13 +49,13 @@ func TestBasicGlobalFreeGitOps(t *testing.T) { ds.BatchSetMDMProfilesFunc = func( ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, - ) error { - return nil + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.BulkSetPendingMDMHostProfilesFunc = func( ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string, - ) error { - return nil + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil } ds.NewActivityFunc = func( @@ -166,13 +166,13 @@ func TestBasicGlobalPremiumGitOps(t *testing.T) { ds.BatchSetMDMProfilesFunc = func( ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, - ) error { - return nil + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.BulkSetPendingMDMHostProfilesFunc = func( ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string, - ) error { - return nil + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil } ds.NewActivityFunc = func( @@ -277,13 +277,13 @@ func TestBasicTeamGitOps(t *testing.T) { ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil } ds.BatchSetMDMProfilesFunc = func( ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, - ) error { - return nil + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.BulkSetPendingMDMHostProfilesFunc = func( ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string, - ) error { - return nil + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.NewActivityFunc = func( ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, @@ -450,13 +450,14 @@ func TestFullGlobalGitOps(t *testing.T) { var appliedWinProfiles []*fleet.MDMWindowsConfigProfile ds.BatchSetMDMProfilesFunc = func( ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, - ) error { + ) (updates fleet.MDMProfilesUpdates, err error) { appliedMacProfiles = macProfiles appliedWinProfiles = winProfiles - return nil + return fleet.MDMProfilesUpdates{}, nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) { return job, nil @@ -625,13 +626,14 @@ func TestFullTeamGitOps(t *testing.T) { var appliedWinProfiles []*fleet.MDMWindowsConfigProfile ds.BatchSetMDMProfilesFunc = func( ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, - ) error { + ) (updates fleet.MDMProfilesUpdates, err error) { appliedMacProfiles = macProfiles appliedWinProfiles = winProfiles - return nil + return fleet.MDMProfilesUpdates{}, nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) { return job, nil @@ -927,10 +929,10 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) { ds.BatchSetMDMProfilesFunc = func( ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, - ) error { + ) (updates fleet.MDMProfilesUpdates, err error) { assert.Empty(t, macProfiles) assert.Empty(t, winProfiles) - return nil + return fleet.MDMProfilesUpdates{}, nil } ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { assert.Empty(t, scripts) @@ -938,9 +940,9 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) { } ds.BulkSetPendingMDMHostProfilesFunc = func( ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string, - ) error { + ) (updates fleet.MDMProfilesUpdates, err error) { assert.Empty(t, profileUUIDs) - return nil + return fleet.MDMProfilesUpdates{}, nil } ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { return nil @@ -1666,14 +1668,14 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, ds.BatchSetMDMProfilesFunc = func( ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, - ) error { - return nil + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil } ds.BulkSetPendingMDMHostProfilesFunc = func( ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string, - ) error { - return nil + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { return nil diff --git a/cmd/fleetctl/hosts_test.go b/cmd/fleetctl/hosts_test.go index a24fc79cbe..6220d6d75c 100644 --- a/cmd/fleetctl/hosts_test.go +++ b/cmd/fleetctl/hosts_test.go @@ -43,8 +43,9 @@ func TestHostsTransferByHosts(t *testing.T) { return nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) { @@ -114,8 +115,9 @@ func TestHostsTransferByLabel(t *testing.T) { return nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) { @@ -184,8 +186,9 @@ func TestHostsTransferByStatus(t *testing.T) { return nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) { @@ -243,8 +246,9 @@ func TestHostsTransferByStatusAndSearchQuery(t *testing.T) { return nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) { diff --git a/docs/Contributing/File-carving.md b/docs/Contributing/File-carving.md index 4daad97fb0..b283f11f4c 100644 --- a/docs/Contributing/File-carving.md +++ b/docs/Contributing/File-carving.md @@ -77,7 +77,7 @@ The same is not true if S3 is used as the storage backend. In that scenario, it ### Alternative carving backends -#### Minio +#### MinIO Configure the following: - `FLEET_S3_ENDPOINT_URL=minio_host:port` @@ -87,6 +87,11 @@ Configure the following: - `FLEET_S3_FORCE_S3_PATH_STYLE=true` - `FLEET_S3_REGION=minio` or any non-empty string otherwise Fleet will attempt to derive the region. +If you're testing file carving locally with the docker-compose environment, the `--dev` flag on Fleet server will +automatically point carves to the local MinIO container and write to the `carves-dev` bucket without needing to set +additional configuration. Note that this bucket is *not* created automatically when bringing MinIO up; you'll need to +log in via `http://localhost:9001` with credentials `minio` / `minio123!` to create the bucket. + ### Troubleshooting #### Check carve status in osquery diff --git a/docs/Contributing/Testing-and-local-development.md b/docs/Contributing/Testing-and-local-development.md index 8f6bc0ce80..45c5a93fe0 100644 --- a/docs/Contributing/Testing-and-local-development.md +++ b/docs/Contributing/Testing-and-local-development.md @@ -489,7 +489,9 @@ FLEET_SERVER_SANDBOX_ENABLED=1 FLEET_PACKAGING_GLOBAL_ENROLL_SECRET=xyz ./build Be sure to replace the `FLEET_PACKAGING_GLOBAL_ENROLL_SECRET` value above with the global enroll secret from the `fleetctl package` command used to build the installers. -MinIO also offers a web interface at http://localhost:9001. Credentials are `minio` / `minio123!`. +MinIO also offers a web interface at http://localhost:9001. Credentials are `minio` / `minio123!`. When starting the +Fleet server up with `--dev` the server will look for installers in the `software-installers-dev` MinIO bucket. You can +create this bucket via the MinIO web UI (it is *not* created by default when setting up the docker-compose environment). ## Telemetry diff --git a/docs/Deploy/deploy-fleet.md b/docs/Deploy/deploy-fleet.md index c0e80f4b2e..ee5299bfc3 100644 --- a/docs/Deploy/deploy-fleet.md +++ b/docs/Deploy/deploy-fleet.md @@ -43,7 +43,7 @@ Render is a cloud hosting service that makes it easy to get up and running fast, -1. Click "Deploy to Render" to open the Fleet Blueprint on Render. You will be prompted to create or log in to your Render account with associated payment information. +1. Click "Deploy to Render" to open the Fleet Blueprint on Render. Ensure that the Redis instance is manually set to the same region as your other resources. You will be prompted to create or log in to your Render account with associated payment information. 2. Give the Blueprint a unique name like `yourcompany-fleet`. diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index c4dd4bbadf..9d94428790 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -1072,7 +1072,7 @@ func (svc *Service) mdmAppleEditedAppleOSUpdates(ctx context.Context, teamID *ui // This only sets profiles that haven't been queued by the cron to 'pending' (both removes and installs, which includes // the OS updates we just deleted). It doesn't have a functional difference because if you don't call this function // the cron will catch up, but it's important for the UX to mark them as pending immediately so it's reflected in the UI. - if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{globalOrTeamID}, nil, nil); err != nil { + if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{globalOrTeamID}, nil, nil); err != nil { return ctxerr.Wrap(ctx, err, "bulk set pending host profiles") } return nil @@ -1105,7 +1105,7 @@ func (svc *Service) mdmAppleEditedAppleOSUpdates(ctx context.Context, teamID *ui return err } - if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil); err != nil { + if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil); err != nil { return ctxerr.Wrap(ctx, err, "bulk set pending host declarations") } return nil @@ -1271,10 +1271,13 @@ func (svc *Service) UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOS // validate the team IDs token.MacOSTeam = fleet.ABMTokenTeam{Name: fleet.TeamNameNoTeam} + token.MacOSDefaultTeamID = nil token.IOSTeam = fleet.ABMTokenTeam{Name: fleet.TeamNameNoTeam} + token.IOSDefaultTeamID = nil token.IPadOSTeam = fleet.ABMTokenTeam{Name: fleet.TeamNameNoTeam} + token.IPadOSDefaultTeamID = nil - if macOSTeamID != nil { + if macOSTeamID != nil && *macOSTeamID != 0 { macOSTeam, err := svc.ds.Team(ctx, *macOSTeamID) if err != nil { return nil, &fleet.BadRequestError{ @@ -1288,7 +1291,7 @@ func (svc *Service) UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOS token.MacOSDefaultTeamID = macOSTeamID } - if iOSTeamID != nil { + if iOSTeamID != nil && *iOSTeamID != 0 { iOSTeam, err := svc.ds.Team(ctx, *iOSTeamID) if err != nil { return nil, &fleet.BadRequestError{ @@ -1301,7 +1304,7 @@ func (svc *Service) UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOS token.IOSDefaultTeamID = iOSTeamID } - if iPadOSTeamID != nil { + if iPadOSTeamID != nil && *iPadOSTeamID != 0 { iPadOSTeam, err := svc.ds.Team(ctx, *iPadOSTeamID) if err != nil { return nil, &fleet.BadRequestError{ diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go index 3d9ecf9924..c6324f84fe 100644 --- a/ee/server/service/mdm_external_test.go +++ b/ee/server/service/mdm_external_test.go @@ -200,8 +200,9 @@ func TestGetOrCreatePreassignTeam(t *testing.T) { declaration.DeclarationUUID = uuid.NewString() return declaration, nil } - ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error { - return nil + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string, + ) (updates fleet.MDMProfilesUpdates, err error) { + return fleet.MDMProfilesUpdates{}, nil } apnsCert, apnsKey, err := mysql.GenerateTestCertBytes() require.NoError(t, err) diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index 153b0e76ba..d4969b7199 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -37,6 +37,7 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. if !ok { return fleet.ErrNoContext } + payload.UserID = vc.UserID() // make sure all scripts use unix-style newlines to prevent errors when // running them, browsers use windows-style newlines, which breaks the @@ -384,6 +385,24 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw // if we found an installer, use that if installer != nil { + lastInstallRequest, err := svc.ds.GetHostLastInstallData(ctx, host.ID, installer.InstallerID) + if err != nil { + return ctxerr.Wrapf(ctx, err, "getting last install data for host %d and installer %d", host.ID, installer.InstallerID) + } + if lastInstallRequest != nil && lastInstallRequest.Status != nil && *lastInstallRequest.Status == fleet.SoftwareInstallerPending { + return &fleet.BadRequestError{ + Message: "Couldn't install software. Host has a pending install request.", + InternalErr: ctxerr.WrapWithData( + ctx, err, "host already has a pending install for this installer", + map[string]any{ + "host_id": host.ID, + "software_installer_id": installer.InstallerID, + "team_id": host.TeamID, + "title_id": softwareTitleID, + }, + ), + } + } return svc.installSoftwareTitleUsingInstaller(ctx, host, installer) } } @@ -629,6 +648,14 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f } return "", ctxerr.Wrap(ctx, err, "extracting metadata from installer") } + + if meta.Version == "" { + return "", &fleet.BadRequestError{ + Message: fmt.Sprintf("Couldn't add. Fleet couldn't read the version from %s.", payload.Filename), + InternalErr: ctxerr.New(ctx, "extracting version from installer metadata"), + } + } + payload.Title = meta.Name if payload.Title == "" { // use the filename if no title from metadata @@ -686,6 +713,11 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin return ctxerr.Wrap(ctx, err, "validating authorization") } + vc, ok := viewer.FromContext(ctx) + if !ok { + return fleet.ErrNoContext + } + g, workerCtx := errgroup.WithContext(ctx) g.SetLimit(3) // critical to avoid data race, the slice is pre-allocated and each @@ -762,6 +794,7 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin PostInstallScript: p.PostInstallScript, InstallerFile: bytes.NewReader(bodyBytes), SelfService: p.SelfService, + UserID: vc.UserID(), } // set the filename before adding metadata, as it is used as fallback diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index f7dcbec15d..bb8e01342b 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -612,7 +612,7 @@ func (svc *Service) DeleteTeam(ctx context.Context, teamID uint) error { } if len(hostIDs) > 0 { - if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, hostIDs, nil, nil, nil); err != nil { + if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, hostIDs, nil, nil, nil); err != nil { return ctxerr.Wrap(ctx, err, "bulk set pending host profiles") } diff --git a/frontend/__mocks__/softwareMock.ts b/frontend/__mocks__/softwareMock.ts index 02a5f2d185..9ef0b14e9a 100644 --- a/frontend/__mocks__/softwareMock.ts +++ b/frontend/__mocks__/softwareMock.ts @@ -14,6 +14,8 @@ import { ISoftwareVersionsResponse, ISoftwareVersionResponse, } from "services/entities/software"; +import { IOSVersionsResponse } from "../services/entities/operating_systems"; +import { IOperatingSystemVersion } from "../interfaces/operating_system"; const DEFAULT_SOFTWARE_MOCK: ISoftware = { hosts_count: 1, @@ -93,12 +95,48 @@ const DEFAULT_SOFTWARE_VERSIONS_RESPONSE_MOCK: ISoftwareVersionsResponse = { }, }; -export const createMockSoftwareVersionsReponse = ( +export const createMockSoftwareVersionsResponse = ( overrides?: Partial ): ISoftwareVersionsResponse => { return { ...DEFAULT_SOFTWARE_VERSIONS_RESPONSE_MOCK, ...overrides }; }; +const DEFAULT_OS_VERSION_MOCK = { + os_version_id: 1, + name: "macOS 14.6.1", + name_only: "macOS", + version: "14.6.1", + platform: "darwin", + hosts_count: 42, + generated_cpes: [], + vulnerabilities: [], +}; + +export const createMockOSVersion = ( + overrides?: Partial +): IOperatingSystemVersion => { + return { + ...DEFAULT_OS_VERSION_MOCK, + ...overrides, + }; +}; + +const DEFAULT_OS_VERSIONS_RESPONSE_MOCK: IOSVersionsResponse = { + counts_updated_at: "2020-01-01T00:00:00.000Z", + count: 1, + os_versions: [createMockOSVersion()], + meta: { + has_next_results: false, + has_previous_results: false, + }, +}; + +export const createMockOSVersionsResponse = ( + overrides?: Partial +): IOSVersionsResponse => { + return { ...DEFAULT_OS_VERSIONS_RESPONSE_MOCK, ...overrides }; +}; + const DEFAULT_APP_STORE_APP_MOCK: IAppStoreApp = { name: "test app", app_store_id: 1, @@ -208,7 +246,7 @@ const DEFAULT_SOFTWARE_TITLES_RESPONSE_MOCK: ISoftwareTitlesResponse = { }, }; -export const createMockSoftwareTitlesReponse = ( +export const createMockSoftwareTitlesResponse = ( overrides?: Partial ): ISoftwareTitlesResponse => { return { ...DEFAULT_SOFTWARE_TITLES_RESPONSE_MOCK, ...overrides }; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index dfb8e3b016..0e4bb125fa 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -41,16 +41,19 @@ const PREMIUM_ACTIVITIES = new Set([ const getProfileMessageSuffix = ( isPremiumTier: boolean, + platform: "apple" | "windows", teamName?: string | null ) => { - let messageSuffix = <>hosts; + const platformDisplayName = + platform === "apple" ? "macOS, iOS, and iPadOS" : "Windows"; + let messageSuffix = <>all {platformDisplayName} hosts; if (isPremiumTier) { messageSuffix = teamName ? ( <> - the {teamName} team + {platformDisplayName} hosts assigned to the {teamName} team ) : ( - <>hosts with no team + <>{platformDisplayName} hosts with no team ); } return messageSuffix; @@ -364,7 +367,12 @@ const TAGGED_TEMPLATES = { ) : ( <>a configuration profile )}{" "} - to {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)} + to{" "} + {getProfileMessageSuffix( + isPremiumTier, + "apple", + activity.details?.team_name + )} . ); @@ -383,7 +391,12 @@ const TAGGED_TEMPLATES = { <>a configuration profile )}{" "} from{" "} - {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}. + {getProfileMessageSuffix( + isPremiumTier, + "apple", + activity.details?.team_name + )} + . ); }, @@ -394,6 +407,7 @@ const TAGGED_TEMPLATES = { edited configuration profiles for{" "} {getProfileMessageSuffix( isPremiumTier, + "apple", activity.details?.team_name )}{" "} via fleetctl. @@ -413,7 +427,12 @@ const TAGGED_TEMPLATES = { ) : ( <>a configuration profile )}{" "} - to {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)} + to{" "} + {getProfileMessageSuffix( + isPremiumTier, + "windows", + activity.details?.team_name + )} . ); @@ -432,7 +451,12 @@ const TAGGED_TEMPLATES = { <>a configuration profile )}{" "} from{" "} - {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}. + {getProfileMessageSuffix( + isPremiumTier, + "windows", + activity.details?.team_name + )} + . ); }, @@ -443,6 +467,7 @@ const TAGGED_TEMPLATES = { edited configuration profiles for{" "} {getProfileMessageSuffix( isPremiumTier, + "windows", activity.details?.team_name )}{" "} via fleetctl. @@ -762,7 +787,12 @@ const TAGGED_TEMPLATES = { added declaration (DDM) profile {activity.details?.profile_name} {" "} - to {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)} + to{" "} + {getProfileMessageSuffix( + isPremiumTier, + "apple", + activity.details?.team_name + )} . ); @@ -773,7 +803,12 @@ const TAGGED_TEMPLATES = { {" "} removed declaration (DDM) profile{" "} {activity.details?.profile_name} from{" "} - {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}. + {getProfileMessageSuffix( + isPremiumTier, + "apple", + activity.details?.team_name + )} + . ); }, @@ -783,7 +818,11 @@ const TAGGED_TEMPLATES = { {" "} edited declaration (DDM) profiles{" "} {activity.details?.profile_name} for{" "} - {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}{" "} + {getProfileMessageSuffix( + isPremiumTier, + "apple", + activity.details?.team_name + )}{" "} via fleetctl. ); diff --git a/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tests.tsx b/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tests.tsx index eb2672562e..934bd5db44 100644 --- a/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tests.tsx +++ b/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tests.tsx @@ -4,7 +4,7 @@ import { render, screen } from "@testing-library/react"; import OSTable from "./OSTable"; describe("Dashboard OS table", () => { - it("renders data normally when present", async () => { + it("renders data normally when present", () => { render( { + it("Renders the page-wide disabled state when software inventory is disabled", async () => { + render( + + ); + + expect(screen.getByText("Software inventory disabled")).toBeInTheDocument(); + }); + + it("Renders the page-wide empty state when no software is present", () => { + render( + + ); + + expect( + screen.getByText("No operating systems detected") + ).toBeInTheDocument(); + expect(screen.getByText("0 items")).toBeInTheDocument(); + expect(screen.queryByText("Search")).toBeNull(); + expect(screen.queryByText("Updated")).toBeNull(); + }); +}); diff --git a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx index b04f99fbe5..9be6ee4d16 100644 --- a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx @@ -129,7 +129,7 @@ const SoftwareOSTable = ({ }; const renderSoftwareCount = () => { - if (!data?.os_versions || !data?.count) return null; + if (!data) return null; return ( <> diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tests.tsx index add93fc3b1..1c07bc5cd7 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tests.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tests.tsx @@ -4,8 +4,8 @@ import { createCustomRenderer } from "test/test-utils"; import createMockUser from "__mocks__/userMock"; import { - createMockSoftwareTitlesReponse, - createMockSoftwareVersionsReponse, + createMockSoftwareTitlesResponse, + createMockSoftwareVersionsResponse, } from "__mocks__/softwareMock"; import { noop } from "lodash"; @@ -25,7 +25,7 @@ const mockRouter = { }; describe("Software table", () => { - it("Renders the page-wide disabled state when software inventory is disabled", async () => { + it("Renders the page-wide disabled state when software inventory is disabled", () => { const render = createCustomRenderer({ context: { app: { @@ -40,7 +40,7 @@ describe("Software table", () => { router={mockRouter} isSoftwareEnabled={false} // Set to false showVersions={false} - data={createMockSoftwareTitlesReponse({ + data={createMockSoftwareTitlesResponse({ counts_updated_at: null, software_titles: [], })} @@ -68,7 +68,7 @@ describe("Software table", () => { expect(screen.queryByText("Vulnerability")).toBeNull(); }); - it("Renders the page-wide empty state when no software are present", async () => { + it("Renders the page-wide empty state when no software are present", () => { const render = createCustomRenderer({ context: { app: { @@ -83,7 +83,8 @@ describe("Software table", () => { router={mockRouter} isSoftwareEnabled showVersions={false} - data={createMockSoftwareTitlesReponse({ + data={createMockSoftwareTitlesResponse({ + count: 0, counts_updated_at: null, software_titles: [], })} @@ -111,11 +112,12 @@ describe("Software table", () => { expect( screen.getByText("Expecting to see software? Check back later.") ).toBeInTheDocument(); + expect(screen.getByText("0 items")).toBeInTheDocument(); expect(screen.queryByText("Search")).toBeNull(); expect(screen.queryByText("Updated")).toBeNull(); }); - it("Renders the page-wide empty state when search query does not exist but versions toggle is applied", async () => { + it("Renders the page-wide empty state when search query does not exist but versions toggle is applied", () => { const render = createCustomRenderer({ context: { app: { @@ -130,7 +132,7 @@ describe("Software table", () => { router={mockRouter} isSoftwareEnabled showVersions // Versions toggle applied - data={createMockSoftwareVersionsReponse({ + data={createMockSoftwareVersionsResponse({ counts_updated_at: null, software: [], })} @@ -160,7 +162,7 @@ describe("Software table", () => { ).toBeInTheDocument(); }); - it("Renders the empty search state when search query does not exist but dropdown is applied", async () => { + it("Renders the empty search state when search query does not exist but dropdown is applied", () => { const render = createCustomRenderer({ context: { app: { @@ -175,7 +177,7 @@ describe("Software table", () => { router={mockRouter} isSoftwareEnabled showVersions={false} - data={createMockSoftwareTitlesReponse({ + data={createMockSoftwareTitlesResponse({ counts_updated_at: null, software_titles: [], })} @@ -209,7 +211,7 @@ describe("Software table", () => { ).toBeInTheDocument(); }); - it("Renders the empty search state when search query does not exist but vulnerability filter is applied", async () => { + it("Renders the empty search state when search query does not exist but vulnerability filter is applied", () => { const render = createCustomRenderer({ context: { app: { @@ -224,7 +226,7 @@ describe("Software table", () => { router={mockRouter} isSoftwareEnabled showVersions={false} - data={createMockSoftwareTitlesReponse({ + data={createMockSoftwareTitlesResponse({ counts_updated_at: null, software_titles: [], })} diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx index 4985990537..9b8df49c2c 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx @@ -269,7 +269,7 @@ const SoftwareTable = ({ }; const renderSoftwareCount = () => { - if (!tableData || !data?.count) return null; + if (!tableData || !data) return null; return ( <> diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx index ea0ac48389..2e869892d1 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx @@ -24,7 +24,7 @@ const mockRouter = { }; describe("Software Vulnerabilities table", () => { - it("Renders the page-wide disabled state when software inventory is disabled", async () => { + it("Renders the page-wide disabled state when software inventory is disabled", () => { const render = createCustomRenderer({ context: { app: { @@ -62,7 +62,7 @@ describe("Software Vulnerabilities table", () => { }); // TODO: Reinstate collecting software view - it("Renders the page-wide empty state when no software vulnerabilities are present", async () => { + it("Renders the page-wide empty state when no software vulnerabilities are present", () => { const render = createCustomRenderer({ context: { app: { @@ -97,13 +97,14 @@ describe("Software Vulnerabilities table", () => { ); expect(screen.getByText("No vulnerabilities detected")).toBeInTheDocument(); + expect(screen.getByText("0 items")).toBeInTheDocument(); expect( screen.getByText("Expecting to see vulnerabilities? Check back later.") ).toBeInTheDocument(); expect(screen.queryByText("Vulnerability")).toBeNull(); }); - it("Renders the empty search state when search query does not exist but exploited vulnerabilities dropdown is applied", async () => { + it("Renders the empty search state when search query does not exist but exploited vulnerabilities dropdown is applied", () => { const render = createCustomRenderer({ context: { app: { @@ -145,7 +146,7 @@ describe("Software Vulnerabilities table", () => { expect(screen.queryByText("Vulnerability")).toBeNull(); }); - it("Renders the invalid CVE empty search state when search query wrapped in quotes is invalid with no results", async () => { + it("Renders the invalid CVE empty search state when search query wrapped in quotes is invalid with no results", () => { const render = createCustomRenderer({ context: { app: { @@ -188,7 +189,7 @@ describe("Software Vulnerabilities table", () => { expect(screen.queryByText("Vulnerability")).toBeNull(); }); - it("Renders the valid known CVE empty search state when search query wrapped in quotes is valid known CVE with no results", async () => { + it("Renders the valid known CVE empty search state when search query wrapped in quotes is valid known CVE with no results", () => { const render = createCustomRenderer({ context: { app: { @@ -233,7 +234,7 @@ describe("Software Vulnerabilities table", () => { expect(screen.queryByText("Vulnerability")).toBeNull(); }); - it("Renders the valid unknown CVE empty search state when search query wrapped in quotes is not a valid known CVE with no results", async () => { + it("Renders the valid unknown CVE empty search state when search query wrapped in quotes is not a valid known CVE with no results", () => { const render = createCustomRenderer({ context: { app: { @@ -276,7 +277,7 @@ describe("Software Vulnerabilities table", () => { expect(screen.queryByText("Vulnerability")).toBeNull(); }); - it("Renders premium columns", async () => { + it("Renders premium columns", () => { const render = createCustomRenderer({ context: { app: { diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx index d37eb63acb..390a3dc866 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx @@ -197,9 +197,9 @@ const SoftwareVulnerabilitiesTable = ({ }; const renderVulnerabilityCount = () => { - if (!data?.count) return null; + if (!data) return null; - const count = data.count; + const count = data?.count; return ( <> diff --git a/frontend/pages/SoftwarePage/components/AppStoreVpp/AppStoreVpp.tsx b/frontend/pages/SoftwarePage/components/AppStoreVpp/AppStoreVpp.tsx index 06108649bf..2eb7597997 100644 --- a/frontend/pages/SoftwarePage/components/AppStoreVpp/AppStoreVpp.tsx +++ b/frontend/pages/SoftwarePage/components/AppStoreVpp/AppStoreVpp.tsx @@ -38,11 +38,11 @@ const EnableVppCard = () => { Volume Purchasing Program (VPP) isn't enabled

- To add App Store apps, first enable VPP. + To add App Store apps, first add VPP.

diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/EditTeamsVppModal/EditTeamsVppModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/EditTeamsVppModal/EditTeamsVppModal.tsx index 8635499a0c..029380e60e 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/EditTeamsVppModal/EditTeamsVppModal.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/EditTeamsVppModal/EditTeamsVppModal.tsx @@ -209,7 +209,7 @@ const EditTeamsVppModal = ({ showArrow tipContent={
- You can’t choose teams because you already have a VPP token + You can't choose teams because you already have a VPP token assigned to all teams. First, edit teams for that VPP token to choose teams here.
@@ -223,6 +223,7 @@ const EditTeamsVppModal = ({ placeholder="Search teams" value={selectedValue} label="Teams" + className={`${baseClass}__vpp-dropdown`} wrapperClassName={`${baseClass}__form-field--vpp-teams ${ isDropdownDisabled ? `${baseClass}__form-field--disabled` : "" }`} @@ -230,7 +231,7 @@ const EditTeamsVppModal = ({ isDropdownDisabled ? undefined : ( <> Each team can have only one VPP token. Teams that already - have a VPP token won’t show up here. + have a VPP token won't show up here. ) } diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/EditTeamsVppModal/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/EditTeamsVppModal/_styles.scss index 9b6f508dc1..72ed9e4a10 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/EditTeamsVppModal/_styles.scss +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/EditTeamsVppModal/_styles.scss @@ -2,6 +2,13 @@ .component__tooltip-wrapper__element { width: 100%; // default component style was causing the select box not to be full width } + + // this is needed to wrap the selected team names in that are displayed + // in the dropdown select box. + .dropdown__select { + text-wrap: wrap; + } + // styles needed to make select look like figma design when disabled, // default styles in the Dropdown component were not enough &__form-field--disabled { diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/AutomaticEnrollmentSection/AppleAutomaticEnrollmentCard/AppleAutomaticEnrollmentCard.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/AutomaticEnrollmentSection/AppleAutomaticEnrollmentCard/AppleAutomaticEnrollmentCard.tsx index dd0ca4c96d..69d2c65ad5 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/AutomaticEnrollmentSection/AppleAutomaticEnrollmentCard/AppleAutomaticEnrollmentCard.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/AutomaticEnrollmentSection/AppleAutomaticEnrollmentCard/AppleAutomaticEnrollmentCard.tsx @@ -25,7 +25,7 @@ const AppleAutomaticEnrollmentCard = ({ "Add an Apple Business Manager (ABM) connection to automatically enroll newly " + "purchased Apple hosts when they're first unboxed and set up by your end users."; } else if (isAppleMdmOn && configured) { - msg = "Automatic enrollment for Apple (macOS, iOS, iPadOS) hosts enabled."; + msg = "Automatic enrollment for Apple (macOS, iOS, iPadOS) is enabled."; icon = "success"; } diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/VppSection/VppSection.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/VppSection/VppSection.tsx index e2b58dd29b..afcd3af3c2 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/VppSection/VppSection.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/VppSection/VppSection.tsx @@ -46,7 +46,7 @@ const VppCard = ({ isAppleMdmOn, isVppOn, router }: IVppCardProps) => {

- Volume Purchasing Program (VPP) enabled. + Volume Purchasing Program (VPP) is enabled.