Use FMA names for macOS software (#42221)

This commit is contained in:
Tim Lee
2026-03-30 10:41:37 -06:00
committed by GitHub
parent 32f1c2026c
commit e98b0f480d
11 changed files with 610 additions and 34 deletions
+79 -24
View File
@@ -65,6 +65,12 @@ const (
// changes, it'll linger for this amount of time. The curent
// implementation assumes infrequent asset changes.
defaultMDMConfigAssetExpiration = 15 * time.Minute
// FMA names cache stores a map of unique_identifier -> canonical name
// for Fleet-maintained apps. Used during software ingestion to override
// osquery-reported names with the FMA canonical name.
fmaNamesByIdentifierKey = "FMANamesByIdentifier"
defaultFMANamesByIdentifierExpiration = 5 * time.Minute
)
// cloneCache wraps the in memory cache with one that clones items before returning them.
@@ -114,17 +120,18 @@ type cachedMysql struct {
c *cloneCache
appConfigExp time.Duration
packsExp time.Duration
scheduledQueriesExp time.Duration
teamAgentOptionsExp time.Duration
teamFeaturesExp time.Duration
teamMDMConfigExp time.Duration
defaultTeamConfigExp time.Duration
queryByNameExp time.Duration
queryResultsCountExp time.Duration
yaraRuleByNameExp time.Duration
mdmConfigAssetExp time.Duration
appConfigExp time.Duration
packsExp time.Duration
scheduledQueriesExp time.Duration
teamAgentOptionsExp time.Duration
teamFeaturesExp time.Duration
teamMDMConfigExp time.Duration
defaultTeamConfigExp time.Duration
queryByNameExp time.Duration
queryResultsCountExp time.Duration
yaraRuleByNameExp time.Duration
mdmConfigAssetExp time.Duration
fmaNamesByIdentifierExp time.Duration
}
type Option func(*cachedMysql)
@@ -195,21 +202,28 @@ func WithDefaultTeamConfigExpiration(d time.Duration) Option {
}
}
func WithFMANamesByIdentifierExpiration(d time.Duration) Option {
return func(o *cachedMysql) {
o.fmaNamesByIdentifierExp = d
}
}
func New(ds fleet.Datastore, opts ...Option) fleet.Datastore {
c := &cachedMysql{
Datastore: ds,
c: &cloneCache{cache.New(5*time.Minute, 10*time.Minute)},
appConfigExp: defaultAppConfigExpiration,
packsExp: defaultPacksExpiration,
scheduledQueriesExp: defaultScheduledQueriesExpiration,
teamAgentOptionsExp: defaultTeamAgentOptionsExpiration,
teamFeaturesExp: defaultTeamFeaturesExpiration,
teamMDMConfigExp: defaultTeamMDMConfigExpiration,
defaultTeamConfigExp: defaultDefaultTeamConfigExpiration,
queryByNameExp: defaultQueryByNameExpiration,
queryResultsCountExp: defaultQueryResultsCountExpiration,
yaraRuleByNameExp: defaultYaraRuleByNameExpiration,
mdmConfigAssetExp: defaultMDMConfigAssetExpiration,
Datastore: ds,
c: &cloneCache{cache.New(5*time.Minute, 10*time.Minute)},
appConfigExp: defaultAppConfigExpiration,
packsExp: defaultPacksExpiration,
scheduledQueriesExp: defaultScheduledQueriesExpiration,
teamAgentOptionsExp: defaultTeamAgentOptionsExpiration,
teamFeaturesExp: defaultTeamFeaturesExpiration,
teamMDMConfigExp: defaultTeamMDMConfigExpiration,
defaultTeamConfigExp: defaultDefaultTeamConfigExpiration,
queryByNameExp: defaultQueryByNameExpiration,
queryResultsCountExp: defaultQueryResultsCountExpiration,
yaraRuleByNameExp: defaultYaraRuleByNameExpiration,
mdmConfigAssetExp: defaultMDMConfigAssetExpiration,
fmaNamesByIdentifierExp: defaultFMANamesByIdentifierExpiration,
}
for _, fn := range opts {
fn(c)
@@ -541,3 +555,44 @@ func (ds *cachedMysql) ApplyYaraRules(ctx context.Context, rules []fleet.YaraRul
return nil
}
func (ds *cachedMysql) GetFMANamesByIdentifier(ctx context.Context) (map[string]string, error) {
if x, found := ds.c.Get(ctx, fmaNamesByIdentifierKey); found {
if names, ok := x.(fmaNameMap); ok {
return names, nil
}
}
names, err := ds.Datastore.GetFMANamesByIdentifier(ctx)
if err != nil {
return nil, err
}
ds.c.Set(ctx, fmaNamesByIdentifierKey, fmaNameMap(names), ds.fmaNamesByIdentifierExp)
return names, nil
}
func (ds *cachedMysql) UpsertMaintainedApp(ctx context.Context, app *fleet.MaintainedApp) (*fleet.MaintainedApp, error) {
result, err := ds.Datastore.UpsertMaintainedApp(ctx, app)
if err != nil {
return nil, err
}
// Invalidate the FMA names cache since an app was added/updated
ds.c.Delete(fmaNamesByIdentifierKey)
return result, nil
}
func (ds *cachedMysql) ClearRemovedFleetMaintainedApps(ctx context.Context, slugsToKeep []string) error {
err := ds.Datastore.ClearRemovedFleetMaintainedApps(ctx, slugsToKeep)
if err != nil {
return err
}
// Invalidate the FMA names cache since apps may have been removed
ds.c.Delete(fmaNamesByIdentifierKey)
return nil
}
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"maps"
"testing"
"time"
@@ -981,3 +982,80 @@ func TestCachedYaraRules(t *testing.T) {
require.Same(t, testRule1, rule1Expired)
require.True(t, mockedDS.YaraRuleByNameFuncInvoked) // from DB after expiration
}
func TestCachedFMANamesByIdentifier(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithFMANamesByIdentifierExpiration(100*time.Millisecond))
fmaNames := map[string]string{
"com.microsoft.VSCode": "Microsoft Visual Studio Code",
"com.1password.1password": "1Password",
}
mockedDS.GetFMANamesByIdentifierFunc = func(ctx context.Context) (map[string]string, error) {
// Return a copy to avoid mutation
result := make(map[string]string, len(fmaNames))
maps.Copy(result, fmaNames)
return result, nil
}
mockedDS.UpsertMaintainedAppFunc = func(ctx context.Context, app *fleet.MaintainedApp) (*fleet.MaintainedApp, error) {
return app, nil
}
// Test 1: Initial call hits the DB
names, err := ds.GetFMANamesByIdentifier(context.Background())
require.NoError(t, err)
require.Len(t, names, 2)
require.Equal(t, "Microsoft Visual Studio Code", names["com.microsoft.VSCode"])
require.Equal(t, "1Password", names["com.1password.1password"])
require.True(t, mockedDS.GetFMANamesByIdentifierFuncInvoked)
mockedDS.GetFMANamesByIdentifierFuncInvoked = false
// Test 2: Second call uses cache
names2, err := ds.GetFMANamesByIdentifier(context.Background())
require.NoError(t, err)
require.Len(t, names2, 2)
require.False(t, mockedDS.GetFMANamesByIdentifierFuncInvoked) // from cache
// Test 3: Modifying returned map doesn't affect cache
names2["com.microsoft.VSCode"] = "Modified"
names3, err := ds.GetFMANamesByIdentifier(context.Background())
require.NoError(t, err)
require.Equal(t, "Microsoft Visual Studio Code", names3["com.microsoft.VSCode"]) // still original
// Test 4: UpsertMaintainedApp invalidates cache
_, err = ds.UpsertMaintainedApp(context.Background(), &fleet.MaintainedApp{
Name: "New App",
Slug: "new-app/darwin",
Platform: "darwin",
UniqueIdentifier: "com.new.app",
})
require.NoError(t, err)
require.True(t, mockedDS.UpsertMaintainedAppFuncInvoked)
// Update mock to return new data
fmaNames["com.new.app"] = "New App"
// Next call should hit DB again since cache was invalidated
names4, err := ds.GetFMANamesByIdentifier(context.Background())
require.NoError(t, err)
require.Len(t, names4, 3)
require.Equal(t, "New App", names4["com.new.app"])
require.True(t, mockedDS.GetFMANamesByIdentifierFuncInvoked)
mockedDS.GetFMANamesByIdentifierFuncInvoked = false
// Test 5: Cache expiration
time.Sleep(200 * time.Millisecond)
// Update mock to return different data
fmaNames["com.microsoft.VSCode"] = "VS Code Updated"
// This call should get from DB again since cache expired
names5, err := ds.GetFMANamesByIdentifier(context.Background())
require.NoError(t, err)
require.Equal(t, "VS Code Updated", names5["com.microsoft.VSCode"])
require.True(t, mockedDS.GetFMANamesByIdentifierFuncInvoked)
}
+14
View File
@@ -2,6 +2,7 @@ package cached_mysql
import (
"encoding/json"
"maps"
"github.com/fleetdm/fleet/v4/server/fleet"
)
@@ -39,3 +40,16 @@ type integer int
func (i integer) Clone() (fleet.Cloner, error) {
return i, nil
}
// fmaNameMap is a map of unique_identifier -> canonical FMA name.
// Used during software ingestion to override osquery-reported names.
type fmaNameMap map[string]string
func (m fmaNameMap) Clone() (fleet.Cloner, error) {
if m == nil {
return fmaNameMap(nil), nil
}
clone := make(fmaNameMap, len(m))
maps.Copy(clone, m)
return clone, nil
}
+58
View File
@@ -34,6 +34,40 @@ ON DUPLICATE KEY UPDATE
}
id, _ := res.LastInsertId()
appID = uint(id) //nolint:gosec // dismiss G115
// For darwin apps, update existing software_titles and software entries
// to use the FMA canonical name. This ensures consistency when an FMA
// is added for software that was previously ingested with osquery-reported names.
//
// We only run these UPDATEs when the FMA was actually inserted or modified.
// MySQL's ON DUPLICATE KEY UPDATE returns RowsAffected:
// 0 = duplicate key, no changes (existing FMA with same values)
// 1 = new row inserted
// 2 = duplicate key, values changed
// Skip if RowsAffected == 0 since nothing changed.
rowsAffected, _ := res.RowsAffected()
if app.Platform == "darwin" && app.UniqueIdentifier != "" && rowsAffected > 0 {
_, err = tx.ExecContext(ctx, `
UPDATE software_titles
SET name = ?
WHERE bundle_identifier = ?
AND name != ?
`, app.Name, app.UniqueIdentifier, app.Name)
if err != nil {
return ctxerr.Wrap(ctx, err, "update software_titles names for FMA")
}
_, err = tx.ExecContext(ctx, `
UPDATE software
SET name = ?
WHERE bundle_identifier = ?
AND name != ?
`, app.Name, app.UniqueIdentifier, app.Name)
if err != nil {
return ctxerr.Wrap(ctx, err, "update software names for FMA")
}
}
return nil
})
if err != nil {
@@ -180,6 +214,30 @@ func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamI
return avail, meta, nil
}
func (ds *Datastore) GetFMANamesByIdentifier(ctx context.Context) (map[string]string, error) {
query := `SELECT unique_identifier, name FROM fleet_maintained_apps WHERE platform = 'darwin'`
rows, err := ds.reader(ctx).QueryContext(ctx, query)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "query FMA names by identifier")
}
defer rows.Close()
result := make(map[string]string)
for rows.Next() {
var identifier, name string
if err := rows.Scan(&identifier, &name); err != nil {
return nil, ctxerr.Wrap(ctx, err, "scan FMA name row")
}
result[identifier] = name
}
if err := rows.Err(); err != nil {
return nil, ctxerr.Wrap(ctx, err, "iterate FMA name rows")
}
return result, nil
}
func (ds *Datastore) ClearRemovedFleetMaintainedApps(ctx context.Context, slugsToKeep []string) error {
stmt := `DELETE FROM fleet_maintained_apps WHERE slug NOT IN (?)`
@@ -25,6 +25,8 @@ func TestMaintainedApps(t *testing.T) {
{"SyncAndRemoveApps", testSyncAndRemoveApps},
{"GetMaintainedAppBySlug", testGetMaintainedAppBySlug},
{"ListAvailableAppsWindows", testListAvailableAppsWindows},
{"GetFMANamesByIdentifier", testGetFMANamesByIdentifier},
{"UpsertMaintainedAppUpdatesSoftware", testUpsertMaintainedAppUpdatesSoftware},
}
for _, c := range cases {
@@ -629,3 +631,176 @@ func testListAvailableAppsWindows(t *testing.T, ds *Datastore) {
// the darwin app should not be matched by name
require.Nil(t, apps[1].TitleID)
}
func testUpsertMaintainedAppUpdatesSoftware(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Create a host to associate software with
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host",
Platform: "darwin",
OsqueryHostID: ptr.String("osquery-host-id"),
NodeKey: ptr.String("node-key"),
DetailUpdatedAt: ds.clock.Now(),
LabelUpdatedAt: ds.clock.Now(),
PolicyUpdatedAt: ds.clock.Now(),
SeenTime: ds.clock.Now(),
})
require.NoError(t, err)
// Create software entries with osquery-reported name ("Code" instead of "Microsoft Visual Studio Code")
software := []fleet.Software{
{
Name: "Code",
Version: "1.85.0",
Source: "apps",
BundleIdentifier: "com.microsoft.VSCode",
},
{
Name: "Code",
Version: "1.84.0",
Source: "apps",
BundleIdentifier: "com.microsoft.VSCode",
},
}
// Insert software using the normal ingestion path
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
// Verify the software and software_titles were created with the osquery name "Code"
var softwareNames []string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &softwareNames,
`SELECT name FROM software WHERE bundle_identifier = 'com.microsoft.VSCode' ORDER BY version`)
})
require.Len(t, softwareNames, 2)
require.Equal(t, "Code", softwareNames[0])
require.Equal(t, "Code", softwareNames[1])
var titleName string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &titleName,
`SELECT name FROM software_titles WHERE bundle_identifier = 'com.microsoft.VSCode'`)
})
require.Equal(t, "Code", titleName)
// Now upsert an FMA with the canonical name
_, err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "Microsoft Visual Studio Code",
Slug: "visual-studio-code/darwin",
Platform: "darwin",
UniqueIdentifier: "com.microsoft.VSCode",
})
require.NoError(t, err)
// Verify software entries were updated to use the FMA canonical name
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &softwareNames,
`SELECT name FROM software WHERE bundle_identifier = 'com.microsoft.VSCode' ORDER BY version`)
})
require.Len(t, softwareNames, 2)
require.Equal(t, "Microsoft Visual Studio Code", softwareNames[0])
require.Equal(t, "Microsoft Visual Studio Code", softwareNames[1])
// Verify software_titles was also updated
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &titleName,
`SELECT name FROM software_titles WHERE bundle_identifier = 'com.microsoft.VSCode'`)
})
require.Equal(t, "Microsoft Visual Studio Code", titleName)
// Verify upserting the same FMA again doesn't cause issues (idempotent)
_, err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "Microsoft Visual Studio Code",
Slug: "visual-studio-code/darwin",
Platform: "darwin",
UniqueIdentifier: "com.microsoft.VSCode",
})
require.NoError(t, err)
// Names should still be the FMA canonical name
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &softwareNames,
`SELECT name FROM software WHERE bundle_identifier = 'com.microsoft.VSCode' ORDER BY version`)
})
require.Len(t, softwareNames, 2)
require.Equal(t, "Microsoft Visual Studio Code", softwareNames[0])
require.Equal(t, "Microsoft Visual Studio Code", softwareNames[1])
// Verify Windows FMA does NOT update darwin software entries
// First create darwin software with a different bundle_id
software2 := []fleet.Software{
{
Name: "Some App",
Version: "1.0.0",
Source: "apps",
BundleIdentifier: "com.example.someapp",
},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, append(software, software2...))
require.NoError(t, err)
// Upsert a Windows FMA - should not affect darwin software
_, err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "Some App Windows",
Slug: "some-app/windows",
Platform: "windows",
UniqueIdentifier: "com.example.someapp", // Same identifier but different platform
})
require.NoError(t, err)
// The darwin software should NOT have been renamed
var someAppName string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &someAppName,
`SELECT name FROM software WHERE bundle_identifier = 'com.example.someapp'`)
})
require.Equal(t, "Some App", someAppName)
}
func testGetFMANamesByIdentifier(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Initially empty
names, err := ds.GetFMANamesByIdentifier(ctx)
require.NoError(t, err)
require.Empty(t, names)
// Add some darwin FMAs
_, err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "Microsoft Visual Studio Code",
Slug: "visual-studio-code/darwin",
Platform: "darwin",
UniqueIdentifier: "com.microsoft.VSCode",
})
require.NoError(t, err)
_, err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "1Password",
Slug: "1password/darwin",
Platform: "darwin",
UniqueIdentifier: "com.1password.1password",
})
require.NoError(t, err)
// Add a Windows FMA - should NOT be returned (only darwin)
_, err = ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "Microsoft Visual Studio Code",
Slug: "visual-studio-code/windows",
Platform: "windows",
UniqueIdentifier: "Microsoft Visual Studio Code",
})
require.NoError(t, err)
// Get FMA names - should only return darwin apps
names, err = ds.GetFMANamesByIdentifier(ctx)
require.NoError(t, err)
require.Len(t, names, 2)
require.Equal(t, "Microsoft Visual Studio Code", names["com.microsoft.VSCode"])
require.Equal(t, "1Password", names["com.1password.1password"])
// Windows identifier should not be present
_, ok := names["Microsoft Visual Studio Code"]
require.False(t, ok)
}
@@ -0,0 +1,55 @@
package tables
import "database/sql"
func init() {
MigrationClient.AddMigration(Up_20260326210603, Down_20260326210603)
}
func Up_20260326210603(tx *sql.Tx) error {
// Update software_titles to use FMA canonical names where there's a matching
// bundle_identifier. This fixes existing titles that were created with
// osquery-reported names (e.g., "Code") instead of the FMA name
// (e.g., "Microsoft Visual Studio Code").
//
// Note: We intentionally do NOT add an index on software.bundle_identifier.
// The software table is a hot table with frequent writes (software ingestion
// runs per-host/hour). Adding an index would impose write overhead on every
// ingestion. The cost of a full table scan here (one-time migration) and during
// rare FMA additions is acceptable compared to continuous index maintenance.
//
// software_titles.bundle_identifier already has an index (idx_software_titles_bundle_identifier).
_, err := tx.Exec(`
UPDATE software_titles st
JOIN fleet_maintained_apps fma
ON st.bundle_identifier = fma.unique_identifier
AND fma.platform = 'darwin'
SET st.name = fma.name
WHERE st.bundle_identifier IS NOT NULL
AND st.bundle_identifier != ''
AND st.name != fma.name
`)
if err != nil {
return err
}
// Also update software entries to match their software_titles names.
// This ensures consistency when navigating from software_titles to software versions.
_, err = tx.Exec(`
UPDATE software s
JOIN fleet_maintained_apps fma
ON s.bundle_identifier = fma.unique_identifier
AND fma.platform = 'darwin'
SET s.name = fma.name
WHERE s.bundle_identifier IS NOT NULL
AND s.bundle_identifier != ''
AND s.name != fma.name
`)
return err
}
func Down_20260326210603(tx *sql.Tx) error {
// Down migration is a no-op because we cannot reliably restore the original
// osquery-reported names. The FMA names are the canonical/correct names anyway.
return nil
}
@@ -0,0 +1,76 @@
package tables
import (
"testing"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/require"
)
func TestUp_20260326210603(t *testing.T) {
db := applyUpToPrev(t)
// Insert FMAs with canonical names
dataStmts := `
INSERT INTO fleet_maintained_apps (name, slug, unique_identifier, platform) VALUES
('Microsoft Visual Studio Code', 'visual-studio-code/darwin', 'com.microsoft.VSCode', 'darwin'),
('1Password', '1password/darwin', 'com.1password.1password', 'darwin'),
('Windows App', 'windows-app/windows', 'com.windows.app', 'windows');
INSERT INTO software_titles (id, name, source, bundle_identifier) VALUES
(1, 'Code', 'apps', 'com.microsoft.VSCode'),
(2, '1Password 7', 'apps', 'com.1password.1password'),
(3, 'Other App', 'apps', 'com.example.other'),
(4, 'No Bundle ID App', 'apps', NULL);
INSERT INTO software (id, checksum, name, version, source, bundle_identifier, title_id) VALUES
(1, 'checksum_01', 'Code', '1.85.0', 'apps', 'com.microsoft.VSCode', 1),
(2, 'checksum_02', 'Code', '1.86.0', 'apps', 'com.microsoft.VSCode', 1),
(3, 'checksum_03', '1Password 7', '7.10.0', 'apps', 'com.1password.1password', 2),
(4, 'checksum_04', 'Other App', '1.0.0', 'apps', 'com.example.other', 3),
(5, 'checksum_05', 'No Bundle ID App', '1.0.0', 'apps', '', 4);
`
_, err := db.Exec(dataStmts)
require.NoError(t, err)
// Apply the migration
applyNext(t, db)
// Verify software_titles were updated correctly
type softwareTitle struct {
ID uint `db:"id"`
Name string `db:"name"`
BundleIdentifier string `db:"bundle_identifier"`
}
var titles []softwareTitle
err = db.Select(&titles, `SELECT id, name, COALESCE(bundle_identifier, '') as bundle_identifier FROM software_titles ORDER BY id`)
require.NoError(t, err)
require.ElementsMatch(t, []softwareTitle{
{1, "Microsoft Visual Studio Code", "com.microsoft.VSCode"}, // Updated to FMA name
{2, "1Password", "com.1password.1password"}, // Updated to FMA name
{3, "Other App", "com.example.other"}, // No matching FMA, unchanged
{4, "No Bundle ID App", ""}, // No bundle_identifier, unchanged
}, titles)
// Verify software entries were updated correctly
type softwareRow struct {
ID uint `db:"id"`
Name string `db:"name"`
Version string `db:"version"`
BundleIdentifier string `db:"bundle_identifier"`
TitleID *uint `db:"title_id"`
}
var software []softwareRow
err = db.Select(&software, `SELECT id, name, version, COALESCE(bundle_identifier, '') as bundle_identifier, title_id FROM software ORDER BY id`)
require.NoError(t, err)
require.ElementsMatch(t, []softwareRow{
{1, "Microsoft Visual Studio Code", "1.85.0", "com.microsoft.VSCode", ptr.Uint(1)}, // Updated to FMA name
{2, "Microsoft Visual Studio Code", "1.86.0", "com.microsoft.VSCode", ptr.Uint(1)}, // Updated to FMA name
{3, "1Password", "7.10.0", "com.1password.1password", ptr.Uint(2)}, // Updated to FMA name
{4, "Other App", "1.0.0", "com.example.other", ptr.Uint(3)}, // No matching FMA, unchanged
{5, "No Bundle ID App", "1.0.0", "", ptr.Uint(4)}, // No bundle_identifier, unchanged
}, software)
}
File diff suppressed because one or more lines are too long
+56 -8
View File
@@ -925,6 +925,22 @@ func (ds *Datastore) preInsertSoftwareInventory(
}
}
// Fetch FMA canonical names to override osquery-reported names for macOS apps.
// This ensures software titles use consistent names (e.g., "Microsoft Visual Studio Code"
// instead of "Code" which is what osquery reports for VS Code).
// Note: This call is made from the base datastore so it bypasses the cached_mysql layer.
// The query is simple (SELECT from the small fleet_maintained_apps table) so this is acceptable.
// The cached_mysql layer still caches this method for other callers (e.g., API endpoints).
fmaNames, fmaErr := ds.GetFMANamesByIdentifier(ctx)
if fmaErr != nil {
// Log but don't fail - we can still use osquery-reported names.
// A nil map is safe here since Go's map access on nil returns the zero value.
if ds.logger != nil {
ds.logger.WarnContext(ctx, "failed to get FMA names by identifier", "err", fmaErr)
}
fmaNames = nil
}
// Process in smaller batches to reduce lock time
err := common_mysql.BatchProcessSimple(keys, softwareInventoryInsertBatchSize, func(batchKeys []string) error {
batchSoftware := make(map[string]fleet.Software, len(batchKeys))
@@ -941,13 +957,19 @@ func (ds *Datastore) preInsertSoftwareInventory(
// there is not an existing software title corresponding to this incoming software version
newTitleName := sw.Name
if sw.BundleIdentifier != "" {
key := titleKey{
bundleID: sw.BundleIdentifier,
source: sw.Source,
extensionFor: sw.ExtensionFor,
}
if computedName, exists := bestTitleNames[key]; exists {
newTitleName = computedName
// First check if there's an FMA with this bundle identifier - use its canonical name
if fmaName, ok := fmaNames[sw.BundleIdentifier]; ok {
newTitleName = fmaName
} else {
// Fall back to computed best name from osquery reports
key := titleKey{
bundleID: sw.BundleIdentifier,
source: sw.Source,
extensionFor: sw.ExtensionFor,
}
if computedName, exists := bestTitleNames[key]; exists {
newTitleName = computedName
}
}
}
@@ -1186,8 +1208,34 @@ func (ds *Datastore) preInsertSoftwareInventory(
missingSoftwareTitles = append(missingSoftwareTitles,
fmt.Sprintf("%s %s %s", sw.Name, sw.Version, sw.Source))
}
// Use FMA canonical name if available, otherwise use osquery-reported name.
// This ensures software.name matches software_titles.name for consistency.
//
// IMPORTANT: The checksum is intentionally computed from osquery data
// (including the osquery-reported name, NOT the FMA name) for these reasons:
//
// 1. The checksum is used for deduplication via unique index. It serves as
// an internal identifier, not a content integrity hash. The stored name
// can differ from the name used in checksum computation.
//
// 2. Checksums are computed before FMA lookup, using raw osquery data.
// If we regenerated checksums with FMA names:
// - A cache miss or FMA sync delay could cause the same software to
// generate different checksums, creating duplicate entries.
// - Migration would require recomputing checksums for millions of rows.
//
// 3. The checksum is never recomputed from stored data - it's only computed
// from incoming osquery data during ingestion and used for lookup.
softwareName := sw.Name
if sw.BundleIdentifier != "" {
if fmaName, ok := fmaNames[sw.BundleIdentifier]; ok {
softwareName = fmaName
}
}
args = append(
args, sw.Name, sw.Version, sw.Source, sw.Release, sw.Vendor, sw.Arch,
args, softwareName, sw.Version, sw.Source, sw.Release, sw.Vendor, sw.Arch,
sw.BundleIdentifier, sw.ExtensionID, sw.ExtensionFor, titleID, checksum, sw.ApplicationID, sw.UpgradeCode,
)
}
+5
View File
@@ -2447,6 +2447,11 @@ type Datastore interface {
// metadata provided via app.
UpsertMaintainedApp(ctx context.Context, app *MaintainedApp) (*MaintainedApp, error)
// GetFMANamesByIdentifier returns a map of unique_identifier -> canonical name
// for all Fleet-maintained apps on macOS. This is used during software ingestion
// to use the FMA name instead of the osquery-reported name.
GetFMANamesByIdentifier(ctx context.Context) (map[string]string, error)
// /////////////////////////////////////////////////////////////////////////////
// Certificate management
+12
View File
@@ -1553,6 +1553,8 @@ type GetMaintainedAppBySlugFunc func(ctx context.Context, slug string, teamID *u
type UpsertMaintainedAppFunc func(ctx context.Context, app *fleet.MaintainedApp) (*fleet.MaintainedApp, error)
type GetFMANamesByIdentifierFunc func(ctx context.Context) (map[string]string, error)
type BulkUpsertMDMManagedCertificatesFunc func(ctx context.Context, payload []*fleet.MDMManagedCertificate) error
type GetAppleHostMDMCertificateProfileFunc func(ctx context.Context, hostUUID string, profileUUID string, caName string) (*fleet.HostMDMCertificateProfile, error)
@@ -4133,6 +4135,9 @@ type DataStore struct {
UpsertMaintainedAppFunc UpsertMaintainedAppFunc
UpsertMaintainedAppFuncInvoked bool
GetFMANamesByIdentifierFunc GetFMANamesByIdentifierFunc
GetFMANamesByIdentifierFuncInvoked bool
BulkUpsertMDMManagedCertificatesFunc BulkUpsertMDMManagedCertificatesFunc
BulkUpsertMDMManagedCertificatesFuncInvoked bool
@@ -9917,6 +9922,13 @@ func (s *DataStore) UpsertMaintainedApp(ctx context.Context, app *fleet.Maintain
return s.UpsertMaintainedAppFunc(ctx, app)
}
func (s *DataStore) GetFMANamesByIdentifier(ctx context.Context) (map[string]string, error) {
s.mu.Lock()
s.GetFMANamesByIdentifierFuncInvoked = true
s.mu.Unlock()
return s.GetFMANamesByIdentifierFunc(ctx)
}
func (s *DataStore) BulkUpsertMDMManagedCertificates(ctx context.Context, payload []*fleet.MDMManagedCertificate) error {
s.mu.Lock()
s.BulkUpsertMDMManagedCertificatesFuncInvoked = true