From a6dcdca2ddffb25e6a77acefe5f94d43b3270403 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:39:29 -0400 Subject: [PATCH 01/25] Validate Premium license when uploading VPP tokens (#21720) #21315 Ensures VPP uploads are behind premium license. Also moved the VPP service methods to the correct file --- changes/21315-vpp-premium-license | 1 + ee/server/service/vpp.go | 144 +++++++++++++ server/service/mdm.go | 333 ------------------------------ server/service/vpp.go | 240 +++++++++++++++++++++ 4 files changed, 385 insertions(+), 333 deletions(-) create mode 100644 changes/21315-vpp-premium-license diff --git a/changes/21315-vpp-premium-license b/changes/21315-vpp-premium-license new file mode 100644 index 0000000000..2fd081703e --- /dev/null +++ b/changes/21315-vpp-premium-license @@ -0,0 +1 @@ +- Verify user has premium license before uploading VPP tokens diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go index 52f4c1f890..85c29652ca 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "io" "net/http" "sort" "strings" @@ -17,6 +18,9 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/apple/vpp" ) +// Used for overriding the env var value in testing +var testSetEmptyPrivateKey bool + // getVPPToken returns the base64 encoded VPP token, ready for use in requests to Apple's VPP API. // It returns an error if the token is expired. func (svc *Service) getVPPToken(ctx context.Context, teamID *uint) (string, error) { @@ -413,3 +417,143 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V return apps, nil } + +func (svc *Service) UploadVPPToken(ctx context.Context, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return nil, err + } + + privateKey := svc.config.Server.PrivateKey + if testSetEmptyPrivateKey { + privateKey = "" + } + + if len(privateKey) == 0 { + return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + + if token == nil { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + + tokenBytes, err := io.ReadAll(token) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "reading VPP token") + } + + locName, err := vpp.GetConfig(string(tokenBytes)) + if err != nil { + var vppErr *vpp.ErrorResponse + if errors.As(err, &vppErr) { + // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes + if vppErr.ErrorNumber == 9622 { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + } + return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") + } + + data := fleet.VPPTokenData{ + Token: string(tokenBytes), + Location: locName, + } + + tok, err := svc.ds.InsertVPPToken(ctx, &data) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "writing VPP token to db") + } + + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEnabledVPP{ + Location: locName, + }); err != nil { + return nil, ctxerr.Wrap(ctx, err, "create activity for upload VPP token") + } + + return tok, nil +} + +func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return nil, err + } + + privateKey := svc.config.Server.PrivateKey + if testSetEmptyPrivateKey { + privateKey = "" + } + + if len(privateKey) == 0 { + return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + + if token == nil { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + + tokenBytes, err := io.ReadAll(token) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "reading VPP token") + } + + locName, err := vpp.GetConfig(string(tokenBytes)) + if err != nil { + var vppErr *vpp.ErrorResponse + if errors.As(err, &vppErr) { + // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes + if vppErr.ErrorNumber == 9622 { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + } + return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") + } + + data := fleet.VPPTokenData{ + Token: string(tokenBytes), + Location: locName, + } + + tok, err := svc.ds.UpdateVPPToken(ctx, tokenID, &data) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "updating vpp token") + } + + return tok, nil +} + +func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return nil, err + } + + tok, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, teamIDs) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "updating vpp token team") + } + + return tok, nil +} + +func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionRead); err != nil { + return nil, err + } + + return svc.ds.ListVPPTokens(ctx) +} + +func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return err + } + tok, err := svc.ds.GetVPPToken(ctx, tokenID) + if err != nil { + return ctxerr.Wrap(ctx, err, "getting vpp token") + } + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDisabledVPP{ + Location: tok.Location, + }); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for delete VPP token") + } + + return svc.ds.DeleteVPPToken(ctx, tokenID) +} diff --git a/server/service/mdm.go b/server/service/mdm.go index db6ce00944..e50d283ee6 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -30,7 +30,6 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" - "github.com/fleetdm/fleet/v4/server/mdm/apple/vpp" "github.com/fleetdm/fleet/v4/server/mdm/assets" nanomdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/ptr" @@ -2542,335 +2541,3 @@ func (svc *Service) DeleteMDMAppleAPNSCert(ctx context.Context) error { return svc.ds.SaveAppConfig(ctx, appCfg) } - -//////////////////////////////////////////////////////////////////////////////// -// POST /api/_version_/vpp_tokens -//////////////////////////////////////////////////////////////////////////////// - -type uploadVPPTokenRequest struct { - File *multipart.FileHeader -} - -func (uploadVPPTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { - decoded := uploadVPPTokenRequest{} - - err := r.ParseMultipartForm(512 * units.MiB) - if err != nil { - return nil, &fleet.BadRequestError{ - Message: "failed to parse multipart form", - InternalErr: err, - } - } - - if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 { - return nil, &fleet.BadRequestError{ - Message: "token multipart field is required", - InternalErr: err, - } - } - - decoded.File = r.MultipartForm.File["token"][0] - - return &decoded, nil -} - -type uploadVPPTokenResponse struct { - Err error `json:"error,omitempty"` - Token *fleet.VPPTokenDB `json:"token,omitempty"` -} - -func (r uploadVPPTokenResponse) Status() int { return http.StatusAccepted } - -func (r uploadVPPTokenResponse) error() error { - return r.Err -} - -func uploadVPPTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - req := request.(*uploadVPPTokenRequest) - file, err := req.File.Open() - if err != nil { - return uploadVPPTokenResponse{Err: err}, nil - } - defer file.Close() - - tok, err := svc.UploadVPPToken(ctx, file) - if err != nil { - return uploadVPPTokenResponse{Err: err}, nil - } - - return uploadVPPTokenResponse{Token: tok}, nil -} - -func (svc *Service) UploadVPPToken(ctx context.Context, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return nil, err - } - - privateKey := svc.config.Server.PrivateKey - if testSetEmptyPrivateKey { - privateKey = "" - } - - if len(privateKey) == 0 { - return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } - - if token == nil { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - - tokenBytes, err := io.ReadAll(token) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "reading VPP token") - } - - locName, err := vpp.GetConfig(string(tokenBytes)) - if err != nil { - var vppErr *vpp.ErrorResponse - if errors.As(err, &vppErr) { - // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes - if vppErr.ErrorNumber == 9622 { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - } - return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") - } - - data := fleet.VPPTokenData{ - Token: string(tokenBytes), - Location: locName, - } - - tok, err := svc.ds.InsertVPPToken(ctx, &data) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "writing VPP token to db") - } - - if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEnabledVPP{ - Location: locName, - }); err != nil { - return nil, ctxerr.Wrap(ctx, err, "create activity for upload VPP token") - } - - return tok, nil -} - -//////////////////////////////////////////////////// -// PATCH /api/_version_/fleet/vpp_tokens/%d/renew // -//////////////////////////////////////////////////// - -type patchVPPTokenRenewRequest struct { - ID uint `url:"id"` - File *multipart.FileHeader -} - -func (patchVPPTokenRenewRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { - decoded := patchVPPTokenRenewRequest{} - - err := r.ParseMultipartForm(512 * units.MiB) - if err != nil { - return nil, &fleet.BadRequestError{ - Message: "failed to parse multipart form", - InternalErr: err, - } - } - - if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 { - return nil, &fleet.BadRequestError{ - Message: "token multipart field is required", - InternalErr: err, - } - } - - decoded.File = r.MultipartForm.File["token"][0] - - return &decoded, nil -} - -type patchVPPTokenRenewResponse struct { - Err error `json:"error,omitempty"` - Token *fleet.VPPTokenDB `json:"token,omitempty"` -} - -func (r patchVPPTokenRenewResponse) Status() int { return http.StatusAccepted } - -func (r patchVPPTokenRenewResponse) error() error { - return r.Err -} - -func patchVPPTokenRenewEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - req := request.(*patchVPPTokenRenewRequest) - file, err := req.File.Open() - if err != nil { - return patchVPPTokenRenewResponse{Err: err}, nil - } - defer file.Close() - - tok, err := svc.UpdateVPPToken(ctx, req.ID, file) - if err != nil { - return patchVPPTokenRenewResponse{Err: err}, nil - } - - return patchVPPTokenRenewResponse{Token: tok}, nil -} - -func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return nil, err - } - - privateKey := svc.config.Server.PrivateKey - if testSetEmptyPrivateKey { - privateKey = "" - } - - if len(privateKey) == 0 { - return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } - - if token == nil { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - - tokenBytes, err := io.ReadAll(token) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "reading VPP token") - } - - locName, err := vpp.GetConfig(string(tokenBytes)) - if err != nil { - var vppErr *vpp.ErrorResponse - if errors.As(err, &vppErr) { - // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes - if vppErr.ErrorNumber == 9622 { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - } - return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") - } - - data := fleet.VPPTokenData{ - Token: string(tokenBytes), - Location: locName, - } - - tok, err := svc.ds.UpdateVPPToken(ctx, tokenID, &data) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "updating vpp token") - } - - return tok, nil -} - -//////////////////////////////////////////////////// -// PATCH /api/_version_/fleet/vpp_tokens/%d/teams // -//////////////////////////////////////////////////// - -type patchVPPTokensTeamsRequest struct { - ID uint `url:"id"` - TeamIDs []uint `json:"teams"` -} - -type patchVPPTokensTeamsResponse struct { - Token *fleet.VPPTokenDB `json:"token,omitempty"` - Err error `json:"error,omitempty"` -} - -func (r patchVPPTokensTeamsResponse) error() error { return r.Err } - -func patchVPPTokensTeams(ctx context.Context, request any, svc fleet.Service) (errorer, error) { - req := request.(*patchVPPTokensTeamsRequest) - - tok, err := svc.UpdateVPPTokenTeams(ctx, req.ID, req.TeamIDs) - if err != nil { - return patchVPPTokensTeamsResponse{Err: err}, nil - } - return patchVPPTokensTeamsResponse{Token: tok}, nil -} - -func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return nil, err - } - - tok, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, teamIDs) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "updating vpp token team") - } - - return tok, nil -} - -/////////////////////////////////////////////// -// DELETE /api/_version_/fleet/vpp_tokens/%d // -/////////////////////////////////////////////// - -type getVPPTokensRequest struct{} - -type getVPPTokensResponse struct { - Tokens []*fleet.VPPTokenDB `json:"vpp_tokens"` - Err error `json:"error,omitempty"` -} - -func (r getVPPTokensResponse) error() error { return r.Err } - -func getVPPTokens(ctx context.Context, request any, svc fleet.Service) (errorer, error) { - tokens, err := svc.GetVPPTokens(ctx) - if err != nil { - return getVPPTokensResponse{Err: err}, nil - } - - if tokens == nil { - tokens = []*fleet.VPPTokenDB{} - } - - return getVPPTokensResponse{Tokens: tokens}, nil -} - -func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionRead); err != nil { - return nil, err - } - - return svc.ds.ListVPPTokens(ctx) -} - -type deleteVPPTokenRequest struct { - ID uint `url:"id"` -} - -type deleteVPPTokenResponse struct { - Err error `json:"error,omitempty"` -} - -func (r deleteVPPTokenResponse) error() error { return r.Err } - -func (r deleteVPPTokenResponse) Status() int { return http.StatusNoContent } - -func deleteVPPToken(ctx context.Context, request any, svc fleet.Service) (errorer, error) { - req := request.(*deleteVPPTokenRequest) - - err := svc.DeleteVPPToken(ctx, req.ID) - if err != nil { - return deleteVPPTokenResponse{Err: err}, nil - } - - return deleteVPPTokenResponse{}, nil -} - -func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return err - } - tok, err := svc.ds.GetVPPToken(ctx, tokenID) - if err != nil { - return ctxerr.Wrap(ctx, err, "getting vpp token") - } - if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDisabledVPP{ - Location: tok.Location, - }); err != nil { - return ctxerr.Wrap(ctx, err, "create activity for delete VPP token") - } - - return svc.ds.DeleteVPPToken(ctx, tokenID) -} diff --git a/server/service/vpp.go b/server/service/vpp.go index 04b1ac57a3..c2e25eddc0 100644 --- a/server/service/vpp.go +++ b/server/service/vpp.go @@ -2,7 +2,11 @@ package service import ( "context" + "io" + "mime/multipart" + "net/http" + "github.com/docker/go-units" "github.com/fleetdm/fleet/v4/server/fleet" ) @@ -73,3 +77,239 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, _ *uint, _ fleet.VPPAppT return fleet.ErrMissingLicense } + +//////////////////////////////////////////////////////////////////////////////// +// POST /api/_version_/vpp_tokens +//////////////////////////////////////////////////////////////////////////////// + +type uploadVPPTokenRequest struct { + File *multipart.FileHeader +} + +func (uploadVPPTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { + decoded := uploadVPPTokenRequest{} + + err := r.ParseMultipartForm(512 * units.MiB) + if err != nil { + return nil, &fleet.BadRequestError{ + Message: "failed to parse multipart form", + InternalErr: err, + } + } + + if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 { + return nil, &fleet.BadRequestError{ + Message: "token multipart field is required", + InternalErr: err, + } + } + + decoded.File = r.MultipartForm.File["token"][0] + + return &decoded, nil +} + +type uploadVPPTokenResponse struct { + Err error `json:"error,omitempty"` + Token *fleet.VPPTokenDB `json:"token,omitempty"` +} + +func (r uploadVPPTokenResponse) Status() int { return http.StatusAccepted } + +func (r uploadVPPTokenResponse) error() error { + return r.Err +} + +func uploadVPPTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*uploadVPPTokenRequest) + file, err := req.File.Open() + if err != nil { + return uploadVPPTokenResponse{Err: err}, nil + } + defer file.Close() + + tok, err := svc.UploadVPPToken(ctx, file) + if err != nil { + return uploadVPPTokenResponse{Err: err}, nil + } + + return uploadVPPTokenResponse{Token: tok}, nil +} + +func (svc *Service) UploadVPPToken(ctx context.Context, file io.ReadSeeker) (*fleet.VPPTokenDB, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +//////////////////////////////////////////////////// +// PATCH /api/_version_/fleet/vpp_tokens/%d/renew // +//////////////////////////////////////////////////// + +type patchVPPTokenRenewRequest struct { + ID uint `url:"id"` + File *multipart.FileHeader +} + +func (patchVPPTokenRenewRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { + decoded := patchVPPTokenRenewRequest{} + + err := r.ParseMultipartForm(512 * units.MiB) + if err != nil { + return nil, &fleet.BadRequestError{ + Message: "failed to parse multipart form", + InternalErr: err, + } + } + + if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 { + return nil, &fleet.BadRequestError{ + Message: "token multipart field is required", + InternalErr: err, + } + } + + decoded.File = r.MultipartForm.File["token"][0] + + return &decoded, nil +} + +type patchVPPTokenRenewResponse struct { + Err error `json:"error,omitempty"` + Token *fleet.VPPTokenDB `json:"token,omitempty"` +} + +func (r patchVPPTokenRenewResponse) Status() int { return http.StatusAccepted } + +func (r patchVPPTokenRenewResponse) error() error { + return r.Err +} + +func patchVPPTokenRenewEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*patchVPPTokenRenewRequest) + file, err := req.File.Open() + if err != nil { + return patchVPPTokenRenewResponse{Err: err}, nil + } + defer file.Close() + + tok, err := svc.UpdateVPPToken(ctx, req.ID, file) + if err != nil { + return patchVPPTokenRenewResponse{Err: err}, nil + } + + return patchVPPTokenRenewResponse{Token: tok}, nil +} + +func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +//////////////////////////////////////////////////// +// PATCH /api/_version_/fleet/vpp_tokens/%d/teams // +//////////////////////////////////////////////////// + +type patchVPPTokensTeamsRequest struct { + ID uint `url:"id"` + TeamIDs []uint `json:"teams"` +} + +type patchVPPTokensTeamsResponse struct { + Token *fleet.VPPTokenDB `json:"token,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r patchVPPTokensTeamsResponse) error() error { return r.Err } + +func patchVPPTokensTeams(ctx context.Context, request any, svc fleet.Service) (errorer, error) { + req := request.(*patchVPPTokensTeamsRequest) + + tok, err := svc.UpdateVPPTokenTeams(ctx, req.ID, req.TeamIDs) + if err != nil { + return patchVPPTokensTeamsResponse{Err: err}, nil + } + return patchVPPTokensTeamsResponse{Token: tok}, nil +} + +func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +///////////////////////////////////////// +// GET /api/_version_/fleet/vpp_tokens // +///////////////////////////////////////// + +type getVPPTokensRequest struct{} + +type getVPPTokensResponse struct { + Tokens []*fleet.VPPTokenDB `json:"vpp_tokens"` + Err error `json:"error,omitempty"` +} + +func (r getVPPTokensResponse) error() error { return r.Err } + +func getVPPTokens(ctx context.Context, request any, svc fleet.Service) (errorer, error) { + tokens, err := svc.GetVPPTokens(ctx) + if err != nil { + return getVPPTokensResponse{Err: err}, nil + } + + if tokens == nil { + tokens = []*fleet.VPPTokenDB{} + } + + return getVPPTokensResponse{Tokens: tokens}, nil +} + +func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +/////////////////////////////////////////////// +// DELETE /api/_version_/fleet/vpp_tokens/%d // +/////////////////////////////////////////////// + +type deleteVPPTokenRequest struct { + ID uint `url:"id"` +} + +type deleteVPPTokenResponse struct { + Err error `json:"error,omitempty"` +} + +func (r deleteVPPTokenResponse) error() error { return r.Err } + +func (r deleteVPPTokenResponse) Status() int { return http.StatusNoContent } + +func deleteVPPToken(ctx context.Context, request any, svc fleet.Service) (errorer, error) { + req := request.(*deleteVPPTokenRequest) + + err := svc.DeleteVPPToken(ctx, req.ID) + if err != nil { + return deleteVPPTokenResponse{Err: err}, nil + } + + return deleteVPPTokenResponse{}, nil +} + +func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return fleet.ErrMissingLicense +} From bf8f84a39929ae5817c9071967a6720995b34ae3 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Tue, 3 Sep 2024 14:46:11 +0100 Subject: [PATCH 02/25] show abm and vpp sections when mdm is disabled (#21752) relates to #21716 This shows the abm and vpp sections on the mdm settings page when mdm is not enabled. ![image](https://github.com/user-attachments/assets/357a0f5a-6e99-4a85-aed5-e5e4c18a5b20) --- .../IntegrationsPage/cards/MdmSettings/MdmSettings.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx index ff3cf5b539..b6e0f02795 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx @@ -27,6 +27,8 @@ interface IMdmSettingsProps { const MdmSettings = ({ router }: IMdmSettingsProps) => { const { isPremiumTier, config } = useContext(AppContext); + const isMdmEnabled = !!config?.mdm.enabled_and_configured; + // Currently the status of this API call is what determines various UI states on // this page. Because of this we will not render any of this components UI until this API // call has completed. @@ -48,7 +50,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { // we're fetching and setting the config, but for now we'll just assume that any 400 response // means that MDM is not enabled and we'll show the "Turn on MDM" button. staleTime: 5000, - enabled: !!config?.mdm.enabled_and_configured, + enabled: isMdmEnabled, } ); @@ -63,7 +65,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { { ...DEFAULT_USE_QUERY_OPTIONS, retry: false, - enabled: isPremiumTier && !!config?.mdm.enabled_and_configured, + enabled: isPremiumTier && isMdmEnabled, } ); @@ -80,7 +82,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { { ...DEFAULT_USE_QUERY_OPTIONS, retry: false, - enabled: isPremiumTier && !!config?.mdm.enabled_and_configured, + enabled: isPremiumTier && isMdmEnabled, } ); @@ -104,7 +106,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { // we use this to determine if we have all the data we need to render the UI. // Notice that we do not need VPP or EULA data to render this page. - const hasAllData = !!APNSInfo; + const hasAllData = !isMdmEnabled || !!APNSInfo; return (
From 1b06b050d7a44d240afdbe6c0bf90dd21cb68813 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 3 Sep 2024 09:07:16 -0500 Subject: [PATCH 03/25] Fix issues with coverage uploads (#21736) #21707 --- .github/workflows/test-fleetd-chrome.yml | 3 +- .github/workflows/test-go.yaml | 42 ++++++++++++++++-------- .github/workflows/test-js.yml | 3 +- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test-fleetd-chrome.yml b/.github/workflows/test-fleetd-chrome.yml index 47ba496ebb..8cbb0125f9 100644 --- a/.github/workflows/test-fleetd-chrome.yml +++ b/.github/workflows/test-fleetd-chrome.yml @@ -66,7 +66,8 @@ jobs: npm test - name: Upload to Codecov - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: + token: ${{ secrets.CODECOV_TOKEN }} directory: ./ee/fleetd-chrome/coverage flags: fleetd-chrome diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index 9feba50d8e..d7b93c5d28 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -131,13 +131,17 @@ jobs: NETWORK_TEST_GITHUB_TOKEN=${{ secrets.FLEET_RELEASE_GITHUB_PAT }} \ make test-go 2>&1 | tee /tmp/gotest.log - # note: it's fine to upload multiple reports (one per matrix combination) - # for the same run, see https://docs.codecov.com/docs/merging-reports - - name: Upload to Codecov - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 + - name: Create mysql identifier without colon + if: always() + run: | + echo "MATRIX_MYSQL_ID=$(echo ${{ matrix.mysql }} | tr -d ':')" >> $GITHUB_ENV + + - name: Save coverage + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: - files: coverage.txt - flags: backend + name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-coverage + path: ./coverage.txt + if-no-files-found: error - name: Generate summary of errors if: failure() @@ -167,14 +171,9 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }} SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK - - name: Create mysql identifier without colon - if: always() - run: | - echo "MATRIX_MYSQL_ID=$(echo ${{ matrix.mysql }} | tr -d ':')" >> $GITHUB_ENV - - name: Upload test log if: always() - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-test-log path: /tmp/gotest.log @@ -182,7 +181,24 @@ jobs: - name: Upload summary test log if: always() - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-summary-test-log path: /tmp/summary.txt + + # We upload all backend coverage in one step so that we're less like to end up in a situation with a partial coverage report. + upload-coverage: + needs: [test-go] + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Download artifacts + uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.6 + with: + pattern: '*-coverage' + - name: Upload to Codecov + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: backend diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 9d63523737..15b4fd05ce 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -69,8 +69,9 @@ jobs: yarn test:ci - name: Upload to Codecov - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: + token: ${{ secrets.CODECOV_TOKEN }} flags: frontend lint-js: From f6165a220a29e43ee9d2ba23c06cb327e84b5e27 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 3 Sep 2024 11:40:17 -0300 Subject: [PATCH 04/25] fix: properly catch and log APNs errors (#21753) found reproducing other issues: 1. In the APNs cron, the logger wasn't good enough to print an slice and the log message was "unsupported type" 2. `APNSDeliveryError` _always_ had `Err` set to nil, while we were catching those errors, it was impossible to see the cause in the logs (always printed err=nil) # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Manual QA for all new/changed functionality --- changes/apns-errors | 1 + server/mdm/apple/commander.go | 41 ++++++++++++++++++++----- server/mdm/apple/commander_test.go | 49 ++++++++++++++++++++++++++++++ server/service/apple_mdm.go | 2 +- server/service/mdm.go | 5 +-- 5 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 changes/apns-errors diff --git a/changes/apns-errors b/changes/apns-errors new file mode 100644 index 0000000000..6de48617a1 --- /dev/null +++ b/changes/apns-errors @@ -0,0 +1 @@ +* Fixed logic to properly catch and log APNs errors. diff --git a/server/mdm/apple/commander.go b/server/mdm/apple/commander.go index 68bd90c82e..6a098a5284 100644 --- a/server/mdm/apple/commander.go +++ b/server/mdm/apple/commander.go @@ -5,6 +5,8 @@ import ( "encoding/base64" "fmt" "net/http" + "sort" + "strings" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -383,14 +385,15 @@ func (svc *MDMAppleCommander) SendNotifications(ctx context.Context, hostUUIDs [ // Even if we didn't get an error, some of the APNs // responses might have failed, signal that to the caller. - var failed []string + failed := map[string]error{} for uuid, response := range apnsResponses { if response.Err != nil { - failed = append(failed, uuid) + failed[uuid] = response.Err } } + if len(failed) > 0 { - return &APNSDeliveryError{FailedUUIDs: failed, Err: err} + return &APNSDeliveryError{errorsByUUID: failed} } return nil @@ -399,14 +402,38 @@ func (svc *MDMAppleCommander) SendNotifications(ctx context.Context, hostUUIDs [ // APNSDeliveryError records an error and the associated host UUIDs in which it // occurred. type APNSDeliveryError struct { - FailedUUIDs []string - Err error + errorsByUUID map[string]error } func (e *APNSDeliveryError) Error() string { - return fmt.Sprintf("APNS delivery failed with: %s, for UUIDs: %v", e.Err, e.FailedUUIDs) + var uuids []string + for uuid := range e.errorsByUUID { + uuids = append(uuids, uuid) + } + + // sort UUIDs alphabetically for deterministic output + sort.Strings(uuids) + + var errStrings []string + for _, uuid := range uuids { + errStrings = append(errStrings, fmt.Sprintf("UUID: %s, Error: %v", uuid, e.errorsByUUID[uuid])) + } + + return fmt.Sprintf( + "APNS delivery failed with the following errors:\n%s", + strings.Join(errStrings, "\n"), + ) } -func (e *APNSDeliveryError) Unwrap() error { return e.Err } +func (e *APNSDeliveryError) FailedUUIDs() []string { + var uuids []string + for uuid := range e.errorsByUUID { + uuids = append(uuids, uuid) + } + + // sort UUIDs alphabetically for deterministic output + sort.Strings(uuids) + return uuids +} func (e *APNSDeliveryError) StatusCode() int { return http.StatusBadGateway } diff --git a/server/mdm/apple/commander_test.go b/server/mdm/apple/commander_test.go index 5978944b52..0d21c66ab5 100644 --- a/server/mdm/apple/commander_test.go +++ b/server/mdm/apple/commander_test.go @@ -3,7 +3,9 @@ package apple_mdm import ( "context" "crypto/tls" + "errors" "fmt" + "net/http" "os" "testing" @@ -200,3 +202,50 @@ func mobileconfigForTest(name, identifier string) []byte { `, name, identifier, uuid.New().String())) } + +func TestAPNSDeliveryError(t *testing.T) { + tests := []struct { + name string + errorsByUUID map[string]error + expectedError string + expectedFailedUUIDs []string + expectedStatusCode int + }{ + { + name: "single error", + errorsByUUID: map[string]error{ + "uuid1": errors.New("network error"), + }, + expectedError: `APNS delivery failed with the following errors: +UUID: uuid1, Error: network error`, + expectedFailedUUIDs: []string{"uuid1"}, + expectedStatusCode: http.StatusBadGateway, + }, + { + name: "multiple errors, sorted", + errorsByUUID: map[string]error{ + "uuid3": errors.New("timeout error"), + "uuid1": errors.New("network error"), + "uuid2": errors.New("certificate error"), + }, + expectedError: `APNS delivery failed with the following errors: +UUID: uuid1, Error: network error +UUID: uuid2, Error: certificate error +UUID: uuid3, Error: timeout error`, + expectedFailedUUIDs: []string{"uuid1", "uuid2", "uuid3"}, + expectedStatusCode: http.StatusBadGateway, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + apnsErr := &APNSDeliveryError{ + errorsByUUID: tt.errorsByUUID, + } + + require.Equal(t, tt.expectedError, apnsErr.Error()) + require.Equal(t, tt.expectedFailedUUIDs, apnsErr.FailedUUIDs()) + require.Equal(t, tt.expectedStatusCode, apnsErr.StatusCode()) + }) + } +} diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 1131a84b2d..f870d33b71 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -3149,7 +3149,7 @@ func SendPushesToPendingDevices( if err := commander.SendNotifications(ctx, uuids); err != nil { var apnsErr *apple_mdm.APNSDeliveryError if errors.As(err, &apnsErr) { - level.Info(logger).Log("msg", "failed to send APNs notification to some hosts", "host_uuids", apnsErr.FailedUUIDs) + level.Info(logger).Log("msg", "failed to send APNs notification to some hosts", "error", apnsErr.Error()) return nil } diff --git a/server/service/mdm.go b/server/service/mdm.go index e50d283ee6..294d503d81 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -566,13 +566,14 @@ func (svc *Service) enqueueAppleMDMCommand(ctx context.Context, rawXMLCmd []byte var apnsErr *apple_mdm.APNSDeliveryError var mysqlErr *mysql.MySQLError if errors.As(err, &apnsErr) { - if len(apnsErr.FailedUUIDs) < len(deviceIDs) { + failedUUIDs := apnsErr.FailedUUIDs() + if len(failedUUIDs) < len(deviceIDs) { // some hosts properly received the command, so return success, with the list // of failed uuids. return &fleet.CommandEnqueueResult{ CommandUUID: cmd.CommandUUID, RequestType: cmd.Command.RequestType, - FailedUUIDs: apnsErr.FailedUUIDs, + FailedUUIDs: failedUUIDs, }, nil } // push failed for all hosts From f51fe63259a03d29f4d2548012372e5ca4372ffb Mon Sep 17 00:00:00 2001 From: Mike Thomas <78363703+mike-j-thomas@users.noreply.github.com> Date: Tue, 3 Sep 2024 08:29:36 -0700 Subject: [PATCH 05/25] Update mdm-migration.md (#21534) - Updated the intro to this guide. - I added a note directing users to "Seamless MDM migration." - Updated the requirements section. --------- Co-authored-by: JD --- articles/mdm-migration.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/articles/mdm-migration.md b/articles/mdm-migration.md index 76c6254125..5eb9e8473d 100644 --- a/articles/mdm-migration.md +++ b/articles/mdm-migration.md @@ -1,11 +1,15 @@ # MDM migration -This section provides instructions for migrating your hosts away from your old MDM solution to Fleet. +This guide provides instructions for migrating devices from your current MDM solution to Fleet. + +> For seamless MDM migration, [view this guide](https://fleetdm.com/guides/seamless-mdm-migration). ## Requirements -1. A [deployed Fleet instance](https://fleetdm.com/docs/deploy/deploy-fleet) -2. [Fleet connected to Apple](https://fleetdm.com/guides/macos-mdm-setup) + +- A [deployed Fleet instance](https://fleetdm.com/docs/deploy/deploy-fleet) +- Fleet is connected to Apple Push Notification service (APNs) and Apple Business Manager (ABM). [See macOS MDM setup](https://fleetdm.com/guides/macos-mdm-setup) + ## Migrate manually enrolled hosts From 004ed9fc9abe4840c85963bcfacfe91a8706411a Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Tue, 3 Sep 2024 10:52:38 -0500 Subject: [PATCH 06/25] Update KPI script (#21732) --- website/scripts/get-bug-and-pr-report.js | 57 ++++++++++++------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/website/scripts/get-bug-and-pr-report.js b/website/scripts/get-bug-and-pr-report.js index 5894be6ecb..06791bf10a 100644 --- a/website/scripts/get-bug-and-pr-report.js +++ b/website/scripts/get-bug-and-pr-report.js @@ -33,8 +33,10 @@ module.exports = { let daysSinceReleasedBugsWereOpened = []; let allBugsWithUnreleasedLabel = []; let allBugsWithReleasedLabel = []; + let allBugs32DaysOrOlder = []; let allBugsCreatedInPastWeek = []; let allBugsClosedInPastWeek = []; + let allBugsReportedByCustomersInPastWeek = []; let daysSincePullRequestsWereOpened = []; let daysSinceContributorPullRequestsWereOpened = []; let commitToMergeTimesInDays = []; @@ -44,7 +46,7 @@ module.exports = { let allNonPublicOpenPrs = []; let nonPublicPrsClosedInThePastThreeWeeks = []; - // Product group KPIS + // Endpoint operations let allBugsCreatedInPastWeekEndpointOps = []; @@ -103,8 +105,16 @@ module.exports = { let timeOpenInMS = Math.abs(todaysDate - issueOpenedOn); // Convert the miliseconds to days and add the value to the daysSinceBugsWereOpened array let timeOpenInDays = timeOpenInMS / ONE_DAY_IN_MILLISECONDS; + if (timeOpenInDays >= 32) { + allBugs32DaysOrOlder.push(issue); + } if (timeOpenInDays <= 7) { + // All bugs in past week allBugsCreatedInPastWeek.push(issue); + // Customer-reported bugs + if (issue.labels.some(label => label.name.indexOf('customer-') >= 0)) { + allBugsReportedByCustomersInPastWeek.push(issue); + } // Get Endpoint Ops KPIs if (issue.labels.some(label => label.name === '#g-endpoint-ops')) { allBugsCreatedInPastWeekEndpointOps.push(issue); @@ -132,6 +142,7 @@ module.exports = { } } } + daysSinceBugsWereOpened.push(timeOpenInDays); // Send to released or unreleased bugs array if (issue.labels.some(label => label.name === '~unreleased bug')) { @@ -316,8 +327,8 @@ module.exports = { // async()=>{ - // Fetch confidential and classified PRs (current open, and recent closed) - for (let repoName of ['classified', 'confidential']) { + // Fetch confidential PRs (current open, and recent closed) + for (let repoName of ['confidential']) { // [?] https://docs.github.com/en/free-pro-team@latest/rest/pulls/pulls#list-pull-requests let openPrs = await sails.helpers.http.get(`https://api.github.com/repos/fleetdm/${encodeURIComponent(repoName)}/pulls`, { state: 'open', @@ -380,25 +391,12 @@ module.exports = { // NOTE: If order of the KPI sheets columns changes, the order values are pushed into this array needs to change, as well. kpiResults.push( averageDaysContributorPullRequestsAreOpenFor, - daysSinceContributorPullRequestsWereOpened.length, - averageDaysPullRequestsAreOpenFor, - daysSincePullRequestsWereOpened.length, + allBugs32DaysOrOlder.length, + allBugsReportedByCustomersInPastWeek.length, averageNumberOfDaysReleasedBugsAreOpenFor, averageNumberOfDaysUnreleasedBugsAreOpenFor, - allBugsClosedInPastWeek.length, - averageNumberOfDaysBugsAreOpenFor, allBugsCreatedInPastWeek.length, - allBugsCreatedInPastWeekEndpointOps.length, - allBugsCreatedInPastWeekEndpointOpsCustomerImpacting.length, - allBugsCreatedInPastWeekEndpointOpsReleased.length, - allBugsCreatedInPastWeekEndpointOpsUnreleased.length, - allBugsCreatedInPastWeekMobileDeviceManagement.length, - allBugsCreatedInPastWeekMobileDeviceManagementCustomerImpacting.length, - allBugsCreatedInPastWeekMobileDeviceManagementReleased.length, - allBugsCreatedInPastWeekMobileDeviceManagementUnreleased.length, - daysSinceBugsWereOpened.length, - allBugsWithReleasedLabel.length, - allBugsWithUnreleasedLabel.length); + allBugsClosedInPastWeek.length,); // Log the results sails.log(` @@ -407,17 +405,19 @@ module.exports = { --------------------------- ${kpiResults.join(',')} - Note: Copy the values above, then in Google sheets paste them into a cell and select "Split text to columns" to paste the values into separate cells. + Note: Copy the values above, then paste into Google KPI sheet and select "Split text to columns" to split the values into separate columns. Pull requests: --------------------------- Average open time (no bots, no handbook, no ceo): ${averageDaysContributorPullRequestsAreOpenFor} days. + Number of open pull requests in the fleetdm/fleet Github repo (no bots, no handbook, no ceo): ${daysSinceContributorPullRequestsWereOpened.length} Average open time (all PRs): ${averageDaysPullRequestsAreOpenFor} days. + Number of open pull requests in the fleetdm/fleet Github repo: ${daysSincePullRequestsWereOpened.length} - Bugs (part 1): + Bugs: --------------------------- Average open time (released bugs): ${averageNumberOfDaysReleasedBugsAreOpenFor} days. @@ -429,6 +429,12 @@ module.exports = { Number of issues with the "bug" label opened in the past week: ${allBugsCreatedInPastWeek.length} + Number of open issues with the "bug" label in fleetdm/fleet: ${daysSinceBugsWereOpened.length} + + Number of open issues with the "~released bug" label in fleetdm/fleet: ${allBugsWithReleasedLabel.length} + + Number of open issues with the "~unreleased bug" label in fleetdm/fleet: ${allBugsWithUnreleasedLabel.length} + Endpoint Operations: --------------------------- Number of issues with the "#g-endpoint-ops" and "bug" labels opened in the past week: ${allBugsCreatedInPastWeekEndpointOps.length} @@ -449,17 +455,10 @@ module.exports = { Number of issues with the "#g-mdm", "bug", and "~unreleased bug" labels opened in the past week: ${allBugsCreatedInPastWeekMobileDeviceManagementUnreleased.length} - Bugs (part 2): - --------------------------- - Number of open issues with the "bug" label in fleetdm/fleet: ${daysSinceBugsWereOpened.length} - - Number of open issues with the "~released bug" label in fleetdm/fleet: ${allBugsWithReleasedLabel.length} - - Number of open issues with the "~unreleased bug" label in fleetdm/fleet: ${allBugsWithUnreleasedLabel.length} - Pull requests requiring CEO review --------------------------------------- Number of open ~ceo pull requests in the fleetdm Github org: ${ceoDependentOpenPrs.length} + Average open time (~ceo PRs): ${Math.round(ceoDependentPrOpenTime*100)/100} days. `); From 6de73537a1ac4308ba9ca451828c96300d1964b8 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Tue, 3 Sep 2024 17:04:55 +0100 Subject: [PATCH 07/25] show correct error message when the VPP token is invalid (#21744) relates to #21725 shows the correct UI error when a user tries to upload an invalid token - [x] Added/updated tests --- .../MdmSettings/VppPage/components/AddVppModal/helpers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/AddVppModal/helpers.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/AddVppModal/helpers.tsx index 77dc842782..7716c6de6c 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/AddVppModal/helpers.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/AddVppModal/helpers.tsx @@ -20,7 +20,7 @@ export const getErrorMessage = (err: unknown) => { reasonIncludes: "Duplicate entry", }); const invalidTokenReason = getErrorReason(err, { - reasonIncludes: "invalid", + reasonIncludes: "Invalid token", }); if (duplicateEntryReason) { @@ -28,7 +28,7 @@ export const getErrorMessage = (err: unknown) => { } if (invalidTokenReason) { - return "Invalid token. Please provide a valid token from Apple Business Manager."; + return invalidTokenReason; } return DEFAULT_ERROR_MESSAGE; From d44d6755683cb911733c4c1407dda18226c99c97 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:32:18 -0400 Subject: [PATCH 08/25] Fix API docs bug (#21263) Broken label's don't have an `id` (they've been deleted): https://github.com/fleetdm/fleet/pull/21162#discussion_r1713715452 --- docs/REST API/rest-api.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 607ac2f28d..a5e1ba6c84 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -5424,7 +5424,6 @@ List all configuration profiles for macOS and Windows hosts enrolled to Fleet's { "name": "Label name 2", "broken": true, - "id": 2 }, { "name": "Label name 3", From aa952962b51e5dcbd639175788dac74a16059269 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:32:39 -0400 Subject: [PATCH 09/25] API design: Fleet MDM features depend on MDM name (#21279) API design for this bug: #18977 Documentation story is here: - #20373 --- docs/REST API/rest-api.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index a5e1ba6c84..c721ea1a92 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -2489,6 +2489,7 @@ the `software` table. | mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | | mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | | mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. | +| connected_to_fleet | boolean | query | Filter hosts that are talking to this Fleet server for MDM features. In rare cases, hosts can be enrolled to one Fleet server but talk to a different Fleet server for MDM features. In this case, the value would be `false`. Always `false` for Linux hosts. | | macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** | | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | @@ -3099,10 +3100,11 @@ Returns the information of the specified host. "timezone": "America/New_York" }, "mdm": { - "encryption_key_available": false, - "enrollment_status": null, - "name": "", - "server_url": null, + "encryption_key_available": true, + "enrollment_status": "On (manual)", + "name": "Fleet", + "connected_to_fleet": true, + "server_url": "https://acme.com/mdm/apple/mdm", "device_status": "unlocked", "pending_action": "", "macos_settings": { @@ -3518,10 +3520,11 @@ This is the API route used by the **My device** page in Fleet desktop to display } ], "mdm": { - "encryption_key_available": false, - "enrollment_status": null, - "name": "", - "server_url": null, + "encryption_key_available": true, + "enrollment_status": "On (manual)", + "name": "Fleet", + "connected_to_fleet": true, + "server_url": "https://acme.com/mdm/apple/mdm", "macos_settings": { "disk_encryption": null, "action_required": null From 904234251f84806bdf185dfae139dcc7c5f73e2d Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:33:09 -0400 Subject: [PATCH 10/25] App deployment API endpoints are experimental (#21615) App deployment features are experimental features: https://fleetdm.com/handbook/company/product-groups#experimental-features --- docs/REST API/rest-api.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index c721ea1a92..06fcee7e9a 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -8992,6 +8992,8 @@ OS vulnerability data is currently available for Windows and macOS. For other pl ### Add package +> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. + _Available in Fleet Premium._ Add a package (.pkg, .msi, .exe, .deb) to install on macOS, Windows, or Linux (Ubuntu) hosts. @@ -9138,6 +9140,8 @@ Add App Store (VPP) app purchased in Apple Business Manager. ### Download package +> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. + _Available in Fleet Premium._ `GET /api/v1/fleet/software/titles/:software_title_id/package?alt=media` @@ -9193,6 +9197,8 @@ Install software (package or App Store app) on a macOS, iOS, iPadOS, Windows, or ### Get package install result +> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. + _Available in Fleet Premium._ `GET /api/v1/fleet/software/install/results/:install_uuid` From 5b1a603d3b52d6570e2c2ea0cff2c27b969c16a5 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 3 Sep 2024 13:23:44 -0400 Subject: [PATCH 11/25] MABM bugfix: fix the expected format of the migrated VPP token (#21761) --- cmd/fleet/serve.go | 6 ++++ server/fleet/mdm.go | 43 ------------------------ server/test/mdm.go | 8 +++++ server/worker/db_migrations.go | 51 ++++++++++++++++++++++++++--- server/worker/db_migrations_test.go | 17 +++++----- 5 files changed, 69 insertions(+), 56 deletions(-) diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 67ce51f7d0..6c363965a8 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -633,6 +633,12 @@ the way that the Fleet server works. appCfg.MDM.AppleBMEnabledAndConfigured = count > 0 } } + if appCfg.MDM.EnabledAndConfigured { + level.Info(logger).Log("msg", "Apple MDM enabled") + } + if appCfg.MDM.AppleBMEnabledAndConfigured { + level.Info(logger).Log("msg", "Apple Business Manager enabled") + } // register the Microsoft MDM services var ( diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 44b422f913..69df8e0323 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -3,7 +3,6 @@ package fleet import ( "bytes" "context" - "encoding/base64" "encoding/json" "fmt" "net/url" @@ -791,48 +790,6 @@ type TeamTuple struct { Name string `json:"name"` } -// ExtractToken extracts the metadata from the token as stored in the database, -// and returns the raw token that can be used directly with Apple's VPP API. If -// while extracting the token it notices that the metadata has changed, it will -// update t and return true as second return value, indicating that it changed -// and should be saved. -func (t *VPPTokenDB) ExtractToken() (rawAppleToken string, didUpdateMetadata bool, err error) { - var vppTokenData VPPTokenData - if err := json.Unmarshal([]byte(t.Token), &vppTokenData); err != nil { - return "", false, fmt.Errorf("unmarshaling VPP token data: %w", err) - } - - vppTokenRawBytes, err := base64.StdEncoding.DecodeString(vppTokenData.Token) - if err != nil { - return "", false, fmt.Errorf("decoding raw vpp token data: %w", err) - } - - var vppTokenRaw VPPTokenRaw - if err := json.Unmarshal(vppTokenRawBytes, &vppTokenRaw); err != nil { - return "", false, fmt.Errorf("unmarshaling raw vpp token data: %w", err) - } - - exp, err := time.Parse("2006-01-02T15:04:05Z0700", vppTokenRaw.ExpDate) - if err != nil { - return "", false, fmt.Errorf("parsing vpp token expiration date: %w", err) - } - - if vppTokenData.Location != t.Location { - t.Location = vppTokenData.Location - didUpdateMetadata = true - } - if vppTokenRaw.OrgName != t.OrgName { - t.OrgName = vppTokenRaw.OrgName - didUpdateMetadata = true - } - if !exp.Equal(t.RenewDate) { - t.RenewDate = exp.UTC() - didUpdateMetadata = true - } - - return vppTokenRaw.Token, didUpdateMetadata, nil -} - type NullTeamType string const ( diff --git a/server/test/mdm.go b/server/test/mdm.go index 0bcd17d258..df59cb2e6c 100644 --- a/server/test/mdm.go +++ b/server/test/mdm.go @@ -36,6 +36,14 @@ func CreateVPPTokenEncoded(expiration time.Time, orgName, location string) ([]by if err != nil { return nil, err } + return []byte(dataToken.Token), nil +} + +func CreateVPPTokenEncodedAfterMigration(expiration time.Time, orgName, location string) ([]byte, error) { + dataToken, err := CreateVPPTokenData(expiration, orgName, location) + if err != nil { + return nil, err + } dataTokenJson, err := json.Marshal(dataToken) if err != nil { diff --git a/server/worker/db_migrations.go b/server/worker/db_migrations.go index be5bc31762..88d0839d19 100644 --- a/server/worker/db_migrations.go +++ b/server/worker/db_migrations.go @@ -2,7 +2,10 @@ package worker import ( "context" + "encoding/base64" "encoding/json" + "fmt" + "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -70,7 +73,7 @@ func (m *DBMigration) migrateVPPToken(ctx context.Context) error { return ctxerr.Wrap(ctx, err, "get VPP token to migrate") } - rawToken, didUpdate, err := tok.ExtractToken() + tokenData, didUpdate, err := extractVPPTokenFromMigration(tok) if err != nil { return ctxerr.Wrap(ctx, err, "extract VPP token metadata") } @@ -81,7 +84,47 @@ func (m *DBMigration) migrateVPPToken(ctx context.Context) error { m.Log.Log("info", "VPP token metadata was not updated") } - tokenData := fleet.VPPTokenData{Token: rawToken, Location: tok.Location} - _, err = m.Datastore.UpdateVPPToken(ctx, tok.ID, &tokenData) - return ctxerr.Wrap(ctx, err, "update VPP token") + if _, err := m.Datastore.UpdateVPPToken(ctx, tok.ID, tokenData); err != nil { + return ctxerr.Wrap(ctx, err, "update VPP token") + } + // the migated token should target "All teams" + _, err = m.Datastore.UpdateVPPTokenTeams(ctx, tok.ID, []uint{}) + return ctxerr.Wrap(ctx, err, "update VPP token teams") +} + +func extractVPPTokenFromMigration(migratedToken *fleet.VPPTokenDB) (tokData *fleet.VPPTokenData, didUpdateMetadata bool, err error) { + var vppTokenData fleet.VPPTokenData + if err := json.Unmarshal([]byte(migratedToken.Token), &vppTokenData); err != nil { + return nil, false, fmt.Errorf("unmarshaling VPP token data: %w", err) + } + + vppTokenRawBytes, err := base64.StdEncoding.DecodeString(vppTokenData.Token) + if err != nil { + return nil, false, fmt.Errorf("decoding raw vpp token data: %w", err) + } + + var vppTokenRaw fleet.VPPTokenRaw + if err := json.Unmarshal(vppTokenRawBytes, &vppTokenRaw); err != nil { + return nil, false, fmt.Errorf("unmarshaling raw vpp token data: %w", err) + } + + exp, err := time.Parse("2006-01-02T15:04:05Z0700", vppTokenRaw.ExpDate) + if err != nil { + return nil, false, fmt.Errorf("parsing vpp token expiration date: %w", err) + } + + if vppTokenData.Location != migratedToken.Location { + migratedToken.Location = vppTokenData.Location + didUpdateMetadata = true + } + if vppTokenRaw.OrgName != migratedToken.OrgName { + migratedToken.OrgName = vppTokenRaw.OrgName + didUpdateMetadata = true + } + if !exp.Equal(migratedToken.RenewDate) { + migratedToken.RenewDate = exp.UTC() + didUpdateMetadata = true + } + + return &vppTokenData, didUpdateMetadata, nil } diff --git a/server/worker/db_migrations_test.go b/server/worker/db_migrations_test.go index 8faca12acd..1aeeb697e9 100644 --- a/server/worker/db_migrations_test.go +++ b/server/worker/db_migrations_test.go @@ -12,12 +12,11 @@ import ( "github.com/fleetdm/fleet/v4/server/test" kitlog "github.com/go-kit/log" "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDBMigrationsVPPToken(t *testing.T) { - // FIXME - t.Skip() ctx := context.Background() ds := mysql.CreateMySQLDS(t) @@ -38,7 +37,7 @@ func TestDBMigrationsVPPToken(t *testing.T) { // create the migrated token and enqueue the job expDate := time.Date(2024, 8, 27, 0, 0, 0, 0, time.UTC) - tok, err := test.CreateVPPTokenEncoded(expDate, "test-org", "test-loc") + tok, err := test.CreateVPPTokenEncodedAfterMigration(expDate, "test-org", "test-loc") require.NoError(t, err) encTok, err := mysql.EncryptWithPrivateKey(t, ds, tok) require.NoError(t, err) @@ -49,12 +48,10 @@ INSERT INTO vpp_tokens organization_name, location, renew_at, - token, - team_id, - null_team_type + token ) VALUES - ('', '', DATE('2000-01-01'), ?, NULL, 'allteams') + ('', '', DATE('2000-01-01'), ?) ` const insJob = ` @@ -93,7 +90,9 @@ VALUES (?, ?, ?, '', ?, ?, ?) // nothing more to run jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job require.NoError(t, err) - require.Empty(t, jobs) + if !assert.Empty(t, jobs) { + t.Logf(">>> %#+v", jobs[0]) + } // token should've been updated vppTok, err := ds.GetVPPTokenByLocation(ctx, "test-loc") @@ -101,7 +100,7 @@ VALUES (?, ?, ?, '', ?, ?, ?) require.Equal(t, "test-org", vppTok.OrgName) require.Equal(t, "test-loc", vppTok.Location) require.Equal(t, expDate, vppTok.RenewDate) - require.Equal(t, string(tok), vppTok.Token) + require.Contains(t, string(tok), `"token":"`+vppTok.Token+`"`) // the DB-stored token is the "token" JSON field in the raw tok require.NotNil(t, vppTok.Teams) require.Len(t, vppTok.Teams, 0) From 80e37fb787e8d00de0a953f57db30803205b08ee Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:24:24 -0400 Subject: [PATCH 12/25] Update cryptoinfo.yml (#21762) --- schema/osquery_fleet_schema.json | 2 +- schema/tables/cryptoinfo.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json index ed0f676276..097534dbd1 100644 --- a/schema/osquery_fleet_schema.json +++ b/schema/osquery_fleet_schema.json @@ -5349,7 +5349,7 @@ }, { "name": "cryptoinfo", - "description": "Get info about the a certificate on the host.", + "description": "Get info about a certificate on the host.", "evented": false, "notes": "This table is not a core osquery table. It is included as part of fleetd, the osquery manager from Fleet. Code based on work by [Kolide](https://github.com/kolide/launcher).", "platforms": [ diff --git a/schema/tables/cryptoinfo.yml b/schema/tables/cryptoinfo.yml index 4fd8d5da37..38055381b7 100644 --- a/schema/tables/cryptoinfo.yml +++ b/schema/tables/cryptoinfo.yml @@ -1,5 +1,5 @@ name: cryptoinfo -description: Get info about the a certificate on the host. +description: Get info about a certificate on the host. evented: false notes: This table is not a core osquery table. It is included as part of fleetd, the osquery manager from Fleet. Code based on work by [Kolide](https://github.com/kolide/launcher). platforms: From a8a739ab297965ce9b93fe6fd25438625e202e5e Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 3 Sep 2024 14:32:40 -0300 Subject: [PATCH 13/25] update copy for VPP team modal (#21766) unreleased spec change. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Manual QA for all new/changed functionality --- .../components/AppStoreVpp/AppStoreVpp.tsx | 11 +++++------ .../SoftwarePage/components/AppStoreVpp/_styles.scss | 1 + frontend/router/paths.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/pages/SoftwarePage/components/AppStoreVpp/AppStoreVpp.tsx b/frontend/pages/SoftwarePage/components/AppStoreVpp/AppStoreVpp.tsx index 2eb7597997..ffde9220a2 100644 --- a/frontend/pages/SoftwarePage/components/AppStoreVpp/AppStoreVpp.tsx +++ b/frontend/pages/SoftwarePage/components/AppStoreVpp/AppStoreVpp.tsx @@ -35,14 +35,14 @@ const EnableVppCard = () => {

- Volume Purchasing Program (VPP) isn't enabled + No Volume Purchasing Program (VPP) token assigned

- To add App Store apps, first add VPP. + To add App Store apps, assign a VPP token to this team.

@@ -57,9 +57,8 @@ const NoVppAppsCard = () => ( You don't have any App Store apps

- Add apps in{" "} - Apps - that are already added to this team are not listed. + You must purchase apps in ABM. App Store apps that are already added to + this team are not listed.

diff --git a/frontend/pages/SoftwarePage/components/AppStoreVpp/_styles.scss b/frontend/pages/SoftwarePage/components/AppStoreVpp/_styles.scss index e530588b7a..604750a258 100644 --- a/frontend/pages/SoftwarePage/components/AppStoreVpp/_styles.scss +++ b/frontend/pages/SoftwarePage/components/AppStoreVpp/_styles.scss @@ -53,6 +53,7 @@ &__no-software-description { margin: 0; color: $ui-fleet-black-75; + text-align: center; } &__error { diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index ca0d3683f5..3c22cd097a 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -42,7 +42,7 @@ export default { ADMIN_INTEGRATIONS_APPLE_BUSINESS_MANAGER: `${URL_PREFIX}/settings/integrations/mdm/abm`, ADMIN_INTEGRATIONS_AUTOMATIC_ENROLLMENT_WINDOWS: `${URL_PREFIX}/settings/integrations/automatic-enrollment/windows`, ADMIN_INTEGRATIONS_CALENDARS: `${URL_PREFIX}/settings/integrations/calendars`, - ADMIN_INTEGRATIONS_VPP: `${URL_PREFIX}/settings/integrations/vpp`, + ADMIN_INTEGRATIONS_VPP: `${URL_PREFIX}/settings/integrations/mdm/vpp`, ADMIN_INTEGRATIONS_VPP_SETUP: `${URL_PREFIX}/settings/integrations/vpp/setup`, ADMIN_TEAMS: `${URL_PREFIX}/settings/teams`, From be2d0a65567eb00ba5c89f6d455965e9a7bb8684 Mon Sep 17 00:00:00 2001 From: Allen Houchins <32207388+allenhouchins@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:38:27 -0700 Subject: [PATCH 14/25] added allenhouchins to name list (#21781) --- 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 3923a50a06..05067166ec 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 = { 'SFriendLee', 'ddribeiro', 'rebeccaui', + 'allenhouchins', ]; let GREEN_LABEL_COLOR = 'C2E0C6';// « Used in multiple places below. (FUTURE: Use the "+" prefix for this instead of color. 2022-05-05) From c0687573c67752725b5f2ff4bc475f99713e343e Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 3 Sep 2024 13:42:08 -0500 Subject: [PATCH 15/25] Added backend patterns.md (#21782) As discussed at backend sync https://us-65885.app.gong.io/call?id=8041045095900447703 --- server/docs/patterns.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 server/docs/patterns.md diff --git a/server/docs/patterns.md b/server/docs/patterns.md new file mode 100644 index 0000000000..49b97c6ac0 --- /dev/null +++ b/server/docs/patterns.md @@ -0,0 +1,26 @@ +# Backend patterns + +The backend software patterns that we follow in Fleet. + +> NOTE: There are always exceptions to the rules, but we try to follow these patterns as much as possible unless a specific use case calls +> for something else. These should be discussed within the team and documented before merging. + +## MySQL + +Use high precision for all time fields. Precise timestamps make sure that we can accurately track when records were created and updated, +keep records in order with a reliable sort, and speed up testing by not having to wait for the time to +update. [MySQL reference](https://dev.mysql.com/doc/refman/8.4/en/date-and-time-type-syntax.html). [Backend sync where discussed](https://us-65885.app.gong.io/call?id=8041045095900447703). +Example: + +```sql +CREATE TABLE `sample` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) +); +``` + +Do not use [goqu](https://github.com/doug-martin/goqu); use MySQL queries directly. Searching for, understanding, and debugging direct MySQL +queries is easier. If needing to modify an existing `goqu` query, try to rewrite it in +MySQL. [Backend sync where discussed](https://us-65885.app.gong.io/call?id=8041045095900447703). From bb77b9a5ff5f6117f14e2c970951a6f88b3f9500 Mon Sep 17 00:00:00 2001 From: Rachael Shaw Date: Tue, 3 Sep 2024 14:04:54 -0500 Subject: [PATCH 16/25] Handbook: Add note about image exports (#21772) Follow-up from discussion @ product design sync awhile back re: exporting any image more complex than an icon as PNG. (Complex SVGs can have issues like emojis not rendering and inconsistencies between browsers.) --- handbook/company/product-groups.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/handbook/company/product-groups.md b/handbook/company/product-groups.md index fa8bf1bf31..3190df3fc8 100644 --- a/handbook/company/product-groups.md +++ b/handbook/company/product-groups.md @@ -584,6 +584,10 @@ Use the 🧩 ["Design System (current)"](https://www.figma.com/file/8oXlYXpgCV1S Use `---`, with color `$ui-fleet-black-50` as the default UI for empty columns. +**Images** + +Simple icons (aka any images used in the icon [design system component](https://www.figma.com/design/8oXlYXpgCV1Sn4ek7OworP/%F0%9F%A7%A9-Design-system-(current)?node-id=12-2&t=iO2vXbQ9Sc1kFVEJ-1)) are exported as SVGs. All other images are exported as PNGs, following the [Fleet website image](https://github.com/fleetdm/fleet/tree/main/website/assets/images) naming conventions. + **Form behavior** Pressing the return or enter key with an open form will cause the form to be submitted. From 93b4f484570809f57c52218762e136feea0c0ba9 Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:06:12 -0500 Subject: [PATCH 17/25] Add context to Prioritize for next sprint ritual (#21737) --- .../business-operations/business-operations.rituals.yml | 2 +- handbook/customer-success/customer-success.rituals.yml | 2 +- handbook/demand/demand.rituals.yml | 9 ++++++++- .../digital-experience/digital-experience.rituals.yml | 6 +++--- handbook/sales/sales.rituals.yml | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/handbook/business-operations/business-operations.rituals.yml b/handbook/business-operations/business-operations.rituals.yml index e6df744a0a..fec5055898 100644 --- a/handbook/business-operations/business-operations.rituals.yml +++ b/handbook/business-operations/business-operations.rituals.yml @@ -52,7 +52,7 @@ task: "Prioritize for next sprint" # Title that will actually show in rituals table startedOn: "2023-08-09" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday frequency: "Triweekly" # must be supported by https://github.com/fleetdm/fleet/blob/dbbb501358e226fa3fdf48865175efe3334c826c/website/scripts/build-static-content.js - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)" moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table dri: "jostableford" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # Enables automation of GitHub issues diff --git a/handbook/customer-success/customer-success.rituals.yml b/handbook/customer-success/customer-success.rituals.yml index 017063ae96..2c493ccf74 100644 --- a/handbook/customer-success/customer-success.rituals.yml +++ b/handbook/customer-success/customer-success.rituals.yml @@ -2,7 +2,7 @@ task: "Prioritize for next sprint" # Title that will actually show in rituals table startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday frequency: "Triweekly" # must be supported by - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table dri: "zayhanlon" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # Enables automation of GitHub issues diff --git a/handbook/demand/demand.rituals.yml b/handbook/demand/demand.rituals.yml index f64b087da0..1594c4d510 100644 --- a/handbook/demand/demand.rituals.yml +++ b/handbook/demand/demand.rituals.yml @@ -9,7 +9,7 @@ task: "Prioritize for next sprint" # Title that will actually show in rituals table startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday frequency: "Triweekly" - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table dri: "mikermcneil" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # « Enable automation of GitHub issues @@ -22,6 +22,13 @@ description: "https://fleetdm.com/handbook/demand#settle-event-strategy" moreInfoUrl: "https://fleetdm.com/handbook/demand#settle-event-strategy" dri: "Drew-P-drawers" +- + task: "🫧 Pipeline sync" + startedOn: "2024-08-29" + frequency: "Weekly" + description: "Allign with CRO and AEs on pipeline processes and incoming leads" + moreInfoUrl: "" + dri: "Drew-P-drawers" - task: "Optimize ads" startedOn: "2024-02-26" diff --git a/handbook/digital-experience/digital-experience.rituals.yml b/handbook/digital-experience/digital-experience.rituals.yml index 5e2e58a922..9e1999d304 100644 --- a/handbook/digital-experience/digital-experience.rituals.yml +++ b/handbook/digital-experience/digital-experience.rituals.yml @@ -74,7 +74,7 @@ task: "Prioritize for next sprint" # Title that will actually show in rituals table startedOn: "2023-08-09" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday frequency: "Triweekly" # must be supported by https://github.com/fleetdm/fleet/blob/dbbb501358e226fa3fdf48865175efe3334c826c/website/scripts/build-static-content.js - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)" moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table dri: "sampfluger88" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # Enables automation of GitHub issues @@ -123,8 +123,8 @@ repo: "confidential" - task: "Process and backup Sid agenda" - startedOn: "2023-09-15" - frequency: "Weekly" + startedOn: "2023-09-25" + frequency: "Monthly" description: "Process and backup Sid agenda" moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#process-and-backup-e-group-agenda" dri: "SFriendLee" diff --git a/handbook/sales/sales.rituals.yml b/handbook/sales/sales.rituals.yml index 6b2fd8fd0d..601d659e3d 100644 --- a/handbook/sales/sales.rituals.yml +++ b/handbook/sales/sales.rituals.yml @@ -6,7 +6,7 @@ task: "Prioritize for next sprint" # Title that will actually show in rituals table startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday frequency: "Triweekly" # must be supported by - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table dri: "alexmitchelliii" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # Enables automation of GitHub issues From 6a0b0cc018d9b12ea9e8b1899d3ad23aa91633b0 Mon Sep 17 00:00:00 2001 From: Robert Fairburn <8029478+rfairburn@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:12:04 -0500 Subject: [PATCH 18/25] Update mdmproxy dockerfile to use go 1.22.6 (#21785) --- tools/mdm/migration/mdmproxy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/mdm/migration/mdmproxy/Dockerfile b/tools/mdm/migration/mdmproxy/Dockerfile index 5d3369304b..bfe5fb62f1 100644 --- a/tools/mdm/migration/mdmproxy/Dockerfile +++ b/tools/mdm/migration/mdmproxy/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.5-alpine3.20@sha256:8c9183f715b0b4eca05b8b3dbf59766aaedb41ec07477b132ee2891ac0110a07 +FROM golang:1.22.6-alpine3.20@sha256:1a478681b671001b7f029f94b5016aed984a23ad99c707f6a0ab6563860ae2f3 ARG TAG RUN apk update && apk add --no-cache git RUN git clone -b $TAG --depth=1 --no-tags --progress --no-recurse-submodules https://github.com/fleetdm/fleet.git && cd /go/fleet/tools/mdm/migration/mdmproxy && go build . From f5720d38a43a050bb6b3a83b5d238d841a897159 Mon Sep 17 00:00:00 2001 From: Rachael Shaw Date: Tue, 3 Sep 2024 14:40:58 -0500 Subject: [PATCH 19/25] Docs: Use consistent naming for parameter types (#21787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bool ➡️ boolean list ➡️ array --- docs/REST API/rest-api.md | 90 +++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 06fcee7e9a..c4ad4e3858 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -1090,7 +1090,7 @@ Modifies the Fleet's configuration with the supplied information. | integrations | object | body | Includes `jira`, `zendesk`, and `google_calendar` arrays. See [integrations](#integrations) for details. | | mdm | object | body | See [mdm](#mdm). | | features | object | body | See [features](#features). | -| scripts | list | body | A list of script files to add so they can be executed at a later time. | +| scripts | array | body | A list of script files to add so they can be executed at a later time. | | force | boolean | query | Whether to force-apply the agent options even if there are validation errors. | | dry_run | boolean | query | Whether to validate the configuration and return any validation errors **without** applying changes. | @@ -1518,10 +1518,10 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ----- | ---------------------------------------------------------------------------------------------- | -| host_status_webhook | list | See [`webhook_settings.host_status_webhook`](#webhook-settings-host-status-webhook). | -| failing_policies_webhook | list | See [`webhook_settings.failing_policies_webhook`](#webhook-settings-failing-policies-webhook). | -| vulnerabilities_webhook | list | See [`webhook_settings.vulnerabilities_webhook`](#webhook-settings-vulnerabilities-webhook). | -| activities_webhook | list | See [`webhook_settings.activities_webhook`](#webhook-settings-activities-webhook). | +| host_status_webhook | array | See [`webhook_settings.host_status_webhook`](#webhook-settings-host-status-webhook). | +| failing_policies_webhook | array | See [`webhook_settings.failing_policies_webhook`](#webhook-settings-failing-policies-webhook). | +| vulnerabilities_webhook | array | See [`webhook_settings.vulnerabilities_webhook`](#webhook-settings-vulnerabilities-webhook). | +| activities_webhook | array | See [`webhook_settings.activities_webhook`](#webhook-settings-activities-webhook). |
@@ -1614,9 +1614,9 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ----- | -------------------------------------------------------------------- | -| jira | list | See [`integrations.jira`](#integrations-jira). | -| zendesk | list | See [`integrations.zendesk`](#integrations-zendesk). | -| google_calendar | list | See [`integrations.google_calendar`](#integrations-google-calendar). | +| jira | array | See [`integrations.jira`](#integrations-jira). | +| zendesk | array | See [`integrations.zendesk`](#integrations-zendesk). | +| google_calendar | array | See [`integrations.google_calendar`](#integrations-google-calendar). | > Note that when making changes to the `integrations` object, all integrations must be provided (not just the one being modified). This is because the endpoint will consider missing integrations as deleted. @@ -1792,7 +1792,7 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| custom_settings | list | macOS hosts that belong to no team will have custom profiles applied. | +| custom_settings | array | macOS hosts that belong to no team will have custom profiles applied. |
@@ -1802,7 +1802,7 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| custom_settings | list | Windows hosts that belong to no team will have custom profiles applied. | +| custom_settings | array | Windows hosts that belong to no team will have custom profiles applied. |
@@ -2099,7 +2099,7 @@ Delete all of a team's existing enroll secrets | email | string | body | **Required.** The email of the invited user. This email will receive the invitation link. | | name | string | body | **Required.** The name of the invited user. | | sso_enabled | boolean | body | **Required.** Whether or not SSO will be enabled for the invited user. | -| teams | list | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | +| teams | array | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | #### Example @@ -2299,7 +2299,7 @@ Verify the specified invite. | email | string | body | The email of the invited user. Updates on the email won't resend the invitation. | | name | string | body | The name of the invited user. | | sso_enabled | boolean | body | Whether or not SSO will be enabled for the invited user. | -| teams | list | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | +| teams | array | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | #### Example @@ -3699,7 +3699,7 @@ _Available in Fleet Premium_ | Name | Type | In | Description | | ------- | ------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ids | list | body | A list of the host IDs you'd like to delete. If `ids` is specified, `filters` cannot be specified. | +| ids | array | body | A list of the host IDs you'd like to delete. If `ids` is specified, `filters` cannot be specified. | | filters | object | body | Contains any of the following four properties: `query` for search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, and `ipv4`. `status` to indicate the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. `label_id` to indicate the selected label. `team_id` to indicate the selected team. If `filters` is specified, `id` cannot be specified. `label_id` and `status` cannot be used at the same time. | Either ids or filters are required. @@ -4666,9 +4666,9 @@ Adds manual labels to a host. #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ---- | ---------------------------- | -| labels | list | body | The list of label names to add to the host. | +| Name | Type | In | Description | +| ------ | ------- | ---- | ---------------------------- | +| labels | array | body | The list of label names to add to the host. | #### Example @@ -4695,9 +4695,9 @@ Removes manual labels from a host. #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ---- | ---------------------------- | -| labels | list | body | The list of label names to delete from the host. | +| Name | Type | In | Description | +| ------ | ------- | ---- | ---------------------------- | +| labels | array | body | The list of label names to delete from the host. | #### Example @@ -6561,7 +6561,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th | Name | Type | In | Description | | -------- | ------- | ---- | ------------------------------------------------- | -| ids | list | body | **Required.** The IDs of the policies to delete. | +| ids | array | body | **Required.** The IDs of the policies to delete. | #### Example @@ -6655,8 +6655,8 @@ Triggers [automations](https://fleetdm.com/docs/using-fleet/automations#policy-a | Name | Type | In | Description | | ---------- | -------- | ---- | -------------------------------------------------------- | -| policy_ids | list | body | Filters to only run policy automations for the specified policies. | -| team_ids | list | body | _Available in Fleet Premium_. Filters to only run policy automations for hosts in the specified teams. | +| policy_ids | array | body | Filters to only run policy automations for the specified policies. | +| team_ids | array | body | _Available in Fleet Premium_. Filters to only run policy automations for hosts in the specified teams. | #### Example @@ -6996,7 +6996,7 @@ Either `query` or `query_id` must be provided. | Name | Type | In | Description | | -------- | ------- | ---- | ------------------------------------------------- | | team_id | integer | path | **Required.** Defines what team ID to operate on | -| ids | list | body | **Required.** The IDs of the policies to delete. | +| ids | array | body | **Required.** The IDs of the policies to delete. | #### Example @@ -7433,14 +7433,14 @@ Creates a global query or team query. | name | string | body | **Required**. The name of the query. | | query | string | body | **Required**. The query in SQL syntax. | | description | string | body | The query's description. | -| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | +| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | | team_id | integer | body | _Available in Fleet Premium_. The parent team to which the new query should be added. If omitted, the query will be global. | -| interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. | +| interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. | | platform | string | body | The OS platforms where this query will run (other platforms ignored). Comma-separated string. If omitted, runs on all compatible platforms. | | min_osquery_version | string | body | The minimum required osqueryd version installed on a host. If omitted, all osqueryd versions are acceptable. | | automations_enabled | boolean | body | Whether to send data to the configured log destination according to the query's `interval`. | -| logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. | -| discard_data | bool | body | Whether to skip saving the latest query results for each host. Default: `false`. | +| logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. | +| discard_data | boolean | body | Whether to skip saving the latest query results for each host. Default: `false`. | #### Example @@ -7507,13 +7507,13 @@ Modifies the query specified by ID. | name | string | body | The name of the query. | | query | string | body | The query in SQL syntax. | | description | string | body | The query's description. | -| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | +| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | | interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. | | platform | string | body | The OS platforms where this query will run (other platforms ignored). Comma-separated string. If set to "", runs on all compatible platforms. | | min_osquery_version | string | body | The minimum required osqueryd version installed on a host. If omitted, all osqueryd versions are acceptable. | | automations_enabled | boolean | body | Whether to send data to the configured log destination according to the query's `interval`. | | logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. | -| discard_data | bool | body | Whether to skip saving the latest query results for each host. | +| discard_data | boolean | body | Whether to skip saving the latest query results for each host. | > Note that any of the following conditions will cause the existing query report to be deleted: > - Updating the `query` (SQL) field @@ -7617,9 +7617,9 @@ Deletes the queries specified by ID. Returns the count of queries successfully d #### Parameters -| Name | Type | In | Description | -| ---- | ---- | ---- | ------------------------------------- | -| ids | list | body | **Required.** The IDs of the queries. | +| Name | Type | In | Description | +| ---- | ----- | ---- | ------------------------------------- | +| ids | array | body | **Required.** The IDs of the queries. | #### Example @@ -8508,9 +8508,9 @@ Get a list of all software. | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | | query | string | query | Search query keywords. Searchable fields include `title` and `cve`. | | team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. Use `0` to filter by hosts assigned to "No team". | -| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | -| available_for_install | bool | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`. | -| self_service | bool | query | If `true` or `1`, only lists self-service software. Default is `false`. | +| vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | +| available_for_install | boolean | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`. | +| self_service | boolean | query | If `true` or `1`, only lists self-service software. Default is `false`. | #### Example @@ -8629,7 +8629,7 @@ Get a list of all software versions. | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | | query | string | query | Search query keywords. Searchable fields include `name`, `version`, and `cve`. | | team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. Use `0` to filter by hosts assigned to "No team". | -| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | +| vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | #### Example @@ -9836,8 +9836,8 @@ _Available in Fleet Premium_ | ------------------------------------------------------- | ------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | id | integer | path | **Required.** The desired team's ID. | | name | string | body | The team's name. | -| host_ids | list | body | A list of hosts that belong to the team. | -| user_ids | list | body | A list of users on the team. | +| host_ids | array | body | A list of hosts that belong to the team. | +| user_ids | array | body | A list of users on the team. | | webhook_settings | object | body | Webhook settings contains for the team. | |   failing_policies_webhook | object | body | Failing policies webhook settings. | |     enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. | @@ -9872,10 +9872,10 @@ _Available in Fleet Premium_ |     deadline_days | integer | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before updates are installed on Windows. | |     grace_period_days | integer | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before Windows restarts to install updates. | |   macos_settings | object | body | macOS-specific settings. | -|     custom_settings | list | body | The list of objects where each object includes .mobileconfig or JSON file (configuration profile) and label name to apply to macOS hosts that belong to this team and are members of the specified label. | +|     custom_settings | array | body | The list of objects where each object includes .mobileconfig or JSON file (configuration profile) and label name to apply to macOS hosts that belong to this team and are members of the specified label. | |     enable_disk_encryption | boolean | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have disk encryption enabled if set to true. | |   windows_settings | object | body | Windows-specific settings. | -|     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. | +|     custom_settings | array | 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. | @@ -10095,8 +10095,8 @@ _Available in Fleet Premium_ | Name | Type | In | Description | | --- | --- | --- | --- | | id | integer | path | **Required.** The desired team's ID. | -| force | bool | query | Force apply the options even if there are validation errors. | -| dry_run | bool | query | Validate the options and return any validation errors, but do not apply the changes. | +| force | boolean | query | Force apply the options even if there are validation errors. | +| dry_run | boolean | query | Validate the options and return any validation errors, but do not apply the changes. | | _JSON data_ | object | body | The JSON to use as agent options for this team. See [Agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) for details. | #### Example @@ -10207,9 +10207,9 @@ Transforms a host name into a host id. For example, the Fleet UI use this endpoi #### Parameters -| Name | Type | In | Description | -| ---- | ----- | ---- | ---------------------------------------- | -| list | array | body | **Required** list of items to translate. | +| Name | Type | In | Description | +| ----- | ----- | ---- | ---------------------------------------- | +| array | array | body | **Required** list of items to translate. | #### Example From 51709eadb6c5470772776b3de233c1b3c64731d7 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 3 Sep 2024 15:50:43 -0400 Subject: [PATCH 20/25] Bugfix: cron startup scheduling is delayed too long if no prior run exists (#21784) --- changes/21757-fix-scheduling-cron-jobs-at-startup | 1 + server/service/schedule/schedule.go | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changes/21757-fix-scheduling-cron-jobs-at-startup diff --git a/changes/21757-fix-scheduling-cron-jobs-at-startup b/changes/21757-fix-scheduling-cron-jobs-at-startup new file mode 100644 index 0000000000..b54ae2c84f --- /dev/null +++ b/changes/21757-fix-scheduling-cron-jobs-at-startup @@ -0,0 +1 @@ +* Fixed an issue with the scheduling of cron jobs at startup if the job has never run, which caused it to be delayed. diff --git a/server/service/schedule/schedule.go b/server/service/schedule/schedule.go index 8965416a64..7ca865416a 100644 --- a/server/service/schedule/schedule.go +++ b/server/service/schedule/schedule.go @@ -167,7 +167,13 @@ func (s *Schedule) Start() { level.Error(s.logger).Log("err", "start schedule", "details", err) ctxerr.Handle(s.ctx, err) } - s.setIntervalStartedAt(prevScheduledRun.CreatedAt) + + // if there is no previous run, set the start time to the current time. + startedAt := prevScheduledRun.CreatedAt + if startedAt.IsZero() { + startedAt = time.Now() + } + s.setIntervalStartedAt(startedAt) initialWait := 10 * time.Second if schedInterval := s.getSchedInterval(); schedInterval < initialWait { From 0091a45905593b3049301818a3c2b7e49245a350 Mon Sep 17 00:00:00 2001 From: Robert Fairburn <8029478+rfairburn@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:58:15 -0500 Subject: [PATCH 21/25] ensure that "/repo" can ever be accessed (#21788) The `/repo` path would never get hit in the order that this was previously. This corrects the behavior. --- tools/mdm/migration/mdmproxy/mdmproxy.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/mdm/migration/mdmproxy/mdmproxy.go b/tools/mdm/migration/mdmproxy/mdmproxy.go index 723db4f19f..c59bf4b425 100644 --- a/tools/mdm/migration/mdmproxy/mdmproxy.go +++ b/tools/mdm/migration/mdmproxy/mdmproxy.go @@ -84,14 +84,6 @@ func (m *mdmProxy) handleProxy(w http.ResponseWriter, r *http.Request) { return } - if !strings.HasPrefix(r.URL.Path, "/mdm") { - if m.logSkipped { - log.Printf("Forbidden non-mdm request: %s %s", r.Method, r.URL.String()) - } - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - // Send all micromdm repo requests to the existing server if strings.HasPrefix(r.URL.Path, "/repo") { log.Printf("%s %s -> Existing (Repo)", r.Method, r.URL.String()) @@ -100,6 +92,14 @@ func (m *mdmProxy) handleProxy(w http.ResponseWriter, r *http.Request) { } + if !strings.HasPrefix(r.URL.Path, "/mdm") { + if m.logSkipped { + log.Printf("Forbidden non-mdm request: %s %s", r.Method, r.URL.String()) + } + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + // Read the body of the request body, err := io.ReadAll(r.Body) _ = r.Body.Close() From 9f5f848cdeebf4f8d2d96d3e6390655d4f1dd6ac Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 3 Sep 2024 15:56:32 -0500 Subject: [PATCH 22/25] Website: add fixed width to Github stars button in website navigation (#21783) Closes: #21469 Changes: - Updated the styles for the GitHub stars button in the websites desktop header navigation to prevent layout shifts when navigating between pages. --- website/assets/styles/layout.less | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/assets/styles/layout.less b/website/assets/styles/layout.less index 94e56945a8..6eefec24a9 100644 --- a/website/assets/styles/layout.less +++ b/website/assets/styles/layout.less @@ -444,8 +444,9 @@ html, body { } } [purpose='gh-button'] { - margin-left: 20px; - margin-right: 20px; + padding: 0px 20px; + min-width: 140px; + width: 140px; } [purpose='header-dropdown'] { box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.4); From 271368c72d39d12d9c6565501802cc6c6e19b5d7 Mon Sep 17 00:00:00 2001 From: Harrison Ravazzolo <38767391+harrisonravazzolo@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:01:33 -0700 Subject: [PATCH 23/25] Add Harrison to humans list (#21794) --- 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 05067166ec..4dcbad029b 100644 --- a/website/api/controllers/webhooks/receive-from-github.js +++ b/website/api/controllers/webhooks/receive-from-github.js @@ -90,6 +90,7 @@ module.exports = { 'ddribeiro', 'rebeccaui', 'allenhouchins', + 'harrisonravazzolo', ]; let GREEN_LABEL_COLOR = 'C2E0C6';// « Used in multiple places below. (FUTURE: Use the "+" prefix for this instead of color. 2022-05-05) From 09b6402f76332b431e16bf37c83d5913cbcb12f4 Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:35:33 -0700 Subject: [PATCH 24/25] =?UTF-8?q?UI=20=E2=80=93=20Policy=20software=20inst?= =?UTF-8?q?all=20automations=20(#21792)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Front end for #19551 Feature branch merge to `main` – all work as been previously approved in individual PRs to the feature branch. - [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/19551-policy-software-automations | 1 + frontend/__mocks__/policyMock.ts | 4 + frontend/components/Editor/Editor.tsx | 6 + frontend/components/Editor/_styles.scss | 1 - frontend/components/FleetAce/FleetAce.tsx | 3 + frontend/components/FleetAce/_styles.scss | 10 + .../forms/fields/Dropdown/Dropdown.jsx | 15 +- frontend/interfaces/policy.ts | 8 + frontend/interfaces/software.ts | 1 + .../AddProfileModal/AddProfileModal.tsx | 2 +- .../AdvancedOptionsModal.tsx | 2 + .../DeleteSoftwareModal.tsx | 13 +- .../SoftwarePackageCard.tsx | 112 +++---- .../SoftwarePackageCard/_styles.scss | 41 ++- .../SoftwareTitleDetailsPage.tsx | 1 + .../SoftwareTitleDetailsTable.tsx | 23 +- .../components/AddPackage/AddPackage.tsx | 25 +- .../AddPackageAdvancedOptions.tsx | 101 +++++++ .../_styles.scss | 2 +- .../AddPackageAdvancedOptions/index.ts | 1 + ...AddSoftwareForm.tsx => AddPackageForm.tsx} | 116 ++------ .../components/AddPackageForm/_styles.scss | 2 +- .../components/AddPackageForm/helpers.ts | 92 +----- .../components/AddPackageForm/index.ts | 2 +- .../AddSoftwareAdvancedOptions.tsx | 110 ------- .../AddSoftwareAdvancedOptions/index.ts | 1 - .../InstallStatusCell/InstallStatusCell.tsx | 26 +- .../SelfService/SelfService.tests.tsx | 4 +- .../SelfServiceItem/SelfServiceItem.tsx | 32 +- .../SelfService/SelfServiceItem/_styles.scss | 4 - .../ManagePoliciesPage/ManagePoliciesPage.tsx | 109 ++++++- .../policies/ManagePoliciesPage/_styles.scss | 8 +- .../CalendarEventsModal.tsx | 10 +- .../InstallSoftwareModal.tsx | 276 ++++++++++++++++++ .../InstallSoftwareModal/_styles.scss | 41 +++ .../components/InstallSoftwareModal/index.ts | 1 + .../OtherWorkflowsModal.tsx | 4 +- frontend/services/entities/software.ts | 11 +- frontend/services/entities/team_policies.ts | 2 + frontend/utilities/constants.tsx | 8 +- 40 files changed, 817 insertions(+), 414 deletions(-) create mode 100644 changes/19551-policy-software-automations create mode 100644 frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/AddPackageAdvancedOptions.tsx rename frontend/pages/SoftwarePage/components/{AddSoftwareAdvancedOptions => AddPackageAdvancedOptions}/_styles.scss (88%) create mode 100644 frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/index.ts rename frontend/pages/SoftwarePage/components/AddPackageForm/{AddSoftwareForm.tsx => AddPackageForm.tsx} (55%) delete mode 100644 frontend/pages/SoftwarePage/components/AddSoftwareAdvancedOptions/AddSoftwareAdvancedOptions.tsx delete mode 100644 frontend/pages/SoftwarePage/components/AddSoftwareAdvancedOptions/index.ts create mode 100644 frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx create mode 100644 frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss create mode 100644 frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/index.ts diff --git a/changes/19551-policy-software-automations b/changes/19551-policy-software-automations new file mode 100644 index 0000000000..4b88cb4c1f --- /dev/null +++ b/changes/19551-policy-software-automations @@ -0,0 +1 @@ +* Implement features allowing automatic installation of software on hosts that fail policies. diff --git a/frontend/__mocks__/policyMock.ts b/frontend/__mocks__/policyMock.ts index b14095463e..ba60c55c74 100644 --- a/frontend/__mocks__/policyMock.ts +++ b/frontend/__mocks__/policyMock.ts @@ -23,6 +23,10 @@ const DEFAULT_POLICY_MOCK: IPolicyStats = { has_run: true, next_update_ms: 3600000, calendar_events_enabled: true, + install_software: { + name: "testSw0", + software_title_id: 1, + }, }; const createMockPolicy = (overrides?: Partial): IPolicyStats => { diff --git a/frontend/components/Editor/Editor.tsx b/frontend/components/Editor/Editor.tsx index e8f230e379..b724a296c3 100644 --- a/frontend/components/Editor/Editor.tsx +++ b/frontend/components/Editor/Editor.tsx @@ -29,6 +29,10 @@ interface IEditorProps { * @default "editor" */ name?: string; + /** Include correct styles as a form field. + * @default false + */ + isFormField?: boolean; maxLines?: number; className?: string; onChange?: (value: string, event?: any) => void; @@ -52,11 +56,13 @@ const Editor = ({ readOnly = false, wrapEnabled = false, name = "editor", + isFormField = false, maxLines = 20, className, onChange, }: IEditorProps) => { const classNames = classnames(baseClass, className, { + "form-field": isFormField, [`${baseClass}__error`]: !!error, }); diff --git a/frontend/components/Editor/_styles.scss b/frontend/components/Editor/_styles.scss index 676172697d..22fbacfd33 100644 --- a/frontend/components/Editor/_styles.scss +++ b/frontend/components/Editor/_styles.scss @@ -3,7 +3,6 @@ &__label { font-size: $x-small; font-weight: $bold; - margin-bottom: $pad-small; &--error { color: $core-vibrant-red; diff --git a/frontend/components/FleetAce/FleetAce.tsx b/frontend/components/FleetAce/FleetAce.tsx index c30232cb59..b0422d4cde 100644 --- a/frontend/components/FleetAce/FleetAce.tsx +++ b/frontend/components/FleetAce/FleetAce.tsx @@ -29,6 +29,7 @@ export interface IFleetAceProps { label?: string; name?: string; value?: string; + placeholder?: string; readOnly?: boolean; maxLines?: number; showGutter?: boolean; @@ -55,6 +56,7 @@ const FleetAce = ({ labelActionComponent, name = "query-editor", value, + placeholder, readOnly, maxLines = 20, showGutter = true, @@ -266,6 +268,7 @@ const FleetAce = ({ showPrintMargin={false} theme="fleet" value={value} + placeholder={placeholder} width="100%" wrapEnabled={wrapEnabled} style={style} diff --git a/frontend/components/FleetAce/_styles.scss b/frontend/components/FleetAce/_styles.scss index c12237a3a7..f9f0dccf89 100644 --- a/frontend/components/FleetAce/_styles.scss +++ b/frontend/components/FleetAce/_styles.scss @@ -25,6 +25,16 @@ } } + .ace_content { + padding-left: 4px; + } + + .ace_placeholder { + font-family: "SourceCodePro", $monospace; + margin: initial; + font-size: 15px; + } + &__help-text { @include help-text; diff --git a/frontend/components/forms/fields/Dropdown/Dropdown.jsx b/frontend/components/forms/fields/Dropdown/Dropdown.jsx index 1edde66fec..302233e5d6 100644 --- a/frontend/components/forms/fields/Dropdown/Dropdown.jsx +++ b/frontend/components/forms/fields/Dropdown/Dropdown.jsx @@ -27,6 +27,19 @@ class Dropdown extends Component { onClose: PropTypes.func, options: PropTypes.arrayOf(dropdownOptionInterface).isRequired, placeholder: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), + /** + value must correspond to the value of a dropdown option to render + e.g. with options: + + [ + { + label: "Display name", + value: 1, <– the id of the thing + } + ] + + set value to 1, not "Display name" + */ value: PropTypes.oneOfType([ PropTypes.array, PropTypes.string, @@ -75,7 +88,7 @@ class Dropdown extends Component { const { multi, onChange, clearable, name, parseTarget } = this.props; if (parseTarget) { - // Returns both name and value + // Returns both name of the Dropdown and value of the selected option return onChange({ value: selected.value, name }); } diff --git a/frontend/interfaces/policy.ts b/frontend/interfaces/policy.ts index 41586ea22d..621c52f638 100644 --- a/frontend/interfaces/policy.ts +++ b/frontend/interfaces/policy.ts @@ -41,6 +41,12 @@ export interface IPolicy { updated_at: string; critical: boolean; calendar_events_enabled: boolean; + install_software?: IPolicySoftwareToInstall; +} + +export interface IPolicySoftwareToInstall { + name: string; + software_title_id: number; } // Used on the manage hosts page and other places where aggregate stats are displayed @@ -94,6 +100,8 @@ export interface IPolicyFormData { team_id?: number | null; id?: number; calendar_events_enabled?: boolean; + // undefined from GET/LIST when not set, null for PATCH to unset + software_title_id?: number | null; } export interface IPolicyNew { diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index bf6ba786a1..9d6d3617b2 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -109,6 +109,7 @@ export interface ISoftwareTitleDetails { source: string; // "apps" | "ios_apps" | "ipados_apps" | ? hosts_count: number; versions: ISoftwareTitleVersion[] | null; + versions_updated_at?: string; bundle_identifier?: string; browser?: string; versions_count?: number; diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/AddProfileModal.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/AddProfileModal.tsx index 12191a9641..694970c4ca 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/AddProfileModal.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/AddProfileModal.tsx @@ -77,7 +77,7 @@ interface IFileDetailsProps { // TODO: if we reuse this one more time, we should consider moving this // into FileUploader as a default preview. Currently we have this in -// AddSoftwareForm.tsx and here. +// AddPackageForm.tsx and here. const FileDetails = ({ details: { name, platform } }: IFileDetailsProps) => (
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx index b3f7c31146..5cfc313e44 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx @@ -38,6 +38,7 @@ const AdvancedOptionsModal = ({ helpText="Fleet will run this command on hosts to install software." label="Install script" labelTooltip="For security agents, add the script provided by the vendor." + isFormField /> {preInstallQuery && (
@@ -72,6 +73,7 @@ const AdvancedOptionsModal = ({ maxLines={10} value={postInstallScript} helpText="Shell (macOS and Linux) or PowerShell (Windows)." + isFormField />
)} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx index 23d03c5852..fa75f1d554 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx @@ -3,11 +3,15 @@ import React, { useCallback, useContext } from "react"; import softwareAPI from "services/entities/software"; import { NotificationContext } from "context/notification"; +import { getErrorReason } from "interfaces/errors"; + import Modal from "components/Modal"; import Button from "components/buttons/Button"; const baseClass = "delete-software-modal"; +const DELETE_SW_USED_BY_POLICY_ERROR_MSG = + "Couldn't delete. Policy automation uses this software. Please disable policy automation for this software and try again."; interface IDeleteSoftwareModalProps { softwareId: number; teamId: number; @@ -28,8 +32,13 @@ const DeleteSoftwareModal = ({ await softwareAPI.deleteSoftwarePackage(softwareId, teamId); renderFlash("success", "Software deleted successfully!"); onSuccess(); - } catch { - renderFlash("error", "Couldn't delete. Please try again."); + } catch (error) { + const reason = getErrorReason(error); + if (reason.includes("Policy automation uses this software")) { + renderFlash("error", DELETE_SW_USED_BY_POLICY_ERROR_MSG); + } else { + renderFlash("error", "Couldn't delete. Please try again."); + } } onExit(); }, [softwareId, teamId, renderFlash, onSuccess, onExit]); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx index 8453abfbb6..1c6a31e9d7 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx @@ -4,8 +4,6 @@ import React, { useLayoutEffect, useState, } from "react"; -import FileSaver from "file-saver"; -import { parse } from "content-disposition"; import PATHS from "router/paths"; import { AppContext } from "context/app"; @@ -45,10 +43,15 @@ function useTruncatedElement(ref: React.RefObject) { useLayoutEffect(() => { const element = ref.current; - if (element) { - const { scrollWidth, clientWidth } = element; - setIsTruncated(scrollWidth > clientWidth); + function updateIsTruncated() { + if (element) { + const { scrollWidth, clientWidth } = element; + setIsTruncated(scrollWidth > clientWidth); + } } + window.addEventListener("resize", updateIsTruncated); + updateIsTruncated(); + return () => window.removeEventListener("resize", updateIsTruncated); }, [ref]); return isTruncated; @@ -92,20 +95,29 @@ const STATUS_DISPLAY_OPTIONS: Record< iconName: "success", tooltip: ( <> - Fleet installed software on these hosts. Currently, if the software is - uninstalled, the "Installed" status won't be updated. + Software is installed on these hosts (install script finished +
+ with exit code 0). Currently, if the software is uninstalled, the +
+ "installed" status won't be updated. ), }, pending: { displayName: "Pending", iconName: "pending-outline", - tooltip: "Fleet will install software when these hosts come online.", + tooltip: "Fleet is installing or will install when the host comes online.", }, failed: { displayName: "Failed", iconName: "error", - tooltip: "Fleet failed to install software on these hosts.", + tooltip: ( + <> + These hosts failed to install software. Click on a host to view +
+ error(s). + + ), }, }; @@ -130,16 +142,18 @@ const PackageStatusCount = ({ })}`; return (
- {displayData.displayName} +
{displayData.displayName}
} @@ -305,7 +319,7 @@ const SoftwarePackageCard = ({ return ( -
+
{/* TODO: main-info could be a seperate component as its reused on a couple pages already. Come back and pull this into a component */}
@@ -315,46 +329,46 @@ const SoftwarePackageCard = ({ {renderDetails()}
-
- - - +
+ {isSelfService && ( +
+ + Self-service +
+ )} + {showActions && ( + + )}
-
- {isSelfService && ( -
- - Self-service -
- )} - {showActions && ( - - )} +
+ + +
{showAdvancedOptionsModal && ( diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx index 4eb9660e62..6eaafa2a28 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx @@ -13,6 +13,7 @@ import TableContainer from "components/TableContainer"; import TableCount from "components/TableContainer/TableCount"; import EmptyTable from "components/EmptyTable"; import CustomLink from "components/CustomLink"; +import LastUpdatedText from "components/LastUpdatedText"; import generateSoftwareTitleDetailsTableConfig from "./SoftwareTitleDetailsTableConfig"; @@ -21,6 +22,21 @@ const DEFAULT_SORT_DIRECTION = "desc"; const baseClass = "software-title-details-table"; +const SoftwareLastUpdatedInfo = (lastUpdatedAt: string) => { + return ( + + The last time software data was
+ updated, including vulnerabilities
+ and host counts. + + } + /> + ); +}; + const NoVersionsDetected = (isAvailableForInstall = false): JSX.Element => { return ( { const handleRowSelect = (row: IRowProps) => { const hostsBySoftwareParams = { @@ -95,7 +113,10 @@ const SoftwareTitleDetailsTable = ({ ); const renderVersionsCount = () => ( - + <> + + {countsUpdatedAt && SoftwareLastUpdatedInfo(countsUpdatedAt)} + ); return ( diff --git a/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx b/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx index 2e7e4b17a5..52cb805f7f 100644 --- a/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx +++ b/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx @@ -1,13 +1,19 @@ import React, { useContext, useEffect, useState } from "react"; import { InjectedRouter } from "react-router"; +import { getErrorReason } from "interfaces/errors"; + import PATHS from "router/paths"; import { NotificationContext } from "context/notification"; import softwareAPI from "services/entities/software"; import { QueryParams, buildQueryStringFromParams } from "utilities/url"; +import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; + +import CustomLink from "components/CustomLink"; + import AddPackageForm from "../AddPackageForm"; -import { IAddSoftwareFormData } from "../AddPackageForm/AddSoftwareForm"; +import { IAddPackageFormData } from "../AddPackageForm/AddPackageForm"; import { getErrorMessage } from "../AddSoftwareModal/helpers"; const baseClass = "add-package"; @@ -60,7 +66,7 @@ const AddPackage = ({ }; }, [isUploading]); - const onAddPackage = async (formData: IAddSoftwareFormData) => { + const onAddPackage = async (formData: IAddPackageFormData) => { setIsUploading(true); if (formData.software && formData.software.size > MAX_FILE_SIZE_BYTES) { @@ -98,6 +104,21 @@ const AddPackage = ({ `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}` ); } catch (e) { + const reason = getErrorReason(e); + if ( + reason.includes("Couldn't add. Fleet couldn't read the version from") + ) { + renderFlash( + "error", + `${reason}. ${( + + )} ` + ); + } renderFlash("error", getErrorMessage(e)); } diff --git a/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/AddPackageAdvancedOptions.tsx b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/AddPackageAdvancedOptions.tsx new file mode 100644 index 0000000000..5d6524e8dc --- /dev/null +++ b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/AddPackageAdvancedOptions.tsx @@ -0,0 +1,101 @@ +import React, { useState } from "react"; + +import Editor from "components/Editor"; +import CustomLink from "components/CustomLink"; +import FleetAce from "components/FleetAce"; +import RevealButton from "components/buttons/RevealButton"; + +const baseClass = "add-package-advanced-options"; + +interface IAddPackageAdvancedOptionsProps { + errors: { preInstallQuery?: string; postInstallScript?: string }; + preInstallQuery?: string; + installScript: string; + postInstallScript?: string; + onChangePreInstallQuery: (value?: string) => void; + onChangeInstallScript: (value: string) => void; + onChangePostInstallScript: (value?: string) => void; +} + +const AddPackageAdvancedOptions = ({ + errors, + preInstallQuery, + installScript, + postInstallScript, + onChangePreInstallQuery, + onChangeInstallScript, + onChangePostInstallScript, +}: IAddPackageAdvancedOptionsProps) => { + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + return ( +
+ setShowAdvancedOptions(!showAdvancedOptions)} + /> + {showAdvancedOptions && ( +
+ + Software will be installed only if the{" "} + + + } + /> + + Fleet will run this script on hosts to install software. Use the +
+ $INSTALLER_PATH variable to point to the installer. + + } + isFormField + /> + +
+ )} +
+ ); +}; + +export default AddPackageAdvancedOptions; diff --git a/frontend/pages/SoftwarePage/components/AddSoftwareAdvancedOptions/_styles.scss b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/_styles.scss similarity index 88% rename from frontend/pages/SoftwarePage/components/AddSoftwareAdvancedOptions/_styles.scss rename to frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/_styles.scss index 58f1f85892..0728e32415 100644 --- a/frontend/pages/SoftwarePage/components/AddSoftwareAdvancedOptions/_styles.scss +++ b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/_styles.scss @@ -1,4 +1,4 @@ -.add-software-advanced-options { +.add-package-advanced-options { display: flex; flex-direction: column; align-items: flex-start; diff --git a/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/index.ts b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/index.ts new file mode 100644 index 0000000000..004c96332d --- /dev/null +++ b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/index.ts @@ -0,0 +1 @@ +export { default } from "./AddPackageAdvancedOptions"; diff --git a/frontend/pages/SoftwarePage/components/AddPackageForm/AddSoftwareForm.tsx b/frontend/pages/SoftwarePage/components/AddPackageForm/AddPackageForm.tsx similarity index 55% rename from frontend/pages/SoftwarePage/components/AddPackageForm/AddSoftwareForm.tsx rename to frontend/pages/SoftwarePage/components/AddPackageForm/AddPackageForm.tsx index aab97985d0..cf3802b3f6 100644 --- a/frontend/pages/SoftwarePage/components/AddPackageForm/AddSoftwareForm.tsx +++ b/frontend/pages/SoftwarePage/components/AddPackageForm/AddPackageForm.tsx @@ -6,7 +6,6 @@ import getInstallScript from "utilities/software_install_scripts"; import Button from "components/buttons/Button"; import Checkbox from "components/forms/fields/Checkbox"; -import Editor from "components/Editor"; import { FileUploader, FileDetails, @@ -14,25 +13,25 @@ import { import Spinner from "components/Spinner"; import TooltipWrapper from "components/TooltipWrapper"; -import AddSoftwareAdvancedOptions from "../AddSoftwareAdvancedOptions"; +import AddPackageAdvancedOptions from "../AddPackageAdvancedOptions"; import { generateFormValidation } from "./helpers"; -export const baseClass = "add-software-form"; +export const baseClass = "add-package-form"; const UploadingSoftware = () => { return (
-

Uploading. It may take a few minutes to finish.

+

Adding software. This may take a few minutes to finish.

); }; -export interface IAddSoftwareFormData { +export interface IAddPackageFormData { software: File | null; installScript: string; - preInstallCondition?: string; + preInstallQuery?: string; postInstallScript?: string; selfService: boolean; } @@ -40,30 +39,28 @@ export interface IAddSoftwareFormData { export interface IFormValidation { isValid: boolean; software: { isValid: boolean }; - preInstallCondition?: { isValid: boolean; message?: string }; + preInstallQuery?: { isValid: boolean; message?: string }; postInstallScript?: { isValid: boolean; message?: string }; selfService?: { isValid: boolean }; } -interface IAddSoftwareFormProps { +interface IAddPackageFormProps { isUploading: boolean; onCancel: () => void; - onSubmit: (formData: IAddSoftwareFormData) => void; + onSubmit: (formData: IAddPackageFormData) => void; } -const AddSoftwareForm = ({ +const AddPackageForm = ({ isUploading, onCancel, onSubmit, -}: IAddSoftwareFormProps) => { +}: IAddPackageFormProps) => { const { renderFlash } = useContext(NotificationContext); - const [showPreInstallCondition, setShowPreInstallCondition] = useState(false); - const [showPostInstallScript, setShowPostInstallScript] = useState(false); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ software: null, installScript: "", - preInstallCondition: undefined, + preInstallQuery: undefined, postInstallScript: undefined, selfService: false, }); @@ -90,13 +87,7 @@ const AddSoftwareForm = ({ installScript, }; setFormData(newData); - setFormValidation( - generateFormValidation( - newData, - showPreInstallCondition, - showPostInstallScript - ) - ); + setFormValidation(generateFormValidation(newData)); } }; @@ -105,62 +96,26 @@ const AddSoftwareForm = ({ onSubmit(formData); }; - const onTogglePreInstallConditionCheckbox = (value: boolean) => { - const newData = { ...formData, preInstallCondition: undefined }; - setShowPreInstallCondition(value); - setFormData(newData); - setFormValidation( - generateFormValidation(newData, value, showPostInstallScript) - ); - }; - - const onTogglePostInstallScriptCheckbox = (value: boolean) => { - const newData = { ...formData, postInstallScript: undefined }; - setShowPostInstallScript(value); - setFormData(newData); - setFormValidation( - generateFormValidation(newData, showPreInstallCondition, value) - ); - }; - const onChangeInstallScript = (value: string) => { setFormData({ ...formData, installScript: value }); }; - const onChangePreInstallCondition = (value?: string) => { - const newData = { ...formData, preInstallCondition: value }; + const onChangePreInstallQuery = (value?: string) => { + const newData = { ...formData, preInstallQuery: value }; setFormData(newData); - setFormValidation( - generateFormValidation( - newData, - showPreInstallCondition, - showPostInstallScript - ) - ); + setFormValidation(generateFormValidation(newData)); }; const onChangePostInstallScript = (value?: string) => { const newData = { ...formData, postInstallScript: value }; setFormData(newData); - setFormValidation( - generateFormValidation( - newData, - showPreInstallCondition, - showPostInstallScript - ) - ); + setFormValidation(generateFormValidation(newData)); }; const onToggleSelfServiceCheckbox = (value: boolean) => { const newData = { ...formData, selfService: value }; setFormData(newData); - setFormValidation( - generateFormValidation( - newData, - showPreInstallCondition, - showPostInstallScript - ) - ); + setFormValidation(generateFormValidation(newData)); }; const isSubmitDisabled = !formValidation.isValid; @@ -185,25 +140,6 @@ const AddSoftwareForm = ({ ) } /> - {formData.software && ( - - For security agents, add the script provided by the vendor. -
- In custom scripts, you can use the $INSTALLER_PATH variable to - point to the installer. - - } - /> - )} -
-
+ ); })} -
+ A calendar event will be created for end users if one of their hosts fail any of these policies.{" "} diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx new file mode 100644 index 0000000000..7c29b4979f --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx @@ -0,0 +1,276 @@ +import React, { useCallback, useState } from "react"; + +import { useQuery } from "react-query"; +import { omit } from "lodash"; + +import { IPolicyStats } from "interfaces/policy"; +import softwareAPI, { + ISoftwareTitlesQueryKey, + ISoftwareTitlesResponse, +} from "services/entities/software"; +import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; + +// @ts-ignore +import Dropdown from "components/forms/fields/Dropdown"; +import Modal from "components/Modal"; +import DataError from "components/DataError"; +import Spinner from "components/Spinner"; +import Checkbox from "components/forms/fields/Checkbox"; +import TooltipTruncatedText from "components/TooltipTruncatedText"; +import CustomLink from "components/CustomLink"; +import Button from "components/buttons/Button"; +import { ISoftwareTitle } from "interfaces/software"; + +const getPlatformDisplayFromPackageSuffix = (packageName: string) => { + const split = packageName.split("."); + const suff = split[split.length - 1]; + switch (suff) { + case "pkg": + return "macOS"; + case "deb": + return "Linux"; + case "exe": + return "Windows"; + case "msi": + return "Windows"; + default: + return null; + } +}; + +const AFI_SOFTWARE_BATCH_SIZE = 1000; + +const baseClass = "install-software-modal"; + +interface ISwDropdownField { + name: string; + value: number; +} +interface IFormPolicy { + name: string; + id: number; + installSoftwareEnabled: boolean; + swIdToInstall?: number; +} + +export type IInstallSoftwareFormData = IFormPolicy[]; + +interface IInstallSoftwareModal { + onExit: () => void; + onSubmit: (formData: IInstallSoftwareFormData) => void; + isUpdating: boolean; + policies: IPolicyStats[]; + teamId: number; +} +const InstallSoftwareModal = ({ + onExit, + onSubmit, + isUpdating, + policies, + teamId, +}: IInstallSoftwareModal) => { + const [formData, setFormData] = useState( + policies.map((policy) => ({ + name: policy.name, + id: policy.id, + installSoftwareEnabled: !!policy.install_software, + swIdToInstall: policy.install_software?.software_title_id, + })) + ); + + const anyPolicyEnabledWithoutSelectedSoftware = formData.some( + (policy) => policy.installSoftwareEnabled && !policy.swIdToInstall + ); + const { + data: titlesAFI, + isLoading: isTitlesAFILoading, + isError: isTitlesAFIError, + } = useQuery< + ISoftwareTitlesResponse, + Error, + ISoftwareTitle[], + [ISoftwareTitlesQueryKey] + >( + [ + { + scope: "software-titles", + page: 0, + perPage: AFI_SOFTWARE_BATCH_SIZE, + query: "", + orderDirection: "desc", + orderKey: "hosts_count", + teamId, + availableForInstall: true, + packagesOnly: true, + }, + ], + ({ queryKey: [queryKey] }) => + softwareAPI.getSoftwareTitles(omit(queryKey, "scope")), + { + select: (data) => data.software_titles, + ...DEFAULT_USE_QUERY_OPTIONS, + } + ); + + const onUpdateInstallSoftware = useCallback(() => { + onSubmit(formData); + }, [formData, onSubmit]); + + const onChangeEnableInstallSoftware = useCallback( + (newVal: { policyName: string; value: boolean }) => { + const { policyName, value } = newVal; + setFormData( + formData.map((policy) => { + if (policy.name === policyName) { + return { + ...policy, + installSoftwareEnabled: value, + swIdToInstall: value ? policy.swIdToInstall : undefined, + }; + } + return policy; + }) + ); + }, + [formData] + ); + + const onSelectPolicySoftware = useCallback( + ({ name, value }: ISwDropdownField) => { + const [policyName, softwareId] = [name, value]; + setFormData( + formData.map((policy) => { + if (policy.name === policyName) { + return { ...policy, swIdToInstall: softwareId }; + } + return policy; + }) + ); + }, + [formData] + ); + + const availableSoftwareOptions = titlesAFI?.map((title) => { + const platformDisplay = getPlatformDisplayFromPackageSuffix( + title.software_package?.name ?? "" + ); + const platformString = platformDisplay ? `${platformDisplay} • ` : ""; + return { + label: title.name, + value: title.id, + helpText: `${platformString}${title.software_package?.version ?? ""}`, + }; + }); + + const renderPolicySwInstallOption = (policy: IFormPolicy) => { + const { + name: policyName, + id: policyId, + installSoftwareEnabled: enabled, + swIdToInstall, + } = policy; + + return ( +
  • + { + onChangeEnableInstallSoftware({ + policyName, + value: !enabled, + }); + }} + > + + + {enabled && ( + + )} +
  • + ); + }; + + const renderContent = () => { + if (isTitlesAFIError) { + return ; + } + if (isTitlesAFILoading) { + return ; + } + if (!titlesAFI?.length) { + return ( +
    + No software available for install + + Go to Software to add software to this team. + +
    + ); + } + + return ( +
    +
    +
    Policies:
    +
      + {formData.map((policyData) => + renderPolicySwInstallOption(policyData) + )} +
    + + Selected software will be installed when hosts fail the chosen + policy.{" "} + + +
    +
    + + +
    +
    + ); + }; + + return ( + + {renderContent()} + + ); +}; + +export default InstallSoftwareModal; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss new file mode 100644 index 0000000000..de9cfc05be --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss @@ -0,0 +1,41 @@ +.manage-policies-page { + .install-software-modal { + .form-field--dropdown { + width: 276px; + .Select-placeholder { + color: $ui-fleet-black-50; + } + .Select-menu { + max-height: none; + overflow: visible; + } + .Select-menu-outer { + max-height: 240px; + overflow-y: auto; + } + } + .policy-row { + height: 40px; + padding-top: 4px; + padding-bottom: 4px; + } + + &__no-software { + display: flex; + height: 178px; + flex-direction: column; + align-items: center; + gap: $pad-small; + justify-content: center; + font-size: $small; + + span { + color: $ui-fleet-black-75; + font-size: $xx-small; + } + } + .data-error { + padding: 78px; + } + } +} diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/index.ts b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/index.ts new file mode 100644 index 0000000000..a9f46a726a --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/index.ts @@ -0,0 +1 @@ +export { default } from "./InstallSoftwareModal"; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/OtherWorkflowsModal/OtherWorkflowsModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/OtherWorkflowsModal/OtherWorkflowsModal.tsx index d34ae56ff6..7a9668e825 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/OtherWorkflowsModal/OtherWorkflowsModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/OtherWorkflowsModal/OtherWorkflowsModal.tsx @@ -416,8 +416,8 @@ const OtherWorkflowsModal = ({ const { isChecked, name, id } = policyItem; return (
    { @@ -220,8 +219,8 @@ export default { formData.append("software", data.software); formData.append("self_service", data.selfService.toString()); data.installScript && formData.append("install_script", data.installScript); - data.preInstallCondition && - formData.append("pre_install_query", data.preInstallCondition); + data.preInstallQuery && + formData.append("pre_install_query", data.preInstallQuery); data.postInstallScript && formData.append("post_install_script", data.postInstallScript); teamId && formData.append("team_id", teamId.toString()); diff --git a/frontend/services/entities/team_policies.ts b/frontend/services/entities/team_policies.ts index a10d954e8b..d2e1386372 100644 --- a/frontend/services/entities/team_policies.ts +++ b/frontend/services/entities/team_policies.ts @@ -86,6 +86,7 @@ export default { platform, critical, calendar_events_enabled, + software_title_id, } = data; const { TEAMS } = endpoints; const path = `${TEAMS}/${team_id}/policies/${id}`; @@ -98,6 +99,7 @@ export default { platform, critical, calendar_events_enabled, + software_title_id, }); }, destroy: (teamId: number | undefined, ids: number[]) => { diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx index c399b5e9c1..4b780aebc9 100644 --- a/frontend/utilities/constants.tsx +++ b/frontend/utilities/constants.tsx @@ -60,9 +60,13 @@ export const HOST_STATUS_WEBHOOK_WINDOW_DROPDOWN_OPTIONS: IDropdownOption[] = [ export const GITHUB_NEW_ISSUE_LINK = "https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md"; -export const SUPPORT_LINK = "https://fleetdm.com/support"; +export const FLEET_WEBSITE_URL = "https://fleetdm.com"; -export const CONTACT_FLEET_LINK = "https://fleetdm.com/contact"; +export const SUPPORT_LINK = `${FLEET_WEBSITE_URL}/support`; + +export const CONTACT_FLEET_LINK = `${FLEET_WEBSITE_URL}/contact`; + +export const LEARN_MORE_ABOUT_BASE_LINK = `${FLEET_WEBSITE_URL}/learn-more-about`; /** July 28, 2016 is the date of the initial commit to fleet/fleet. */ export const INITIAL_FLEET_DATE = "2016-07-28T00:00:00Z"; From 6a5c515dc4aa19d62d5dd431b9ce33c748608e4e Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Tue, 3 Sep 2024 20:49:50 -0300 Subject: [PATCH 25/25] Attempt to use `go.mod` version instead of hidden Github var (#21768) Done as part of oncall improvements. `vars.GO_VERSION` can only be changed by admins and it's not public (Fleet devs don't know the current value of the variable), this approach uses the version specified in our `go.mod` file. --- ...ild-and-check-fleetctl-docker-and-deps.yml | 2 +- .github/workflows/build-binaries.yaml | 8 ++-- .github/workflows/build-orbit.yaml | 2 +- .github/workflows/check-automated-doc.yml | 9 ++-- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/deploy-fleet-website.yml | 2 +- .github/workflows/dogfood-deploy.yml | 5 ++- .github/workflows/fleet-and-orbit.yml | 42 +++++++------------ .github/workflows/fleetctl-preview-latest.yml | 9 ++-- .github/workflows/fleetd-tuf.yml | 10 ++--- .../workflows/generate-desktop-targets.yml | 32 +++++++------- .github/workflows/golangci-lint.yml | 3 +- .github/workflows/goreleaser-fleet.yaml | 2 +- .github/workflows/goreleaser-orbit.yaml | 8 ++-- .../workflows/goreleaser-snapshot-fleet.yaml | 2 +- .github/workflows/integration.yml | 8 ++-- .../release-fleetctl-docker-deps.yaml | 8 ++-- .github/workflows/release-fleetd-base.yml | 10 ++--- .github/workflows/test-db-changes.yml | 9 ++-- .github/workflows/test-go.yaml | 3 +- .../test-native-tooling-packaging.yml | 9 ++-- .github/workflows/test-packaging.yml | 9 ++-- .github/workflows/test-yml-specs.yml | 9 ++-- handbook/engineering/README.md | 3 +- 24 files changed, 97 insertions(+), 109 deletions(-) diff --git a/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml b/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml index 50f4e58f13..ff20260409 100644 --- a/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml +++ b/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Install Go Dependencies run: make deps-go diff --git a/.github/workflows/build-binaries.yaml b/.github/workflows/build-binaries.yaml index ed18437c74..278f958b28 100644 --- a/.github/workflows/build-binaries.yaml +++ b/.github/workflows/build-binaries.yaml @@ -29,10 +29,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Set the Node.js version - name: Set up Node.js ${{ vars.NODE_VERSION }} @@ -40,9 +43,6 @@ jobs: with: node-version: ${{ vars.NODE_VERSION }} - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: JS Dependency Cache id: js-cache uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v2 diff --git a/.github/workflows/build-orbit.yaml b/.github/workflows/build-orbit.yaml index 09f296aece..002d2657f6 100644 --- a/.github/workflows/build-orbit.yaml +++ b/.github/workflows/build-orbit.yaml @@ -59,7 +59,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Build, codesign and notarize orbit run: go run ./orbit/tools/build/build.go diff --git a/.github/workflows/check-automated-doc.yml b/.github/workflows/check-automated-doc.yml index c654e7ae4f..d289c55318 100644 --- a/.github/workflows/check-automated-doc.yml +++ b/.github/workflows/check-automated-doc.yml @@ -36,15 +36,16 @@ jobs: with: egress-policy: audit - - name: Install Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ vars.GO_VERSION }} - name: Checkout Code uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 with: fetch-depth: 0 + - name: Install Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + - name: Verify golang generated documentation is up-to-date run: | make generate-doc diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 246c6418a1..c69888f874 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -56,7 +56,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/deploy-fleet-website.yml b/.github/workflows/deploy-fleet-website.yml index 9fc044e13b..371a0014f0 100644 --- a/.github/workflows/deploy-fleet-website.yml +++ b/.github/workflows/deploy-fleet-website.yml @@ -64,7 +64,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Download top-level dependencies and build Storybook in the website's assets/ folder - run: npm install --legacy-peer-deps && npm run build-storybook -- -o ./website/assets/storybook --loglevel verbose diff --git a/.github/workflows/dogfood-deploy.yml b/.github/workflows/dogfood-deploy.yml index f9d8cff071..f17768eec7 100644 --- a/.github/workflows/dogfood-deploy.yml +++ b/.github/workflows/dogfood-deploy.yml @@ -51,14 +51,17 @@ jobs: - id: fail-on-main run: "false" if: ${{ github.ref == 'main' }} + - uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # v1.7.0 with: role-to-assume: ${{env.AWS_IAM_ROLE}} aws-region: ${{ env.AWS_REGION }} + - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' + - uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3 with: terraform_version: 1.6.3 diff --git a/.github/workflows/fleet-and-orbit.yml b/.github/workflows/fleet-and-orbit.yml index 571d59d067..f4dfb2780e 100644 --- a/.github/workflows/fleet-and-orbit.yml +++ b/.github/workflows/fleet-and-orbit.yml @@ -62,7 +62,6 @@ jobs: timeout-minutes: 60 strategy: matrix: - go-version: ["${{ vars.GO_VERSION }}"] mysql: ["mysql:8.0.36"] runs-on: ubuntu-latest needs: gen @@ -72,10 +71,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} + go-version-file: 'go.mod' # Set the Node.js version - name: Set up Node.js ${{ vars.NODE_VERSION }} @@ -83,9 +85,6 @@ jobs: with: node-version: ${{ vars.NODE_VERSION }} - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Start tunnel env: CERT_PEM: ${{ secrets.CLOUDFLARE_TUNNEL_FLEETUEM_CERT_B64 }} @@ -175,9 +174,6 @@ jobs: # This job also makes sure the Fleet server is up and running. set-enroll-secret: timeout-minutes: 60 - strategy: - matrix: - go-version: ["${{ vars.GO_VERSION }}"] runs-on: ubuntu-latest needs: gen steps: @@ -186,13 +182,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Build Fleetctl run: make fleetctl @@ -218,9 +214,6 @@ jobs: # Here we generate the Fleet Desktop and osqueryd targets for # macOS which can only be generated from a macOS host. build-macos-targets: - strategy: - matrix: - go-version: ["${{ vars.GO_VERSION }}"] # Set macOS version to '12' (current equivalent to macos-latest) for # building the binary. This ensures compatibility with macOS version 13 and # later, avoiding runtime errors on systems using macOS 13 or newer. @@ -234,13 +227,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Build desktop.app.tar.gz and osqueryd.app.tar.gz run: | @@ -269,9 +262,6 @@ jobs: # installed, and installing it is time consuming and unreliable. run-tuf-and-gen-pkgs: timeout-minutes: 60 - strategy: - matrix: - go-version: ["${{ vars.GO_VERSION }}"] runs-on: ubuntu-latest needs: [gen, build-macos-targets] steps: @@ -280,13 +270,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Download macos pre-built apps id: download diff --git a/.github/workflows/fleetctl-preview-latest.yml b/.github/workflows/fleetctl-preview-latest.yml index dda4e0f73c..630cfd1dc3 100644 --- a/.github/workflows/fleetctl-preview-latest.yml +++ b/.github/workflows/fleetctl-preview-latest.yml @@ -53,7 +53,6 @@ jobs: # - Unattended installation of Docker on macOS fails. (see # https://github.com/docker/for-mac/issues/6450) os: [ubuntu-latest] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: @@ -62,13 +61,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Build Fleetctl run: make fleetctl diff --git a/.github/workflows/fleetd-tuf.yml b/.github/workflows/fleetd-tuf.yml index ebeca889da..7641589f10 100644 --- a/.github/workflows/fleetd-tuf.yml +++ b/.github/workflows/fleetd-tuf.yml @@ -30,16 +30,16 @@ jobs: with: egress-policy: audit - - name: Install Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ vars.GO_VERSION }} - - name: Checkout Code uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 with: fetch-depth: 0 + - name: Install Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + - name: Update orbit/TUF.md run: | make fleetd-tuf diff --git a/.github/workflows/generate-desktop-targets.yml b/.github/workflows/generate-desktop-targets.yml index 67313ea762..d7324c9bf0 100644 --- a/.github/workflows/generate-desktop-targets.yml +++ b/.github/workflows/generate-desktop-targets.yml @@ -45,13 +45,13 @@ jobs: with: egress-policy: audit + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Import signing keys env: @@ -98,13 +98,13 @@ jobs: with: egress-policy: audit + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Generate fleet-desktop.exe run: | @@ -139,13 +139,13 @@ jobs: with: egress-policy: audit + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Generate desktop.tar.gz run: | @@ -167,13 +167,13 @@ jobs: with: egress-policy: audit + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Generate desktop.tar.gz run: | diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index df6b9792b7..3d3e95ed2c 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -38,7 +38,6 @@ jobs: matrix: # See #9943, we just need to add windows-latest here once all issues are fixed. os: [ubuntu-latest, macos-latest] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: - name: Harden Runner @@ -52,7 +51,7 @@ jobs: - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} + go-version-file: 'go.mod' - name: Install dependencies (Linux) if: matrix.os == 'ubuntu-latest' diff --git a/.github/workflows/goreleaser-fleet.yaml b/.github/workflows/goreleaser-fleet.yaml index f4224907e0..6ba9aff8f0 100644 --- a/.github/workflows/goreleaser-fleet.yaml +++ b/.github/workflows/goreleaser-fleet.yaml @@ -44,7 +44,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Set the Node.js version - name: Set up Node.js ${{ vars.NODE_VERSION }} diff --git a/.github/workflows/goreleaser-orbit.yaml b/.github/workflows/goreleaser-orbit.yaml index 666f281120..54e16752b3 100644 --- a/.github/workflows/goreleaser-orbit.yaml +++ b/.github/workflows/goreleaser-orbit.yaml @@ -56,7 +56,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Run GoReleaser run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-macos.yml # v1.20.0 @@ -95,7 +95,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Run GoReleaser run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-linux.yml # v1.20.0 @@ -128,7 +128,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Run GoReleaser run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-linux-arm64.yml # v1.20.0 @@ -161,7 +161,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Run GoReleaser run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-windows.yml # v1.20.0 diff --git a/.github/workflows/goreleaser-snapshot-fleet.yaml b/.github/workflows/goreleaser-snapshot-fleet.yaml index 46c1da4193..927cf31be1 100644 --- a/.github/workflows/goreleaser-snapshot-fleet.yaml +++ b/.github/workflows/goreleaser-snapshot-fleet.yaml @@ -57,7 +57,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Set the Node.js version - name: Set up Node.js ${{ vars.NODE_VERSION }} diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 015a464b4b..98c9cd3a59 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -264,13 +264,13 @@ jobs: npm install -g fleetctl fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }} + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Build Fleetctl run: make fleetctl diff --git a/.github/workflows/release-fleetctl-docker-deps.yaml b/.github/workflows/release-fleetctl-docker-deps.yaml index 8fc698f6ac..c751655d93 100644 --- a/.github/workflows/release-fleetctl-docker-deps.yaml +++ b/.github/workflows/release-fleetctl-docker-deps.yaml @@ -36,13 +36,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Login to Docker Hub uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a diff --git a/.github/workflows/release-fleetd-base.yml b/.github/workflows/release-fleetd-base.yml index d7b02cfcf7..9909901964 100644 --- a/.github/workflows/release-fleetd-base.yml +++ b/.github/workflows/release-fleetd-base.yml @@ -51,16 +51,16 @@ jobs: with: egress-policy: audit - - name: Install Go - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 - with: - go-version: ${{ vars.GO_VERSION }} - - name: Checkout Code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 + - name: Install Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version-file: 'go.mod' + - name: Check for fleetd component updates id: check-for-fleetd-component-updates run: | diff --git a/.github/workflows/test-db-changes.yml b/.github/workflows/test-db-changes.yml index ecfe464072..a5b7dd91e3 100644 --- a/.github/workflows/test-db-changes.yml +++ b/.github/workflows/test-db-changes.yml @@ -35,15 +35,16 @@ jobs: with: egress-policy: audit - - name: Install Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ vars.GO_VERSION }} - name: Checkout Code uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 with: fetch-depth: 0 + - name: Install Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + - name: Start Infra Dependencies # Use & to background this run: docker compose up -d mysql_test & diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index d7b93c5d28..b5f2b8fe94 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -44,7 +44,6 @@ jobs: matrix: suite: ["integration", "core"] os: [ubuntu-latest] - go-version: ['${{ vars.GO_VERSION }}'] mysql: ["mysql:8.0.36", "mysql:8.4.2"] continue-on-error: ${{ matrix.suite == 'integration' }} # Since integration tests have a higher chance of failing, often for unrelated reasons, we don't want to fail the whole job if they fail runs-on: ${{ matrix.os }} @@ -65,7 +64,7 @@ jobs: - name: Install Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: - go-version: ${{ matrix.go-version }} + go-version-file: 'go.mod' # Pre-starting dependencies here means they are ready to go when we need them. - name: Start Infra Dependencies diff --git a/.github/workflows/test-native-tooling-packaging.yml b/.github/workflows/test-native-tooling-packaging.yml index 7678e7eeaa..ff0dc4abad 100644 --- a/.github/workflows/test-native-tooling-packaging.yml +++ b/.github/workflows/test-native-tooling-packaging.yml @@ -41,7 +41,6 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: @@ -50,13 +49,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Install Go Dependencies run: make deps-go diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml index f9643bd4e9..dbe5a96244 100644 --- a/.github/workflows/test-packaging.yml +++ b/.github/workflows/test-packaging.yml @@ -47,7 +47,6 @@ jobs: # `macos-latest` uses arm64 by default now, so please be careful when # updating this version. os: [ubuntu-latest, macos-13] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: @@ -83,13 +82,13 @@ jobs: brew install colima colima start --mount $TMPDIR:w + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Install wine and wix if: startsWith(matrix.os, 'macos') diff --git a/.github/workflows/test-yml-specs.yml b/.github/workflows/test-yml-specs.yml index 75e46d6af0..fe8f3ecace 100644 --- a/.github/workflows/test-yml-specs.yml +++ b/.github/workflows/test-yml-specs.yml @@ -33,7 +33,6 @@ jobs: strategy: matrix: os: [ubuntu-latest] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: @@ -42,13 +41,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Run apply spec tests run: | diff --git a/handbook/engineering/README.md b/handbook/engineering/README.md index d6f18bde2c..70d9cd75f4 100644 --- a/handbook/engineering/README.md +++ b/handbook/engineering/README.md @@ -111,11 +111,10 @@ If there is partially merged feature work when the release candidate is created, Before kicking off release QA, confirm that we are using the latest versions of dependencies we want to keep up-to-date with each release. Currently, those dependencies are: 1. **Go**: Latest minor release -- Check the [version included in Fleet](https://github.com/fleetdm/fleet/settings/variables/actions). +- Check the [Go version specified in Fleet's go.mod file](https://github.com/fleetdm/fleet/blob/main/go.mod) (`go 1.XX.YY`). - Check the [latest minor version of Go](https://go.dev/dl/). For example, if we are using `go1.19.8`, and there is a new minor version `go1.19.9`, we will upgrade. - If the latest minor version is greater than the version included in Fleet, [file a bug](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&projects=&template=bug-report.md&title=) and assign it to the [release ritual DRI](https://fleetdm.com/handbook/engineering#rituals) and the current oncall engineer. Add the `~release blocker` label. We must upgrade to the latest minor version before publishing the next release. - If the latest major version is greater than the version included in Fleet, [create a story](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=story%2C%3Aproduct&projects=&template=story.md&title=) and assign it to the [release ritual DRI](https://fleetdm.com/handbook/engineering#rituals) and the current oncall engineer. This will be considered for an upcoming sprint. The release can proceed without upgrading the major version. -- Note that major version upgrades also require an [update to go.mod](https://github.com/fleetdm/fleet/blob/7b3134498873a31ba748ca27fabb0059cef70db9/go.mod#L3). > In Go versioning, the number after the first dot is the "major" version, while the number after the second dot is the "minor" version. For example, in Go 1.19.9, "19" is the major version and "9" is the minor version. Major version upgrades are assessed separately by engineering.