Fix Microsoft Office FMA version mismatch for Outlook, PowerPoint, OneNote (#40649)

## 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>
This commit is contained in:
Mitch Francese
2026-04-01 17:07:43 -04:00
committed by GitHub
co-authored by Dante Catalfamo
parent 71902db506
commit 082044e7a0
3 changed files with 43 additions and 5 deletions
@@ -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},
@@ -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
}
@@ -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)
})
}