Back-end fixes for FMA (#22742)

for https://github.com/fleetdm/fleet/issues/22733,
https://github.com/fleetdm/fleet/issues/22734 and
https://github.com/fleetdm/fleet/issues/22735

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Roberto Dip 2024-10-08 12:49:11 -03:00 committed by GitHub
parent 2b651a9e01
commit c4c8efb5b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 103 additions and 15 deletions

View file

@ -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,

View file

@ -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

View file

@ -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)
})
}
}