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:
Allen Houchins
2026-08-05 13:38:20 -05:00
committed by GitHub
parent b8e324383d
commit 92aaf1de81
4 changed files with 25 additions and 2 deletions
@@ -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,
+1 -1
View File
@@ -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",