Fix Steam patch policy comparing an empty bundle_short_version (#50428)
**Related issue:** Resolves #50408 ## What changed Steam.app ships without a `CFBundleShortVersionString`, so osquery's `apps.bundle_short_version` is an empty string: ``` $ /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" /Applications/Steam.app/Contents/Info.plist Print: Entry, ":CFBundleShortVersionString", Does Not Exist $ /usr/libexec/PlistBuddy -c "Print :CFBundleVersion" /Applications/Steam.app/Contents/Info.plist 6.0 ``` The generated patch policy compared that column, and `version_compare('', '6.0')` returns `-1`, so the `< 0` predicate was always true. The "Steam up to date" policy could never pass on **any** host with Steam installed, at any version. Meanwhile software inventory falls back to `bundle_version` and correctly showed Steam as up to date, so the two features disagreed about the same app on the same host — and with `install_software: true` the policy repeatedly reinstalled a version that was already installed. This adds a per-app override in the homebrew ingester comparing `bundle_version` (CFBundleVersion `6.0`, which the cask version tracks), following the pattern already used for `sonos` and the Firefox pre-release channels: ```diff -version_compare(bundle_short_version, '6.0') < 0 +version_compare(bundle_version, '6.0') < 0 ``` `ee/maintained-apps/outputs/steam/darwin.json` was regenerated with `go run ./cmd/maintained-apps -slug steam/darwin` — one line changed, no upstream version drift pulled in. ## Why scoped to one app The issue suggested changing the shared darwin version column in `pkg/patch_policy` to `COALESCE(NULLIF(bundle_short_version, ''), bundle_version)`. I didn't do that. It would be a no-op for the ~300 macOS FMAs that do set a short version, but that generator is load-bearing for every one of them, and the blast radius isn't justified by a single broken app. The per-app override is the established mechanism for exactly this. Side note for a possible follow-up: `patch_policy_path` exists in both the homebrew and winget input structs but is never read anywhere — a dead field. If we want a data-driven way to express these overrides instead of token checks in Go, that's the hook. ## Reviewer note: existing deployments do not self-heal `software_installers.patch_query` is snapshotted when the installer is created, and only refreshes on an FMA version change or an "Edit software" save. **Steam's cask version is a static `6.0`**, so this manifest change alone will not fix already-deployed Steam FMAs — the admin has to re-add or re-save the app. A GitOps re-apply doesn't help either; `ApplyPolicySpecs` regenerates from the stale installer row. Closing that gap means either a migration that rewrites stored patch queries, or refreshing `patch_query` when the manifest changes at the same version. Both are broader calls than this bug, so I left them out — happy to file a follow-up if you want it tracked. The exists query is unaffected — it matches on `bundle_identifier` only, with no version predicate. That's why install detection and self-service always worked correctly for Steam. # Checklist for submitter - [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. - [x] 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. <sub>No new interpolation surface: the override formats the same bundle identifier and cask version the surrounding generator already formats.</sub> ## Testing - [x] Added/updated automated tests <sub>New `steam` case in `TestIngestApps` asserting both the patched and exists queries.</sub> - [x] QA'd all new/changed functionality manually Validated with osquery **5.23.1** — the same version as in the bug report. `version_compare('', '6.0')` returns `-1` and `version_compare('6.0', '6.0')` returns `0`, confirming the root cause directly. For an end-to-end check against the real `apps` table without planting a fake Steam.app on a Fleet-enrolled host, I used an already-installed app with the identical shape (`com.citrix.HDXCast`: empty `bundle_short_version`, `bundle_version` `24.05.0.3`): | Query | Host state | Result | |---|---|---| | exists | app installed | row → detected ✅ (unaffected by the bug) | | **old** patched | up to date | **no row → policy FAILS** ← reproduces the bug | | **new** patched | up to date | row → policy PASSES ✅ | | **new** patched | genuinely outdated (available `25.0.0`) | no row → policy FAILS ✅ | | **new** patched, verbatim from the regenerated manifest | Steam not installed | row → PASSES ✅ | The fourth row is the important one: the fix is not a blanket pass — it still fails hosts that are genuinely behind. Not verified: a live host with Steam actually installed (I don't have one). The `com.citrix.HDXCast` row has byte-identical column semantics, so I'm confident, but a QA pass on a real Steam host would close it out. `go test ./cmd/maintained-apps/... ./pkg/patch_policy/... ./ee/maintained-apps/...` passes; `go vet` and `gofmt` clean. I could not run `make lint-go-incremental` locally — it builds a custom golangci-lint via `git clone`, which my sandbox blocked, so I'm relying on CI for that. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved Steam patch detection on macOS by using the correct application version information. * Steam updates are now accurately recognized in Fleet software inventory and Homebrew-generated patch policies. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Fixed the macOS "Steam up to date" patch policy always failing on hosts that have the current version of Steam installed. Steam.app ships without a `CFBundleShortVersionString`, so the generated policy now compares `CFBundleVersion`, which agrees with the version Fleet reports in software inventory.
|
||||
@@ -250,6 +250,18 @@ func (i *brewIngester) ingestOne(ctx context.Context, input inputApp) (*maintain
|
||||
out.UniqueIdentifier, out.Version,
|
||||
)
|
||||
}
|
||||
if input.Token == "steam" {
|
||||
// Steam.app ships without a CFBundleShortVersionString, so osquery's
|
||||
// bundle_short_version is empty and version_compare('', '<version>') < 0 is
|
||||
// always true — the default patch policy can never pass on a host that has
|
||||
// Steam installed. Compare bundle_version (CFBundleVersion, "6.0", which the
|
||||
// cask version tracks); that's also the value software inventory falls back
|
||||
// to, so patch status and inventory agree.
|
||||
out.Queries.Patched = fmt.Sprintf(
|
||||
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = '%s' AND version_compare(bundle_version, '%s') < 0);",
|
||||
out.UniqueIdentifier, out.Version,
|
||||
)
|
||||
}
|
||||
if input.Token == "firefox@developer-edition" {
|
||||
// The bundle reports only the base version ("153.0") for cask version
|
||||
// "153.0b13", so compare CFBundleVersion (encodes the build date, resolved
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestIngestValidations(t *testing.T) {
|
||||
Version: "1.0",
|
||||
}
|
||||
|
||||
case "ok", "docker-desktop", "swiftdialog", "install_script_path", "uninstall_script_path", "uninstall_script_path_with_pre", "uninstall_script_path_with_post", "patch_policy_path":
|
||||
case "ok", "docker-desktop", "steam", "swiftdialog", "install_script_path", "uninstall_script_path", "uninstall_script_path_with_pre", "uninstall_script_path_with_post", "patch_policy_path":
|
||||
cask = brewCask{
|
||||
Token: appToken,
|
||||
Name: []string{appToken},
|
||||
@@ -145,6 +145,7 @@ func TestIngestValidations(t *testing.T) {
|
||||
{"", inputApp{Token: "docker-desktop", UniqueIdentifier: "com.electron.dockerdesktop", InstallerFormat: "dmg", Name: "Docker Desktop", Slug: "docker-desktop/darwin"}},
|
||||
{"", inputApp{Token: "firefox@developer-edition", UniqueIdentifier: "org.mozilla.firefoxdeveloperedition", InstallerFormat: "dmg", Name: "Mozilla Firefox Developer Edition", Slug: "firefox@developer-edition/darwin"}},
|
||||
{"", inputApp{Token: "firefox@nightly", UniqueIdentifier: "org.mozilla.nightly", InstallerFormat: "dmg", Name: "Mozilla Firefox Nightly", Slug: "firefox@nightly/darwin"}},
|
||||
{"", inputApp{Token: "steam", UniqueIdentifier: "com.valvesoftware.steam", InstallerFormat: "dmg", Name: "Steam", Slug: "steam/darwin"}},
|
||||
{"", inputApp{Token: "swiftdialog", UniqueIdentifier: "au.csiro.dialog", InstallerFormat: "pkg", Name: "swiftDialog", Slug: "swiftdialog/darwin"}},
|
||||
{"", inputApp{Token: "install_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", InstallScriptPath: path.Join(tempDir, "install_script.sh")}},
|
||||
{"", inputApp{Token: "uninstall_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh")}},
|
||||
@@ -200,6 +201,15 @@ func TestIngestValidations(t *testing.T) {
|
||||
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.nightly' AND version_compare(bundle_version, '15426.7.17') < 0);",
|
||||
out.Queries.Patched,
|
||||
)
|
||||
case "steam":
|
||||
// Steam.app has no CFBundleShortVersionString, so the patched query
|
||||
// compares CFBundleVersion; the exists query is unaffected because it
|
||||
// only matches on bundle identifier.
|
||||
require.Equal(t, "SELECT 1 FROM apps WHERE bundle_identifier = 'com.valvesoftware.steam';", out.Queries.Exists)
|
||||
require.Equal(t,
|
||||
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.valvesoftware.steam' AND version_compare(bundle_version, '1.0') < 0);",
|
||||
out.Queries.Patched,
|
||||
)
|
||||
case "swiftdialog":
|
||||
require.Equal(t, "SELECT 1 FROM apps WHERE bundle_identifier = 'au.csiro.dialog' AND path != '/opt/orbit/bin/swiftDialog/macos/stable/Dialog.app';", out.Queries.Exists)
|
||||
require.Equal(t,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "6.0",
|
||||
"queries": {
|
||||
"exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.valvesoftware.steam';",
|
||||
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.valvesoftware.steam' AND version_compare(bundle_short_version, '6.0') < 0);"
|
||||
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.valvesoftware.steam' AND version_compare(bundle_version, '6.0') < 0);"
|
||||
},
|
||||
"installer_url": "https://cdn.cloudflare.steamstatic.com/client/installer/steam.dmg",
|
||||
"install_script_ref": "45a6174b",
|
||||
|
||||
Reference in New Issue
Block a user