diff --git a/ee/server/service/maintained_apps.go b/ee/server/service/maintained_apps.go index 1c5b00347d..2cc057308f 100644 --- a/ee/server/service/maintained_apps.go +++ b/ee/server/service/maintained_apps.go @@ -5,10 +5,8 @@ import ( "context" "crypto/sha256" "encoding/hex" - "net/url" "os" "path/filepath" - "strings" "time" "github.com/fleetdm/fleet/v4/pkg/file" @@ -19,6 +17,9 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" ) +// noCheckHash is used by homebrew to signal that a hash shouldn't be checked. +const noCheckHash = "no_check" + func (svc *Service) AddFleetMaintainedApp( ctx context.Context, teamID *uint, @@ -52,16 +53,25 @@ func (svc *Service) AddFleetMaintainedApp( return ctxerr.Wrap(ctx, err, "downloading app installer") } - // Validate the bytes we got are what we expected - h := sha256.New() - _, err = h.Write(installerBytes) + extension, err := maintainedapps.ExtensionForBundleIdentifier(app.BundleIdentifier) if err != nil { - return ctxerr.Wrap(ctx, err, "generating SHA256 of maintained app installer") + return ctxerr.Errorf(ctx, "getting extension from bundle identifier %q", app.BundleIdentifier) } - gotHash := hex.EncodeToString(h.Sum(nil)) - if gotHash != app.SHA256 { - return ctxerr.New(ctx, "mismatch in maintained app SHA256 hash") + // Validate the bytes we got are what we expected, if homebrew supports + // it, the string "no_check" is a special token used to signal that the + // hash shouldn't be checked. + if app.SHA256 != noCheckHash { + h := sha256.New() + _, err = h.Write(installerBytes) + if err != nil { + return ctxerr.Wrap(ctx, err, "generating SHA256 of maintained app installer") + } + gotHash := hex.EncodeToString(h.Sum(nil)) + + if gotHash != app.SHA256 { + return ctxerr.New(ctx, "mismatch in maintained app SHA256 hash") + } } // Fall back to the filename if we weren't able to extract a filename from the installer response @@ -69,6 +79,12 @@ func (svc *Service) AddFleetMaintainedApp( filename = app.Name } + // The UI requires all filenames to have extensions. If we couldn't get + // one, use the extension we extracted prior + if filepath.Ext(filename) == "" { + filename = filename + "." + extension + } + installScript = file.Dos2UnixNewlines(installScript) if installScript == "" { installScript = app.InstallScript @@ -79,11 +95,6 @@ func (svc *Service) AddFleetMaintainedApp( uninstallScript = app.UninstallScript } - installerURL, err := url.Parse(app.InstallerURL) - if err != nil { - return err - } - installerReader := bytes.NewReader(installerBytes) payload := &fleet.UploadSoftwareInstallerPayload{ InstallerFile: installerReader, @@ -94,7 +105,7 @@ func (svc *Service) AddFleetMaintainedApp( Filename: filename, Platform: string(app.Platform), Source: "apps", - Extension: strings.TrimPrefix(filepath.Ext(installerURL.Path), "."), + Extension: extension, BundleIdentifier: app.BundleIdentifier, StorageID: app.SHA256, FleetLibraryAppID: &app.ID, diff --git a/server/mdm/maintainedapps/ingest.go b/server/mdm/maintainedapps/ingest.go index 16b3cffa83..11fc619bcf 100644 --- a/server/mdm/maintainedapps/ingest.go +++ b/server/mdm/maintainedapps/ingest.go @@ -54,6 +54,29 @@ func Refresh(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger) erro return i.ingest(ctx, apps) } +// ExtensionForBundleIdentifier returns an extension for the given FMA +// identifier. If one can't be found it returns an empty string. +// +// This function is used because we can't always extract the extension based on +// the installer URL. +func ExtensionForBundleIdentifier(identifier string) (string, error) { + var apps []maintainedApp + if err := json.Unmarshal(appsJSON, &apps); err != nil { + return "", fmt.Errorf("unmarshal embedded apps.json: %w", err) + } + + for _, app := range apps { + if app.BundleIdentifier == identifier { + formats := strings.Split(app.InstallerFormat, ":") + if len(formats) > 0 { + return formats[0], nil + } + } + } + + return "", nil +} + type ingester struct { baseURL string ds fleet.Datastore diff --git a/server/mdm/maintainedapps/ingest_test.go b/server/mdm/maintainedapps/ingest_test.go index 28e289490c..e70013272c 100644 --- a/server/mdm/maintainedapps/ingest_test.go +++ b/server/mdm/maintainedapps/ingest_test.go @@ -166,3 +166,57 @@ func TestIngestValidations(t *testing.T) { }) } } + +func TestExtensionForBundleIdentifier(t *testing.T) { + testCases := []struct { + name string + identifier string + expected string + expectErr bool + }{ + { + name: "Valid identifier with zip format", + identifier: "com.1password.1password", + expected: "zip", + expectErr: false, + }, + { + name: "Valid identifier with dmg format", + identifier: "com.adobe.Reader", + expected: "dmg", + expectErr: false, + }, + { + name: "Valid identifier with pkg format", + identifier: "com.box.desktop", + expected: "pkg", + expectErr: false, + }, + { + name: "Non-existent identifier", + identifier: "com.nonexistent.app", + expected: "", + expectErr: false, + }, + { + name: "Empty identifier", + identifier: "", + expected: "", + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + extension, err := ExtensionForBundleIdentifier(tc.identifier) + + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + require.Equal(t, tc.expected, extension) + }) + } +}