From 082044e7a02f5760ae19ce4500a98812605b22e6 Mon Sep 17 00:00:00 2001 From: Mitch Francese <2227948+tux234@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:07:43 -0400 Subject: [PATCH] Fix Microsoft Office FMA version mismatch for Outlook, PowerPoint, OneNote (#40649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **Mode 1 fix (structural):** Applies \`MicrosoftVersionFromReleaseNotes\` to \`microsoft-outlook/darwin\`, \`microsoft-powerpoint/darwin\`, and \`microsoft-onenote/darwin\` in the FMA enrichment \`Funcs\` map, and regenerates output manifests with corrected short versions (e.g. \`16.106.2\` instead of \`16.106.26022219\`). Word and Excel already had this transformation; this brings the remaining Office suite into parity. **Mode 2 fix (reliability):** When the exact Homebrew build number cannot be matched against Microsoft's release notes page (e.g., because the page hasn't been updated yet for a newly published build), the transformer now falls back to the base \`major.minor\` version (e.g., \`16.106\`) rather than leaving the raw build string in the manifest (e.g., \`16.106.26021521\`). The raw build string caused a perpetual "update available" loop because \`compareVersions("16.106.1", "16.106.26021521")\` always evaluates to -1, regardless of how many times the update is installed. Also adds a guard for versions with fewer than 3 segments to prevent silent corruption. **Root cause:** osquery reports installed app versions using \`CFBundleShortVersionString\` (e.g. \`16.106.2\`), but the FMA manifest stored the raw Homebrew build version (\`CFBundleVersion\`, e.g. \`16.106.26022219\`). The mismatch caused Fleet to perpetually show "update available" even after the latest version was installed. Fixes #40647 ## Test plan - [x] Verify \`microsoft-outlook/darwin.json\`, \`microsoft-powerpoint/darwin.json\`, and \`microsoft-onenote/darwin.json\` outputs now contain short version strings (e.g. \`16.106.2\`) - [x] Verify \`microsoft-word/darwin.json\` and \`microsoft-excel/darwin.json\` are unchanged - [x] Run \`go test ./ee/maintained-apps/...\` — all tests pass - [ ] Add a fleet-maintained Outlook/PowerPoint/OneNote app to a team; confirm a host with the current version installed shows as up-to-date (no spurious "update available") - [ ] Confirm that if the ingester runs against a Homebrew build whose number is not yet in Microsoft's release notes, the manifest stores the base version (e.g. \`16.107\`) rather than the raw build string (e.g. \`16.107.26031234\`) --------- Co-authored-by: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> --- .../ingesters/homebrew/external_refs/main.go | 3 +++ .../microsoft_version_from_release_notes.go | 23 +++++++++++++++++-- ...crosoft_version_from_release_notes_test.go | 22 +++++++++++++++--- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/ee/maintained-apps/ingesters/homebrew/external_refs/main.go b/ee/maintained-apps/ingesters/homebrew/external_refs/main.go index 008b9cd0a5..2aefd90a8e 100644 --- a/ee/maintained-apps/ingesters/homebrew/external_refs/main.go +++ b/ee/maintained-apps/ingesters/homebrew/external_refs/main.go @@ -9,6 +9,9 @@ import ( var Funcs = map[string][]func(*maintained_apps.FMAManifestApp) (*maintained_apps.FMAManifestApp, error){ "microsoft-word/darwin": {MicrosoftVersionFromReleaseNotes}, "microsoft-excel/darwin": {MicrosoftVersionFromReleaseNotes}, + "microsoft-outlook/darwin": {MicrosoftVersionFromReleaseNotes}, + "microsoft-powerpoint/darwin": {MicrosoftVersionFromReleaseNotes}, + "microsoft-onenote/darwin": {MicrosoftVersionFromReleaseNotes}, "brave-browser/darwin": {BraveVersionTransformer}, "whatsapp/darwin": {WhatsAppVersionShortener, WhatsAppInstallerURL}, "google-chrome/darwin": {ChromePKGInstaller}, diff --git a/ee/maintained-apps/ingesters/homebrew/external_refs/microsoft_version_from_release_notes.go b/ee/maintained-apps/ingesters/homebrew/external_refs/microsoft_version_from_release_notes.go index c221a2971d..8a8747c51a 100644 --- a/ee/maintained-apps/ingesters/homebrew/external_refs/microsoft_version_from_release_notes.go +++ b/ee/maintained-apps/ingesters/homebrew/external_refs/microsoft_version_from_release_notes.go @@ -19,9 +19,24 @@ var ( // Homebrew version "16.95.25032931" // Release notes version "Version 16.95.3 (Build 25032931)" // For this example it would change the app version to "16.95.3" +// +// If the exact build number is not found in the release notes (e.g., because the +// release notes page hasn't been updated yet for a newly published build), the +// function falls back to the base version without the build number (e.g., "16.95" +// instead of "16.95.25032931"). This prevents a perpetual "update available" loop +// that would otherwise occur because osquery reports the short version +// (e.g., "16.95.3") while the manifest stores the raw build string, causing +// compareVersions to always flag the installed version as older. func MicrosoftVersionFromReleaseNotes(app *maintained_apps.FMAManifestApp) (*maintained_apps.FMAManifestApp, error) { homebrewVersion := app.Version - versionParts := strings.Split(homebrewVersion, ".") // homebrew version format is like "16.95.25032931" + versionParts := strings.Split(homebrewVersion, ".") // homebrew version format is like "16.95.25032931" + + // If the version doesn't have at least major.minor.build segments, there is no + // build number to extract and look up; return the version unchanged. + if len(versionParts) < 3 { + return app, nil + } + version := strings.Join(versionParts[:len(versionParts)-1], ".") // Extract version without the build number build := versionParts[len(versionParts)-1] // Extract the build number @@ -42,5 +57,9 @@ func MicrosoftVersionFromReleaseNotes(app *maintained_apps.FMAManifestApp) (*mai } } - return app, fmt.Errorf("no matching version found in release notes for %s", homebrewVersion) + // No exact match found in the release notes for this build number. Fall back to + // the base version (e.g., "16.95" from "16.95.25032931") so that the installed + // short version (e.g., "16.95.3") is not falsely flagged as older. + app.Version = version + return app, nil } diff --git a/ee/maintained-apps/ingesters/homebrew/external_refs/microsoft_version_from_release_notes_test.go b/ee/maintained-apps/ingesters/homebrew/external_refs/microsoft_version_from_release_notes_test.go index cdf69b0a9d..978fe2b097 100644 --- a/ee/maintained-apps/ingesters/homebrew/external_refs/microsoft_version_from_release_notes_test.go +++ b/ee/maintained-apps/ingesters/homebrew/external_refs/microsoft_version_from_release_notes_test.go @@ -43,13 +43,29 @@ func TestMicrosoftVersionFromReleaseNotes(t *testing.T) { assert.Equal(t, "16.98.3", result.Version) }) - t.Run("version not found", func(t *testing.T) { + t.Run("version not found falls back to base version", func(t *testing.T) { + // Build number "25999999" is not in the release notes cache above. + // The function should fall back to the base major.minor version ("16.50") + // rather than leaving the raw Homebrew build string, which would cause a + // perpetual "update available" loop against osquery's short version reporting. app := &maintained_apps.FMAManifestApp{ UniqueIdentifier: "microsoft-excel/darwin", Version: "16.50.25999999", } result, err := MicrosoftVersionFromReleaseNotes(app) - assert.Error(t, err) - assert.Equal(t, "16.50.25999999", result.Version) + assert.NoError(t, err) + assert.Equal(t, "16.50", result.Version) + }) + + t.Run("version with fewer than 3 segments is returned unchanged", func(t *testing.T) { + // Versions with fewer than 3 segments have no build number to look up; + // the function should return the version unchanged. + app := &maintained_apps.FMAManifestApp{ + UniqueIdentifier: "microsoft-onenote/darwin", + Version: "16.106", + } + result, err := MicrosoftVersionFromReleaseNotes(app) + assert.NoError(t, err) + assert.Equal(t, "16.106", result.Version) }) }