Update software title names on FMA sync and upload (#42647)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41710 
Updates (only) macOS software title names on FMA catalog sync.
Updates software title names on installer upload for Windows FMAs with
an upgrade code.

# 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/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually
This commit is contained in:
Jonathan Katz 2026-03-30 15:59:19 -04:00 committed by GitHub
parent 8d63bf2bbe
commit 13f94af560
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 125 additions and 7 deletions

View file

@ -0,0 +1 @@
- Updated Fleet-maintained apps to overwrite software title names on sync and when adding an FMA installer.

View file

@ -128,12 +128,13 @@ func (svc *Service) AddFleetMaintainedApp(
maintainedAppID = nil // don't set app as maintained if scripts have been modified
}
// For platforms other than macOS, installer name has to match what we see in software inventory,
// so we have the UniqueIdentifier field to indicate what that should be (independent of the name we
// display when listing the FMA). For macOS, unique identifier is bundle name, and we use bundle
// identifier to link installers with inventory, so we set the name to the FMA's display name instead.
// For Windows, installer name has to match what we see in software inventory, so we have the
// UniqueIdentifier field to indicate what that should be (independent of the FMA's display name).
// If we have an upgrade code to match inventory with, we can set the installer name to the FMA's
// display name instead. For macOS, unique identifier is bundle name, and we use bundle identifier
// to link installers with inventory, so we set the name to the FMA's display name instead.
appName := app.UniqueIdentifier
if app.Platform == "darwin" || appName == "" {
if app.Platform == "darwin" || appName == "" || app.UpgradeCode != "" {
appName = app.Name
}

View file

@ -3,6 +3,7 @@ package mysql
import (
"context"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
maintained_apps "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps"
@ -25,6 +26,7 @@ func TestMaintainedApps(t *testing.T) {
{"SyncAndRemoveApps", testSyncAndRemoveApps},
{"GetMaintainedAppBySlug", testGetMaintainedAppBySlug},
{"ListAvailableAppsWindows", testListAvailableAppsWindows},
{"SoftwareTitleRenamingWindows", testSoftwareTitleRenamingWindows},
{"GetFMANamesByIdentifier", testGetFMANamesByIdentifier},
{"UpsertMaintainedAppUpdatesSoftware", testUpsertMaintainedAppUpdatesSoftware},
}
@ -632,6 +634,87 @@ func testListAvailableAppsWindows(t *testing.T, ds *Datastore) {
require.Nil(t, apps[1].TitleID)
}
func testSoftwareTitleRenamingWindows(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
software1 := []fleet.Software{
{Name: "Goodbye 1.00 (x64)", Version: "1.0", Source: "programs"},
{Name: "Hello 1.00 (x64)", Version: "1.0", Source: "programs", UpgradeCode: ptr.String("{123456}")},
}
_, err := ds.UpdateHostSoftware(ctx, host1.ID, software1)
require.NoError(t, err)
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
opts := fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{OrderKey: "name"}}
sw, _, _, err := ds.ListSoftwareTitles(ctx, opts, fleet.TeamFilter{})
require.NoError(t, err)
require.Len(t, sw, 2)
require.Equal(t, "Goodbye 1.00 (x64)", sw[0].Name)
require.Equal(t, "Hello 1.00 (x64)", sw[1].Name)
maintained3, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "goodbye",
Slug: "goodbye/windows",
Platform: "windows",
UniqueIdentifier: "Goodbye 1.00 (x64)",
})
require.NoError(t, err)
maintained4, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "Hello",
Slug: "hello/windows",
Platform: "windows",
UniqueIdentifier: "Hello 1.00 (x64)",
})
require.NoError(t, err)
sw, _, _, err = ds.ListSoftwareTitles(ctx, opts, fleet.TeamFilter{})
require.NoError(t, err)
require.Len(t, sw, 2)
require.Equal(t, "Goodbye 1.00 (x64)", sw[0].Name)
require.Equal(t, "Hello 1.00 (x64)", sw[1].Name)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
Title: "Goodbye 1.00 (x64)",
Source: "programs",
StorageID: "storageid1",
Filename: "goodbye.msi",
Extension: "msi",
Platform: "windows",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
FleetMaintainedAppID: new(maintained3.ID),
})
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
Title: "Hello",
UpgradeCode: "{123456}",
Source: "programs",
StorageID: "storageid2",
Filename: "hello.msi",
Extension: "msi",
Platform: "windows",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
FleetMaintainedAppID: new(maintained4.ID),
})
require.NoError(t, err)
// After uploading installers, Goodbye 1.00 (x64) has no upgrade code so it
// keeps its name, and Hello 1.00 (x64) updates to just Hello as it has one.
sw, _, _, err = ds.ListSoftwareTitles(ctx, opts, fleet.TeamFilter{})
require.NoError(t, err)
require.Len(t, sw, 2)
require.Equal(t, "Goodbye 1.00 (x64)", sw[0].Name)
require.Equal(t, "Hello", sw[1].Name)
}
func testUpsertMaintainedAppUpdatesSoftware(t *testing.T, ds *Datastore) {
ctx := t.Context()

View file

@ -553,6 +553,14 @@ func (ds *Datastore) getOrGenerateSoftwareInstallerTitleID(ctx context.Context,
if payload.Source == "programs" && payload.UpgradeCode != "" {
updateStmt := `UPDATE software_titles SET upgrade_code = ? WHERE id = ?`
updateArgs := []any{payload.UpgradeCode, titleID}
// Update the software title name if this is a Windows FMA with an upgrade code. We already update
// software titles with macOS FMA names on FMA catalog sync, so we only do Windows here.
if payload.FleetMaintainedAppID != nil {
updateStmt = `UPDATE software_titles SET name = ?, upgrade_code = ? WHERE id = ?`
updateArgs = []any{payload.Title, payload.UpgradeCode, titleID}
}
_, err := ds.writer(ctx).ExecContext(ctx, updateStmt, updateArgs...)
if err != nil {
return 0, err

View file

@ -2496,6 +2496,7 @@ func testGetOrGenerateSoftwareInstallerTitleID(t *testing.T, ds *Datastore) {
{Name: "Existing Title", Version: "v0.0.2", Source: "apps", BundleIdentifier: "existing.title"},
{Name: "Existing Title", Version: "0.0.3", Source: "apps", BundleIdentifier: "existing.title"},
{Name: "Existing Title Without Bundle", Version: "0.0.3", Source: "apps"},
{Name: "FMA Old Name", Version: "1.0", Source: "apps", BundleIdentifier: "com.fma"},
}
software3 := []fleet.Software{
{Name: "Win Title 1", Version: "11.0", Source: "programs", UpgradeCode: ptr.String("")},
@ -2503,6 +2504,7 @@ func testGetOrGenerateSoftwareInstallerTitleID(t *testing.T, ds *Datastore) {
{Name: "Win Title 3", Version: "11.0", Source: "programs", UpgradeCode: ptr.String("")},
{Name: "Win Title 4", Version: "11.0", Source: "programs", UpgradeCode: ptr.String("12345")},
{Name: "Win Title 5", Version: "11.0", Source: "programs", UpgradeCode: ptr.String("ABCDEF")},
{Name: "Win Title 6", Version: "11.0", Source: "programs", UpgradeCode: ptr.String("GHIJKL")},
}
_, err := ds.UpdateHostSoftware(ctx, host1.ID, software1)
@ -2591,7 +2593,18 @@ func testGetOrGenerateSoftwareInstallerTitleID(t *testing.T, ds *Datastore) {
expectedSource: "ios_apps",
},
{
name: "installer: no upgrade code, existing title: same name, no upgrade code",
name: "don't rename macos FMA titles",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "FMA New Name",
Source: "apps",
BundleIdentifier: "com.fma",
FleetMaintainedAppID: ptr.Uint(2),
},
expectedName: "FMA Old Name",
expectedSource: "apps",
},
{
name: "installer: no upgrade code, existing title: same name, no upgrade code",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "Win Title 1",
Source: "programs",
@ -2601,7 +2614,7 @@ func testGetOrGenerateSoftwareInstallerTitleID(t *testing.T, ds *Datastore) {
expectedUpgradeCode: ptr.String(""),
},
{
name: "installer: no upgrade code, existing title: same name, has upgrade code",
name: "installer: no upgrade code, existing title: same name, has upgrade code",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "Win Title 2",
Source: "programs",
@ -2643,6 +2656,18 @@ func testGetOrGenerateSoftwareInstallerTitleID(t *testing.T, ds *Datastore) {
expectedSource: "programs",
expectedUpgradeCode: ptr.String("ABCDEF"),
},
{
name: "installer: has upgrade code and FMA, existing title: different name, same upgrade code",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "New Name",
Source: "programs",
UpgradeCode: "GHIJKL",
FleetMaintainedAppID: ptr.Uint(1), // FMAs should overwrite name for upgrade code
},
expectedName: "New Name",
expectedSource: "programs",
expectedUpgradeCode: ptr.String("GHIJKL"),
},
}
for _, tt := range tests {