Detect UpgradeCodes when adding Windows FMA software, and persist them when the user adds that software; Fix recently introduced issue with list host software (#35876)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #35724 

- Fixes issues 1 and 2 of the referenced bug ticket
- Also fixes [this
issue](https://github.com/fleetdm/fleet/pull/35739/files#r2548349172)

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:
- [x] Alerted the release DRI if additional load testing is needed

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added upgrade code support to Windows maintained applications for
improved MSI installer tracking and management.

* **Documentation**
* Updated Windows onboarding instructions with more precise manifest
path guidance and concrete command examples for the maintained-apps
generator.

* **Tests**
* Added comprehensive test coverage for upgrade code association with
maintained applications.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
jacobshandling
2025-11-26 17:00:03 -08:00
committed by GitHub
parent 5a10ff78f0
commit 3ca1ff4754
12 changed files with 234 additions and 19 deletions
+4 -2
View File
@@ -49,7 +49,7 @@
## Adding a new app (Windows)
1. Find the Winget PackageIdentifier in the [winget-pkgs repo](https://github.com/microsoft/winget-pkgs).
1. Find the Winget `PackageIdentifier` in the relevant [winget-pkgs repo manifest](https://github.com/microsoft/winget-pkgs/tree/master/manifests).
2. Get the unique identifier that Fleet will use for matching the software with software inventory:
- On a test Windows host, install the app manually, then run the following PowerShell script that correlates to the defined `installer_scope`:
@@ -73,7 +73,9 @@ If the `unique_identifier` doesn't match the `DisplayName`, then Fleet will inco
}
```
4. Run the following command from the root of the Fleet repo to generate the app's output data:
4. Run `go run cmd/maintained-apps/main.go --slug="<app-name>/windows" --debug` from the root of the
Fleet repo to generate the app's output data, replacing `<app-name>` with your app's name, for example:
```bash
go run cmd/maintained-apps/main.go --slug="box-drive/windows" --debug
@@ -250,8 +250,8 @@ func (i *wingetIngester) ingestOne(ctx context.Context, input inputApp) (*mainta
}
}
var upgradeCode string
if (input.InstallerType == installerTypeMSI || input.UninstallType == installerTypeMSI) && input.InstallerScope == machineScope {
var upgradeCode string
for _, fe := range m.AppsAndFeaturesEntries {
if fe.UpgradeCode != "" {
upgradeCode = fe.UpgradeCode
@@ -288,6 +288,10 @@ func (i *wingetIngester) ingestOne(ctx context.Context, input inputApp) (*mainta
}
productCode = strings.Split(productCode, ".")[0]
if upgradeCode != "" {
out.UpgradeCode = upgradeCode
}
out.Name = input.Name
out.Slug = input.Slug
out.InstallerURL = selectedInstaller.InstallerURL
@@ -306,6 +310,8 @@ func (i *wingetIngester) ingestOne(ctx context.Context, input inputApp) (*mainta
if input.UniqueIdentifier != "" {
name = input.UniqueIdentifier
}
// TODO - consider UpgradeCode here?
existsTemplate := "SELECT 1 FROM programs WHERE name = '%s' AND publisher = '%s';"
if input.FuzzyMatchName {
existsTemplate = "SELECT 1 FROM programs WHERE name LIKE '%s %%' AND publisher = '%s';"
+1
View File
@@ -35,6 +35,7 @@ type FMAManifestApp struct {
Name string `json:"-"`
DefaultCategories []string `json:"default_categories"`
Frozen bool `json:"-"`
UpgradeCode string `json:"upgrade_code,omitempty"`
}
func (a *FMAManifestApp) Platform() string {
@@ -11,7 +11,8 @@
"sha256": "96c9631470d3f7eccd3de8ee4ee3b0c8d19adfe3e29ed4e2f9e040f1c7ad8faf",
"default_categories": [
"Productivity"
]
],
"upgrade_code": "{46AF5B38-D258-487A-92BD-792911248CCD}"
}
],
"refs": {
@@ -11,7 +11,8 @@
"sha256": "ac519bcc2937d93d67d31497d72a6262801d30cea98894a9584c0b92d0635ba2",
"default_categories": [
"Productivity"
]
],
"upgrade_code": "{1BF42825-7B65-4CA9-AFFF-B7B5E1CE27B4}"
}
],
"refs": {
+2 -1
View File
@@ -11,7 +11,8 @@
"sha256": "53e1eca704589e41a19c061bfe1bb7cd917ef230c21eb9fdd629d963f58926db",
"default_categories": [
"Communication"
]
],
"upgrade_code": "{C819B794-A45C-4F27-9860-0C86492A52CC}"
}
],
"refs": {
+1 -1
View File
@@ -74,7 +74,6 @@ func (svc *Service) AddFleetMaintainedApp(
if v := os.Getenv("FLEET_DEV_MAINTAINED_APPS_INSTALLER_TIMEOUT"); v != "" {
timeout, _ = time.ParseDuration(v)
}
client := fleethttp.NewClient(fleethttp.WithTimeout(timeout))
installerTFR, filename, err := maintained_apps.DownloadInstaller(ctx, app.InstallerURL, client)
if err != nil {
@@ -149,6 +148,7 @@ func (svc *Service) AddFleetMaintainedApp(
Source: app.Source(),
Extension: extension,
BundleIdentifier: app.BundleIdentifier(),
UpgradeCode: app.UpgradeCode,
StorageID: app.SHA256,
FleetMaintainedAppID: maintainedAppID,
PreInstallQuery: preInstallQuery,
+9 -10
View File
@@ -896,7 +896,8 @@ func (ds *Datastore) preInsertSoftwareInventory(
newTitlesNeeded := make(map[string]fleet.SoftwareTitle)
for checksum, sw := range batchSoftware {
if _, ok := incomingChecksumsToExistingTitleSummaries[checksum]; !ok {
titleName := sw.Name
// there is not an existing software title corresponding to this incoming software version
newTitleName := sw.Name
if sw.BundleIdentifier != "" {
key := titleKey{
bundleID: sw.BundleIdentifier,
@@ -904,27 +905,27 @@ func (ds *Datastore) preInsertSoftwareInventory(
extensionFor: sw.ExtensionFor,
}
if computedName, exists := bestTitleNames[key]; exists {
titleName = computedName
newTitleName = computedName
}
}
st := fleet.SoftwareTitle{
Name: titleName,
newTitle := fleet.SoftwareTitle{
Name: newTitleName,
Source: sw.Source,
ExtensionFor: sw.ExtensionFor,
IsKernel: sw.IsKernel,
}
if sw.BundleIdentifier != "" {
st.BundleIdentifier = ptr.String(sw.BundleIdentifier)
newTitle.BundleIdentifier = ptr.String(sw.BundleIdentifier)
}
if sw.ApplicationID != nil && *sw.ApplicationID != "" {
st.ApplicationID = sw.ApplicationID
newTitle.ApplicationID = sw.ApplicationID
}
if sw.UpgradeCode != nil {
// intentionally write both empty and non-empty strings as upgrade codes
st.UpgradeCode = sw.UpgradeCode
newTitle.UpgradeCode = sw.UpgradeCode
}
newTitlesNeeded[checksum] = st
newTitlesNeeded[checksum] = newTitle
}
}
@@ -2915,7 +2916,6 @@ func (ds *Datastore) ListCVEs(ctx context.Context, maxAge time.Duration) ([]flee
return result, nil
}
// TODO(jacob) SoftwareUpgradeCode ? SoftwareUpgradeCodeList ?
type hostSoftware struct {
fleet.HostSoftwareWithInstaller
@@ -2931,7 +2931,6 @@ type hostSoftware struct {
SoftwareID *uint `db:"software_id"`
SoftwareSource *string `db:"software_source"`
SoftwareExtensionFor *string `db:"software_extension_for"`
UpgradeCode *string `db:"upgrade_code"`
InstallerID *uint `db:"installer_id"`
PackageSelfService *bool `db:"package_self_service"`
PackageName *string `db:"package_name"`
@@ -507,10 +507,18 @@ func (ds *Datastore) getOrGenerateSoftwareInstallerTitleID(ctx context.Context,
insertStmt := `INSERT INTO software_titles (name, source, extension_for) VALUES (?, ?, '')`
insertArgs := []any{payload.Title, payload.Source}
// upgrade_code should be NULL for non-Windows software, empty or non-empty string for Windows
// software
if payload.Source == "programs" {
insertStmt = `INSERT INTO software_titles (name, source, extension_for, upgrade_code) VALUES (?, ?, '', ?)`
insertArgs = []any{payload.Title, payload.Source, payload.UpgradeCode}
}
if payload.BundleIdentifier != "" {
// match by bundle identifier first, or standard matching if we don't have a bundle identifier match
selectStmt = `SELECT id FROM software_titles WHERE bundle_identifier = ? OR (name = ? AND source = ? AND extension_for = '') ORDER BY bundle_identifier = ? DESC LIMIT 1`
selectArgs = []any{payload.BundleIdentifier, payload.Title, payload.Source, payload.BundleIdentifier}
// omit upgrade_code, since title.upgrade_code should be NULL for non-Windows software
insertStmt = `INSERT INTO software_titles (name, source, bundle_identifier, extension_for) VALUES (?, ?, ?, '')`
insertArgs = append(insertArgs, payload.BundleIdentifier)
}
@@ -532,6 +540,9 @@ func (ds *Datastore) getOrGenerateSoftwareInstallerTitleID(ctx context.Context,
}
func (ds *Datastore) addSoftwareTitleToMatchingSoftware(ctx context.Context, titleID uint, payload *fleet.UploadSoftwareInstallerPayload) error {
// not considering upgrade_code, so inventory software will match this Title by this clause, even
// if upgrade_code doesn't match - TODO: enforce matching upgrade_code between software and
// incoming title?
whereClause := "WHERE (s.name, s.source, s.extension_for) = (?, ?, '')"
whereArgs := []any{payload.Title, payload.Source}
if payload.BundleIdentifier != "" {
+2 -2
View File
@@ -2,8 +2,7 @@ package fleet
import "net/http"
// MaintainedApp represents an app in the Fleet library of maintained apps,
// as stored in the fleet_library_apps table.
// MaintainedApp represents an app in the Fleet library of maintained apps
type MaintainedApp struct {
ID uint `json:"id" db:"id"`
Name string `json:"name" db:"name"`
@@ -18,6 +17,7 @@ type MaintainedApp struct {
UninstallScript string `json:"uninstall_script,omitempty"`
AutomaticInstallQuery string `json:"-"`
Categories []string `json:"categories"`
UpgradeCode string `json:"upgrade_code,omitempty"`
}
func (s *MaintainedApp) Source() string {
+1
View File
@@ -153,6 +153,7 @@ func Hydrate(ctx context.Context, app *fleet.MaintainedApp) (*fleet.MaintainedAp
app.UninstallScript = manifest.Refs[manifest.Versions[0].UninstallScriptRef]
app.AutomaticInstallQuery = manifest.Versions[0].Queries.Exists
app.Categories = manifest.Versions[0].DefaultCategories
app.UpgradeCode = manifest.Versions[0].UpgradeCode
return app, nil
}
@@ -53,6 +53,7 @@ import (
"github.com/fleetdm/fleet/v4/server/pubsub"
commonCalendar "github.com/fleetdm/fleet/v4/server/service/calendar"
"github.com/fleetdm/fleet/v4/server/service/conditional_access_microsoft_proxy"
"github.com/fleetdm/fleet/v4/server/service/osquery_utils"
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
"github.com/fleetdm/fleet/v4/server/service/schedule"
"github.com/fleetdm/fleet/v4/server/test"
@@ -18179,6 +18180,197 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() {
require.Contains(t, extractServerErrorText(r.Body), `Only one of "labels_include_any" or "labels_exclude_any" can be included`)
}
func (s *integrationEnterpriseTestSuite) TestUpgradeCodesFromMaintainedApps() {
// Specifically test that `upgrade_code` is correctly associated with host software that has
// first been added via FMA. For a more robust handling of possible error scenarios when adding
// Maintained Apps, see `TestMaintainedApps`
t := s.T()
ctx := context.Background()
warpUpgradeCode := "{1BF42825-7B65-4CA9-AFFF-B7B5E1CE27B4}"
ac, err := s.ds.AppConfig(ctx)
require.NoError(s.T(), err)
ac.Features.EnableSoftwareInventory = true
err = s.ds.SaveAppConfig(context.Background(), ac)
require.NoError(s.T(), err)
time.Sleep(2 * time.Second) // Wait for the app config cache to clear
installerBytes := []byte("abc")
installerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/badinstaller":
_, _ = w.Write([]byte("badinstaller"))
case "/timeout":
time.Sleep(3 * time.Second)
_, _ = w.Write([]byte("timeout"))
default:
_, _ = w.Write(installerBytes)
}
}))
defer installerServer.Close()
// Mock server to serve manifest with no_check
manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var versions []*ma.FMAManifestApp
versions = append(versions, &ma.FMAManifestApp{
Version: "25.9.558.0",
Queries: ma.FMAQueries{
Exists: "SELECT 1 FROM osquery_info;",
},
InstallerURL: installerServer.URL + "/installer.msi",
InstallScriptRef: "foobaz",
UninstallScriptRef: "foobaz",
SHA256: "no_check",
UpgradeCode: warpUpgradeCode,
})
manifest := ma.FMAManifestFile{
Versions: versions,
Refs: map[string]string{
"foobaz": "Hello World!",
},
}
err := json.NewEncoder(w).Encode(manifest)
require.NoError(t, err)
}))
t.Cleanup(manifestServer.Close)
mockTransport := &mockRoundTripper{
mockServer: manifestServer.URL,
origBaseURL: "https://raw.githubusercontent.com",
next: http.DefaultTransport,
}
http.DefaultTransport = mockTransport
// Insert the list of maintained apps
maintained_apps.SyncApps(t, s.ds)
// verify WARP is in `fleet_maintained_apps` table but not in `software_installers`
var warpFmaId uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &warpFmaId, "SELECT id FROM fleet_maintained_apps WHERE name = 'Cloudflare WARP' AND platform = 'windows'")
})
require.NotNil(t, warpFmaId)
var count uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &count, "SELECT COUNT(*) FROM software_installers")
})
require.Zero(t, count)
// Create a team
var newTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp)
team := newTeamResp.Team
// Add WARP for Windows
var warpAppId uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &warpAppId, "SELECT id FROM fleet_maintained_apps WHERE name = 'Cloudflare WARP' and platform = 'windows'")
})
var addMAresp addFleetMaintainedAppResponse
req := &addFleetMaintainedAppRequest{
AppID: warpAppId,
TeamID: &team.ID,
SelfService: true,
PreInstallQuery: "SELECT 1",
PostInstallScript: "echo done",
}
s.DoJSON("POST", "/api/latest/fleet/software/fleet_maintained_apps", req, http.StatusOK, &addMAresp)
require.Nil(t, addMAresp.Err)
// Verify WARP is now in `software_installers`, a `software_tiles` row has been created, and they
// are associated
var warpInstaller fleet.SoftwareInstaller
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &warpInstaller, "SELECT id, title_id, upgrade_code, filename FROM software_installers WHERE upgrade_code = ?", warpUpgradeCode)
})
require.NotNil(t, warpInstaller)
require.NotNil(t, warpInstaller.Name)
var lSTResp listSoftwareTitlesResponse
s.DoJSON(
"GET", "/api/latest/fleet/software/titles",
listSoftwareTitlesRequest{},
http.StatusOK, &lSTResp,
"per_page", "1",
"order_key", "name",
"order_direction", "desc",
"available_for_install", "true",
"team_id", fmt.Sprintf("%d", team.ID),
)
title := lSTResp.SoftwareTitles[0]
require.Equal(t, *warpInstaller.TitleID, title.ID)
require.Equal(t, warpUpgradeCode, *title.UpgradeCode)
// Create a Windows host on the team
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name()),
NodeKey: ptr.String(t.Name()),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
Platform: "windows",
TeamID: &team.ID,
})
require.NoError(t, err)
// mock osquery ingesting matching software from the host
ac, err = s.ds.AppConfig(context.Background())
require.NoError(t, err)
detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, ac, &ac.Features, osquery_utils.Integrations{}, nil)
rows := []map[string]string{
{
"name": "Cloudflare WARP",
"version": "25.9.558.0",
"source": "programs",
"vendor": "Cloudflare, Inc.",
"upgrade_code": warpUpgradeCode,
},
}
err = detailQueries["software_windows"].DirectIngestFunc(
context.Background(),
kitlog.NewNopLogger(),
&fleet.Host{ID: host.ID},
s.ds,
rows,
)
require.NoError(t, err)
// confirm software row now in the database and `upgrade_code`s match for that row, the associated
// `software_titles` row, and the associated `software_installers` row
var swTitleId *uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &swTitleId, "SELECT title_id FROM software WHERE upgrade_code = ?", warpUpgradeCode)
})
require.Equal(t, title.ID, *swTitleId)
require.Equal(t, *warpInstaller.TitleID, *swTitleId)
// GET host software endpoint, confirm upgrade_code is present
var hSWRes getHostSoftwareResponse
s.DoJSON(
"GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID),
getHostSoftwareRequest{},
http.StatusOK, &hSWRes,
)
// should only be the single software and software title in the database
require.Equal(t, hSWRes.Count, 1)
require.Equal(t, len(hSWRes.Software), 1)
sw0 := hSWRes.Software[0]
require.Equal(t, warpUpgradeCode, *sw0.UpgradeCode)
}
func (s *integrationEnterpriseTestSuite) TestWindowsMigrateMDMNotEnabled() {
t := s.T()