Skip to content

Fix mis-assigned FMA bundle identifiers, switch to fuzzy matching on queries where Windows apps include version number in the name (incl. special fixes for Firefox ESR)#42628

Merged
iansltx merged 11 commits intomainfrom
fma-fixes
Apr 10, 2026

Conversation

@iansltx
Copy link
Copy Markdown
Contributor

@iansltx iansltx commented Mar 28, 2026

Resolves #42714.

Zed + Opus 4.6; initial prompts (see additional ones in follow-on commits):


Audit our existing Fleet Maintained App catalog. Look for:

  1. Software that has the wrong identifiers associated (e.g. Abstract), e.g. in exists queries
  2. Software that has the version number in the name that leaks into the exists query, e.g. 7-zip or 010 Editor or Airtame. These should be fuzzy-matched.

For each affected app, revise input manifests to fix the issues. For (1), revise apps.json if needed as well. Don't modify apps.json for (2) cases.


Are there any discrepancies between bundle identifiers in input manifests for Darwin apps and apps.json? If so, fix them.


Outputs will get overwritten by the ingester if neither the ingester nor the input JSON files are changed. Make whatever changes need to be made so that these edits survive an FMA ingestion cycle.


Revise fuzzy to allow specifying a custom value e.g. Mozilal Firefox % (ESR) in addition to the existing true/false, then use that new functionality to build unique queries for Firefox ESR.


Commit these changes, across multiple commits (there will be cases where a changes to a single file will be spread across multiple commits, most notably apps.json). Split commits out as follows:

  1. All darwin-related changes
  2. Windows switches to fuzzy matching + associated unique_identifier changes
  3. Revised handling for Firefox ESR

Prefix commit messages with "🤖 ".


The ingester and test changes should've gone in commit 3. Move them there from commit 2.


Summary by CodeRabbit

  • Bug Fixes

    • Updated application identifiers for Abstract, Amazon Chime, Beyond Compare, and Teleport Suite to use correct bundle and package identifiers.
    • Enhanced Windows and macOS installation detection queries to match multiple application versions using pattern matching instead of exact version strings.
  • New Features

    • Added support for configurable fuzzy matching patterns to improve application name matching flexibility.
  • Tests

    • Added tests validating fuzzy matching configuration unmarshaling and behavior.

iansltx added 3 commits March 28, 2026 17:38
- Abstract: change unique_identifier from com.SweetScape.010Editor
  (010 Editor's bundle ID) to com.elasticprojects.abstract-desktop.
  Update exists query and install script refs in output manifest.
- Teleport Suite: change unique_identifier from com.gravitational.teleport
  (pkg receipt ID) to com.gravitational.teleport.tsh (tsh.app bundle ID).
  Update exists query in output manifest.
- Amazon Chime (outputs/apps.json): fix unique_identifier from
  com.runningwithcrayons.Alfred (Alfred's bundle ID) to
  com.amazon.Amazon-Chime.
- Beyond Compare (outputs/apps.json): fix casing from
  com.scootersoftware.BeyondCompare to com.ScooterSoftware.BeyondCompare.
…matching

Several winget apps had version numbers baked into unique_identifier,
causing exists queries to do exact matches against stale version strings.
Strip version numbers and add fuzzy_match_name: true so the generated
query uses LIKE 'Name %' instead of an exact match.

Replace the bool FuzzyMatchName field with a custom fuzzyMatch type that
accepts a JSON boolean (true/false) or a string containing a custom LIKE
pattern. This is backwards-compatible: true and false behave exactly as
before, and a string value is used as a verbatim LIKE pattern.

Affected apps: 010 Editor, 7-Zip, Airtame, GIMP, Notion, Postman, PuTTY.
Firefox ESR registers in Windows as 'Mozilla Firefox X.Y.Z ESR (x64 en-US)'.
A simple fuzzy prefix match ('Mozilla Firefox %') would also match regular
Firefox ('Mozilla Firefox (x64 en-US)').

Use the new custom fuzzy_match_name string support to specify the pattern
'Mozilla Firefox % ESR %', which uniquely matches ESR builds without
colliding with regular Firefox.
@iansltx
Copy link
Copy Markdown
Contributor Author

iansltx commented Mar 28, 2026

@claude review

@iansltx
Copy link
Copy Markdown
Contributor Author

iansltx commented Mar 28, 2026

@coderabbitai full-review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

@iansltx Sure, I'll redo the full review of this PR now!

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

Walkthrough

This pull request introduces configurable fuzzy matching to the winget app ingester and updates application detection queries across Windows and macOS platforms. The core changes include: refactoring the ingester's fuzzyMatch type to unmarshal from either a boolean or string pattern, enabling three query modes (custom pattern, automatic pattern, or exact match); adding comprehensive tests for the new unmarshalling behavior; updating multiple application input manifests to enable fuzzy matching by setting the fuzzy_match_name field; and modifying output detection queries to use SQL LIKE patterns instead of exact value matches, allowing detection of applications regardless of version or variant strings.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lacks required checklist items (changes files, testing, security validation). It appears to be mostly internal notes rather than a structured checklist. Complete the description template by adding checked/unchecked boxes for testing, security validation, and changes files per repository requirements.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main changes: fixing misassigned bundle identifiers and switching to fuzzy matching for Windows apps with version numbers in names, with special handling for Firefox ESR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fma-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ee/maintained-apps/ingesters/winget/ingester.go`:
- Around line 351-367: The existsQuery construction currently injects raw values
(input.FuzzyMatchName.Custom, name, publisher) into SQL via fmt.Sprintf
(existsQuery), which allows quotes/injection; change to use parameterized
queries instead of interpolation: build the SQL with placeholders (e.g., "SELECT
1 FROM programs WHERE name LIKE ? AND publisher = ?" or the driver-specific
$1/$2 style) for the three branches (the cases for input.FuzzyMatchName.Custom,
input.FuzzyMatchName.Enabled, and default) and pass the corresponding arguments
when executing the query (the code that uses existsQuery should call
db.QueryRow/Query with the args), or if you must keep string composition,
properly escape single quotes in the values before inserting; update the code
paths that execute existsQuery accordingly so no raw name/publisher/custom
strings are interpolated.

In `@ee/maintained-apps/outputs/teleport-suite/darwin.json`:
- Line 6: The detection SQL in darwin.json now checks bundle_identifier =
'com.gravitational.teleport.tsh' but the embedded installer script still targets
'com.gravitational.teleport' for quit/relaunch; update the installer calls to
use the corrected bundle id so lifecycle handling matches detection. Locate the
"exists" SQL entry referencing com.gravitational.teleport.tsh and the installer
script code that calls quit/relaunch on com.gravitational.teleport, and change
those script references to com.gravitational.teleport.tsh (or alternatively make
the SQL match the original bundle id if the script must remain unchanged) so
both the detection (exists) and lifecycle actions reference the same bundle
identifier.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 580c6957-743c-47c6-afe5-a35d5b01e85e

📥 Commits

Reviewing files that changed from the base of the PR and between fb975a7 and be8619a.

📒 Files selected for processing (23)
  • ee/maintained-apps/ingesters/winget/ingester.go
  • ee/maintained-apps/ingesters/winget/ingester_test.go
  • ee/maintained-apps/inputs/homebrew/abstract.json
  • ee/maintained-apps/inputs/homebrew/teleport-suite.json
  • ee/maintained-apps/inputs/winget/010-editor.json
  • ee/maintained-apps/inputs/winget/7-zip.json
  • ee/maintained-apps/inputs/winget/airtame.json
  • ee/maintained-apps/inputs/winget/firefox@esr.json
  • ee/maintained-apps/inputs/winget/gimp.json
  • ee/maintained-apps/inputs/winget/notion.json
  • ee/maintained-apps/inputs/winget/postman.json
  • ee/maintained-apps/inputs/winget/putty.json
  • ee/maintained-apps/outputs/010-editor/windows.json
  • ee/maintained-apps/outputs/7-zip/windows.json
  • ee/maintained-apps/outputs/abstract/darwin.json
  • ee/maintained-apps/outputs/airtame/windows.json
  • ee/maintained-apps/outputs/apps.json
  • ee/maintained-apps/outputs/firefox@esr/windows.json
  • ee/maintained-apps/outputs/gimp/windows.json
  • ee/maintained-apps/outputs/notion/windows.json
  • ee/maintained-apps/outputs/postman/windows.json
  • ee/maintained-apps/outputs/putty/windows.json
  • ee/maintained-apps/outputs/teleport-suite/darwin.json

Comment on lines +351 to +367
var existsQuery string
switch {
case input.FuzzyMatchName.Custom != "":
existsQuery = fmt.Sprintf(
"SELECT 1 FROM programs WHERE name LIKE '%s' AND publisher = '%s';",
input.FuzzyMatchName.Custom, publisher,
)
case input.FuzzyMatchName.Enabled:
existsQuery = fmt.Sprintf(
"SELECT 1 FROM programs WHERE name LIKE '%s %%' AND publisher = '%s';",
name, publisher,
)
default:
existsQuery = fmt.Sprintf(
"SELECT 1 FROM programs WHERE name = '%s' AND publisher = '%s';",
name, publisher,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape SQL literals before composing existsQuery.

Line 355, Line 360, and Line 365 interpolate raw strings into SQL literals. A single quote in publisher, name, or custom pattern can break the query (and can become injection-prone). Please escape values before formatting.

Suggested fix
 	// TODO - consider UpgradeCode here?
 	var existsQuery string
+	sqlEscape := func(v string) string {
+		return strings.ReplaceAll(v, "'", "''")
+	}
+	safeName := sqlEscape(name)
+	safePublisher := sqlEscape(publisher)
 	switch {
 	case input.FuzzyMatchName.Custom != "":
 		existsQuery = fmt.Sprintf(
 			"SELECT 1 FROM programs WHERE name LIKE '%s' AND publisher = '%s';",
-			input.FuzzyMatchName.Custom, publisher,
+			sqlEscape(input.FuzzyMatchName.Custom), safePublisher,
 		)
 	case input.FuzzyMatchName.Enabled:
 		existsQuery = fmt.Sprintf(
 			"SELECT 1 FROM programs WHERE name LIKE '%s %%' AND publisher = '%s';",
-			name, publisher,
+			safeName, safePublisher,
 		)
 	default:
 		existsQuery = fmt.Sprintf(
 			"SELECT 1 FROM programs WHERE name = '%s' AND publisher = '%s';",
-			name, publisher,
+			safeName, safePublisher,
 		)
 	}

As per coding guidelines, "Review all SQL queries for possible SQL injection."

Also applies to: 370-370

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ee/maintained-apps/ingesters/winget/ingester.go` around lines 351 - 367, The
existsQuery construction currently injects raw values
(input.FuzzyMatchName.Custom, name, publisher) into SQL via fmt.Sprintf
(existsQuery), which allows quotes/injection; change to use parameterized
queries instead of interpolation: build the SQL with placeholders (e.g., "SELECT
1 FROM programs WHERE name LIKE ? AND publisher = ?" or the driver-specific
$1/$2 style) for the three branches (the cases for input.FuzzyMatchName.Custom,
input.FuzzyMatchName.Enabled, and default) and pass the corresponding arguments
when executing the query (the code that uses existsQuery should call
db.QueryRow/Query with the args), or if you must keep string composition,
properly escape single quotes in the values before inserting; update the code
paths that execute existsQuery accordingly so no raw name/publisher/custom
strings are interpolated.

"version": "18.7.3",
"queries": {
"exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.gravitational.teleport';",
"exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.gravitational.teleport.tsh';",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bundle ID update is inconsistent with install script app-id calls.

Line 6 now keys detection on com.gravitational.teleport.tsh, but the embedded install script still quits/relaunches com.gravitational.teleport. This leaves app lifecycle handling out of sync with the corrected identifier.

Proposed fix
-quit_and_track_application 'com.gravitational.teleport'
+quit_and_track_application 'com.gravitational.teleport.tsh'
 ...
-relaunch_application 'com.gravitational.teleport'
+relaunch_application 'com.gravitational.teleport.tsh'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ee/maintained-apps/outputs/teleport-suite/darwin.json` at line 6, The
detection SQL in darwin.json now checks bundle_identifier =
'com.gravitational.teleport.tsh' but the embedded installer script still targets
'com.gravitational.teleport' for quit/relaunch; update the installer calls to
use the corrected bundle id so lifecycle handling matches detection. Locate the
"exists" SQL entry referencing com.gravitational.teleport.tsh and the installer
script code that calls quit/relaunch on com.gravitational.teleport, and change
those script references to com.gravitational.teleport.tsh (or alternatively make
the SQL match the original bundle id if the script must remain unchanged) so
both the detection (exists) and lifecycle actions reference the same bundle
identifier.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.86%. Comparing base (6200bdb) to head (4703ae9).
⚠️ Report is 11 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #42628      +/-   ##
==========================================
- Coverage   66.88%   66.86%   -0.03%     
==========================================
  Files        2591     2587       -4     
  Lines      207684   207582     -102     
  Branches     9322     9187     -135     
==========================================
- Hits       138913   138797     -116     
- Misses      56136    56148      +12     
- Partials    12635    12637       +2     
Flag Coverage Δ
backend 68.68% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

iansltx added 2 commits April 10, 2026 09:59
# Conflicts:
#	ee/maintained-apps/ingesters/winget/ingester_test.go
#	ee/maintained-apps/outputs/010-editor/windows.json
#	ee/maintained-apps/outputs/7-zip/windows.json
#	ee/maintained-apps/outputs/abstract/darwin.json
#	ee/maintained-apps/outputs/airtame/windows.json
#	ee/maintained-apps/outputs/firefox@esr/windows.json
#	ee/maintained-apps/outputs/gimp/windows.json
#	ee/maintained-apps/outputs/notion/windows.json
#	ee/maintained-apps/outputs/postman/windows.json
#	ee/maintained-apps/outputs/putty/windows.json
#	ee/maintained-apps/outputs/teleport-suite/darwin.json
@github-actions
Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/010-editor/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/7-zip/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/abstract/darwin.json

=== Install // 40f16970 -> 17ab2dcc ===

--- /tmp/old.YjuE8j	2026-04-10 15:16:59.505924897 +0000
+++ /tmp/new.EuZjTH	2026-04-10 15:16:59.505924897 +0000
@@ -11,7 +11,9 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi

=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/airtame/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/firefox@esr/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/gimp/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/notion/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/postman/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/putty/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/teleport-suite/darwin.json

=== Install // 3435fc52 -> 4f45dd14 ===

--- /tmp/old.3mZiux	2026-04-10 15:16:59.760927258 +0000
+++ /tmp/new.inUwDV	2026-04-10 15:16:59.761927268 +0000
@@ -11,7 +11,9 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi
@@ -80,6 +82,6 @@
 
 
 # install pkg files
-quit_and_track_application 'com.gravitational.teleport'
-sudo installer -pkg "$TMPDIR/teleport-18.7.3.pkg" -target /
-relaunch_application 'com.gravitational.teleport'
+quit_and_track_application 'com.gravitational.teleport.tsh'
+sudo installer -pkg "$TMPDIR/teleport-18.7.4.pkg" -target /
+relaunch_application 'com.gravitational.teleport.tsh'

=== Uninstall Script (no changes) ===

iansltx added 4 commits April 10, 2026 11:24
Zed + Opus 4.6; prompt: `ee/maintained-apps/outputs/postman/windows.json` is behaving unexpectedly; L7 should basically be a wrapped L6. What's going on?
Zed + Opus 4.6; prompt: In `ee/maintained-apps/ingesters/winget/ingester.go`, escape SQL params in L386-400 before interpolating to avoid SQLi.
@github-actions
Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/010-editor/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/7-zip/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/abstract/darwin.json

=== Install // 40f16970 -> 17ab2dcc ===

--- /tmp/old.kEswuI	2026-04-10 18:19:05.456630489 +0000
+++ /tmp/new.SU1hiA	2026-04-10 18:19:05.456630489 +0000
@@ -11,7 +11,9 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi

=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/airtame/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/firefox@esr/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/gimp/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/notion/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/postman/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/putty/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/teleport-suite/darwin.json

=== Install // 3435fc52 -> 4f45dd14 ===

--- /tmp/old.Tt6ik1	2026-04-10 18:19:05.650628933 +0000
+++ /tmp/new.RQgBA1	2026-04-10 18:19:05.650628933 +0000
@@ -11,7 +11,9 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi
@@ -80,6 +82,6 @@
 
 
 # install pkg files
-quit_and_track_application 'com.gravitational.teleport'
-sudo installer -pkg "$TMPDIR/teleport-18.7.3.pkg" -target /
-relaunch_application 'com.gravitational.teleport'
+quit_and_track_application 'com.gravitational.teleport.tsh'
+sudo installer -pkg "$TMPDIR/teleport-18.7.4.pkg" -target /
+relaunch_application 'com.gravitational.teleport.tsh'

=== Uninstall Script (no changes) ===

iansltx added 2 commits April 10, 2026 13:24
Zed + Opus 4.6; prompt: Build tests covering `setUpExistsQuery` in `ee/maintained-apps/ingesters/winget/ingester.go`
Zed + Opus 4.6; prompts:

> Would it make sense to extract name handling from `setUpExistsQuery` into a method on fuzzyMatch? Probably would allow for some early returns rather than the existing case statement.

(it decided it was a worthwhile refactor and asked if it should proceed)

> yep

After the modifications, I switched the method to be a pointer receiver
@github-actions
Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/010-editor/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/7-zip/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/abstract/darwin.json

=== Install // 40f16970 -> 17ab2dcc ===

--- /tmp/old.DgPkwo	2026-04-10 18:31:26.839481539 +0000
+++ /tmp/new.HMR3nj	2026-04-10 18:31:26.839481539 +0000
@@ -11,7 +11,9 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi

=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/airtame/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/firefox@esr/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/gimp/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/notion/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/postman/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/putty/windows.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/teleport-suite/darwin.json

=== Install // 3435fc52 -> 4f45dd14 ===

--- /tmp/old.GyPKoC	2026-04-10 18:31:27.091480058 +0000
+++ /tmp/new.BCqln2	2026-04-10 18:31:27.091480058 +0000
@@ -11,7 +11,9 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi
@@ -80,6 +82,6 @@
 
 
 # install pkg files
-quit_and_track_application 'com.gravitational.teleport'
-sudo installer -pkg "$TMPDIR/teleport-18.7.3.pkg" -target /
-relaunch_application 'com.gravitational.teleport'
+quit_and_track_application 'com.gravitational.teleport.tsh'
+sudo installer -pkg "$TMPDIR/teleport-18.7.4.pkg" -target /
+relaunch_application 'com.gravitational.teleport.tsh'

=== Uninstall Script (no changes) ===

@iansltx iansltx marked this pull request as ready for review April 10, 2026 18:33
@iansltx iansltx requested a review from a team as a code owner April 10, 2026 18:33
Copilot AI review requested due to automatic review settings April 10, 2026 18:33
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect Fleet Maintained App (FMA) identifiers and improves Windows install-detection robustness by switching version-sensitive programs.name checks to fuzzy LIKE matching, including custom patterns for Firefox ESR.

Changes:

  • Corrects mis-assigned Darwin bundle identifiers (e.g., Abstract, Amazon Chime, Teleport Suite) and aligns apps.json unique identifiers.
  • Adds/extends WinGet ingester support for fuzzy name matching (boolean or custom pattern string) and updates affected Windows app manifests/outputs to use LIKE.
  • Hardens patch-policy query generation to safely wrap exists queries that include % (SQL LIKE) by escaping % before fmt.Sprintf, plus adds test coverage.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/patch_policy/patch_policy.go Escapes literal % in ExistsQuery before using fmt.Sprintf to generate patched queries.
pkg/patch_policy/patch_policy_test.go Adds test cases validating patched-query generation with LIKE patterns containing %.
ee/maintained-apps/ingesters/winget/ingester.go Introduces fuzzyMatch (bool or custom string), centralizes exists-query generation, and escapes single quotes in SQL literals.
ee/maintained-apps/ingesters/winget/ingester_test.go Adds unit tests for fuzzyMatch JSON unmarshaling and setUpExistsQuery behavior (including escaping).
ee/maintained-apps/outputs/teleport-suite/darwin.json Updates Teleport Suite Darwin bundle identifier used for exists/patched and updates install script bundle id/ref.
ee/maintained-apps/outputs/abstract/darwin.json Fixes Abstract Darwin bundle identifier in queries and updates install script to use the corrected bundle id.
ee/maintained-apps/outputs/apps.json Fixes catalog unique identifiers (e.g., Amazon Chime, Beyond Compare) to match actual bundle ids.
ee/maintained-apps/outputs/putty/windows.json Switches exists to LIKE so detection survives name/version changes.
ee/maintained-apps/outputs/postman/windows.json Switches exists to LIKE for version-agnostic detection.
ee/maintained-apps/outputs/notion/windows.json Switches exists to LIKE for version-agnostic detection.
ee/maintained-apps/outputs/gimp/windows.json Switches exists to LIKE for version-agnostic detection.
ee/maintained-apps/outputs/airtame/windows.json Switches exists to LIKE for version-agnostic detection.
ee/maintained-apps/outputs/7-zip/windows.json Switches exists to LIKE for version-agnostic detection.
ee/maintained-apps/outputs/010-editor/windows.json Switches exists to LIKE for version-agnostic detection.
ee/maintained-apps/outputs/firefox@esr/windows.json Switches exists to a custom LIKE pattern to uniquely match ESR variants.
ee/maintained-apps/inputs/winget/putty.json Updates unique_identifier and enables fuzzy matching to avoid embedding version in exists.
ee/maintained-apps/inputs/winget/postman.json Updates unique_identifier and enables fuzzy matching.
ee/maintained-apps/inputs/winget/notion.json Updates unique_identifier and enables fuzzy matching.
ee/maintained-apps/inputs/winget/gimp.json Updates unique_identifier and enables fuzzy matching.
ee/maintained-apps/inputs/winget/airtame.json Updates unique_identifier and enables fuzzy matching.
ee/maintained-apps/inputs/winget/7-zip.json Updates unique_identifier and enables fuzzy matching.
ee/maintained-apps/inputs/winget/010-editor.json Updates unique_identifier and enables fuzzy matching.
ee/maintained-apps/inputs/winget/firefox@esr.json Uses custom fuzzy pattern string for ESR-specific matching.
ee/maintained-apps/inputs/homebrew/teleport-suite.json Updates Darwin unique identifier to match corrected Teleport Suite bundle id.
ee/maintained-apps/inputs/homebrew/abstract.json Updates Darwin unique identifier to match corrected Abstract bundle id.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"refs": {
"161ebbe3": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath $INSTALLER_PATH)\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Try to launch the application\n if osascript -e \"tell application id \\\"$bundle_id\\\" to activate\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# install pkg files\nquit_and_track_application 'com.gravitational.teleport'\nsudo installer -pkg \"$TMPDIR/teleport-18.7.4.pkg\" -target /\nrelaunch_application 'com.gravitational.teleport'\n",
"42474e69": "#!/bin/sh\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n # Convert (.*) to .* for regex matching to handle patterns like (.*).com.example.app\n local regex_pattern=$(echo \"$prefix\" | sed 's/(\\.\\*)/.*/g')\n echo \"Expanding wildcard for PKGID: $PKGID (pattern: ^${regex_pattern})\"\n for receipt in $(pkgutil --pkgs | grep -E \"^${regex_pattern}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\n# Remove teleport-suite packages (handles wildcard patterns with (.*))\nremove_pkg_files '(.*).com.gravitational.teleport.tctl'\nforget_pkg '(.*).com.gravitational.teleport.tctl'\nremove_pkg_files '(.*).com.gravitational.teleport.tsh'\nforget_pkg '(.*).com.gravitational.teleport.tsh'\nremove_pkg_files 'com.gravitational.teleport'\nforget_pkg 'com.gravitational.teleport'\n\n# Explicitly remove apps from /Applications (in case pkgutil removal didn't catch them)\nsudo rm -rf '/Applications/tctl.app'\nsudo rm -rf '/Applications/tsh.app'\n\n# Remove binaries\nsudo rm -rf '/usr/local/bin/fdpass-teleport'\nsudo rm -rf '/usr/local/bin/tbot'\nsudo rm -rf '/usr/local/bin/tctl'\nsudo rm -rf '/usr/local/bin/teleport'\nsudo rm -rf '/usr/local/bin/tsh'\n\n# Remove user data\ntrash $LOGGED_IN_USER '~/.tsh'\n\n"
"42474e69": "#!/bin/sh\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n # Convert (.*) to .* for regex matching to handle patterns like (.*).com.example.app\n local regex_pattern=$(echo \"$prefix\" | sed 's/(\\.\\*)/.*/g')\n echo \"Expanding wildcard for PKGID: $PKGID (pattern: ^${regex_pattern})\"\n for receipt in $(pkgutil --pkgs | grep -E \"^${regex_pattern}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\n# Remove teleport-suite packages (handles wildcard patterns with (.*))\nremove_pkg_files '(.*).com.gravitational.teleport.tctl'\nforget_pkg '(.*).com.gravitational.teleport.tctl'\nremove_pkg_files '(.*).com.gravitational.teleport.tsh'\nforget_pkg '(.*).com.gravitational.teleport.tsh'\nremove_pkg_files 'com.gravitational.teleport'\nforget_pkg 'com.gravitational.teleport'\n\n# Explicitly remove apps from /Applications (in case pkgutil removal didn't catch them)\nsudo rm -rf '/Applications/tctl.app'\nsudo rm -rf '/Applications/tsh.app'\n\n# Remove binaries\nsudo rm -rf '/usr/local/bin/fdpass-teleport'\nsudo rm -rf '/usr/local/bin/tbot'\nsudo rm -rf '/usr/local/bin/tctl'\nsudo rm -rf '/usr/local/bin/teleport'\nsudo rm -rf '/usr/local/bin/tsh'\n\n# Remove user data\ntrash $LOGGED_IN_USER '~/.tsh'\n\n",
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedded uninstall script’s expand_pkgid_and_map logic appears incorrect for the (.*).com.gravitational.teleport.* patterns used below. It strips everything after the last * via ${PKGID%\*}, but in a pattern like (.*).com.gravitational.teleport.tsh the last * is inside (.*), so the computed prefix becomes (. and the subsequent grep -E won’t match any receipts. This will prevent remove_pkg_files/forget_pkg from expanding and removing the intended package IDs. Consider changing the wildcard expansion to convert the full pattern to a regex (without truncating at *), or switch to a wildcard form where * only appears as a trailing glob (e.g. com.gravitational.teleport.tsh*) so the existing prefix logic works.

Suggested change
"42474e69": "#!/bin/sh\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n # Convert (.*) to .* for regex matching to handle patterns like (.*).com.example.app\n local regex_pattern=$(echo \"$prefix\" | sed 's/(\\.\\*)/.*/g')\n echo \"Expanding wildcard for PKGID: $PKGID (pattern: ^${regex_pattern})\"\n for receipt in $(pkgutil --pkgs | grep -E \"^${regex_pattern}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\n# Remove teleport-suite packages (handles wildcard patterns with (.*))\nremove_pkg_files '(.*).com.gravitational.teleport.tctl'\nforget_pkg '(.*).com.gravitational.teleport.tctl'\nremove_pkg_files '(.*).com.gravitational.teleport.tsh'\nforget_pkg '(.*).com.gravitational.teleport.tsh'\nremove_pkg_files 'com.gravitational.teleport'\nforget_pkg 'com.gravitational.teleport'\n\n# Explicitly remove apps from /Applications (in case pkgutil removal didn't catch them)\nsudo rm -rf '/Applications/tctl.app'\nsudo rm -rf '/Applications/tsh.app'\n\n# Remove binaries\nsudo rm -rf '/usr/local/bin/fdpass-teleport'\nsudo rm -rf '/usr/local/bin/tbot'\nsudo rm -rf '/usr/local/bin/tctl'\nsudo rm -rf '/usr/local/bin/teleport'\nsudo rm -rf '/usr/local/bin/tsh'\n\n# Remove user data\ntrash $LOGGED_IN_USER '~/.tsh'\n\n",
"42474e69": "#!/bin/sh\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local regex_pattern=\"^${PKGID}$\"\n echo \"Expanding package pattern for PKGID: $PKGID (pattern: ${regex_pattern})\"\n for receipt in $(pkgutil --pkgs | grep -E \"$regex_pattern\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\n# Remove teleport-suite packages (handles regex package ID patterns such as (.*).com.gravitational.teleport.tsh)\nremove_pkg_files '(.*).com.gravitational.teleport.tctl'\nforget_pkg '(.*).com.gravitational.teleport.tctl'\nremove_pkg_files '(.*).com.gravitational.teleport.tsh'\nforget_pkg '(.*).com.gravitational.teleport.tsh'\nremove_pkg_files 'com.gravitational.teleport'\nforget_pkg 'com.gravitational.teleport'\n\n# Explicitly remove apps from /Applications (in case pkgutil removal didn't catch them)\nsudo rm -rf '/Applications/tctl.app'\nsudo rm -rf '/Applications/tsh.app'\n\n# Remove binaries\nsudo rm -rf '/usr/local/bin/fdpass-teleport'\nsudo rm -rf '/usr/local/bin/tbot'\nsudo rm -rf '/usr/local/bin/tctl'\nsudo rm -rf '/usr/local/bin/teleport'\nsudo rm -rf '/usr/local/bin/tsh'\n\n# Remove user data\ntrash $LOGGED_IN_USER '~/.tsh'\n\n",

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenhouchins Have you seen this issue come up anywhere?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iansltx these "suites" of apps have been challenging. I did confirm that the install and uninstall script work for this specific FMA before the proposed changes.

"exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.SweetScape.010Editor';",
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.SweetScape.010Editor' AND version_compare(bundle_short_version, '98.6.3') < 0);"
"exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.elasticprojects.abstract-desktop';",
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.elasticprojects.abstract-desktop' AND version_compare(bundle_short_version, '98.6.3') < 0);"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this!

@allenhouchins
Copy link
Copy Markdown
Member

@iansltx is the plan that the windows ingester will always put "fuzzy_match_name": true by default? Or could you at least help me understand and document when we should or should not use that logic?

@iansltx
Copy link
Copy Markdown
Contributor Author

iansltx commented Apr 10, 2026

@allenhouchins Fuzzy matching is off by default because a lot of Windows apps are well-behaved (no version number in the osquery-pulled name). You'd use this when osquery reports a name including version number. The standard fuzzy match handles cases where the version number is the last thing in the name string, while you can provide your own value for the LIKE query in case you need more control (e.g. with Firefox ESR).

@allenhouchins
Copy link
Copy Markdown
Member

@allenhouchins Fuzzy matching is off by default because a lot of Windows apps are well-behaved (no version number in the osquery-pulled name). You'd use this when osquery reports a name including version number. The standard fuzzy match handles cases where the version number is the last thing in the name string, while you can provide your own value for the LIKE query in case you need more control (e.g. with Firefox ESR).

Make sense. Thanks!

"slug": "amazon-chime/darwin",
"platform": "darwin",
"unique_identifier": "com.runningwithcrayons.Alfred",
"unique_identifier": "com.amazon.Amazon-Chime",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we eventually want to have ingesters automatically update apps.json rather than only add new apps?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah probably. Out of scope for this case though.

@iansltx iansltx merged commit 65030e9 into main Apr 10, 2026
63 checks passed
@iansltx iansltx deleted the fma-fixes branch April 10, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some Windows FMAs generate ineffectual policies due to version number in name

4 participants