From e8bb6b33b04b0d6b3903ad4c0622e350b8955729 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Thu, 9 Jan 2025 11:18:47 -0500 Subject: [PATCH 1/8] fix: fallback to bundle id if a default name is set --- .../distribution/distribution-sentinelone.xml | 155 ++++++++++++++++++ pkg/file/xar.go | 10 +- pkg/file/xar_test.go | 49 ++++-- 3 files changed, 195 insertions(+), 19 deletions(-) create mode 100644 pkg/file/testdata/distribution/distribution-sentinelone.xml diff --git a/pkg/file/testdata/distribution/distribution-sentinelone.xml b/pkg/file/testdata/distribution/distribution-sentinelone.xml new file mode 100644 index 000000000000..f1cca6211e7e --- /dev/null +++ b/pkg/file/testdata/distribution/distribution-sentinelone.xml @@ -0,0 +1,155 @@ + + + + + + + DISTRIBUTION_TITLE + + + + + + + + + + + #SentinelOne.pkg + + + + \ No newline at end of file diff --git a/pkg/file/xar.go b/pkg/file/xar.go index 31e3773b2bc3..8303121fdedd 100644 --- a/pkg/file/xar.go +++ b/pkg/file/xar.go @@ -270,7 +270,6 @@ func parseDistributionFile(rawXML []byte) (*InstallerMetadata, error) { BundleIdentifier: identifier, PackageIDs: packageIDs, }, nil - } // getDistributionInfo gets the name, bundle identifier and version of a PKG distribution file @@ -278,7 +277,7 @@ func getDistributionInfo(d *distributionXML) (name string, identifier string, ve var appVersion string // find the package ids that have an installation size - var packageIDSet = make(map[string]struct{}, 1) + packageIDSet := make(map[string]struct{}, 1) for _, pkg := range d.PkgRefs { if pkg.InstallKBytes != "" && pkg.InstallKBytes != "0" { var id string @@ -368,7 +367,9 @@ out: if name == "" && d.Title != "" { name = d.Title } - if name == "" { + // "DISTRIBUTION_TITLE" is a default title that is sometimes left in the Distribution file. Fall + // back to the bundle ID if this is the case. + if name == "" || name == "DISTRIBUTION_TITLE" { name = identifier } @@ -405,12 +406,11 @@ func parsePackageInfoFile(rawXML []byte) (*InstallerMetadata, error) { BundleIdentifier: identifier, PackageIDs: packageIDs, }, nil - } // getPackageInfo gets the name, bundle identifier and version of a PKG top level PackageInfo file func getPackageInfo(p *packageInfoXML) (name string, identifier string, version string, packageIDs []string) { - var packageIDSet = make(map[string]struct{}, 1) + packageIDSet := make(map[string]struct{}, 1) for _, bundle := range p.Bundles { installPath := bundle.Path if p.InstallLocation != "" { diff --git a/pkg/file/xar_test.go b/pkg/file/xar_test.go index 15f76dff25b7..85002b675b5c 100644 --- a/pkg/file/xar_test.go +++ b/pkg/file/xar_test.go @@ -108,8 +108,10 @@ func TestParseRealDistributionFiles(t *testing.T) { expectedName: "Microsoft Teams.app", expectedVersion: "24124.1412.2911.3341", expectedBundleID: "com.microsoft.teams2", - expectedPackageIDs: []string{"com.microsoft.teams2", "com.microsoft.package.Microsoft_AutoUpdate.app", - "com.microsoft.MSTeamsAudioDevice"}, + expectedPackageIDs: []string{ + "com.microsoft.teams2", "com.microsoft.package.Microsoft_AutoUpdate.app", + "com.microsoft.MSTeamsAudioDevice", + }, }, { file: "distribution-zoom.xml", @@ -123,8 +125,10 @@ func TestParseRealDistributionFiles(t *testing.T) { expectedName: "Adobe Acrobat Reader.app", expectedVersion: "24.002.20857", expectedBundleID: "com.adobe.Reader", - expectedPackageIDs: []string{"com.adobe.acrobat.DC.reader.app.pkg.MUI", "com.adobe.acrobat.DC.reader.appsupport.pkg.MUI", - "com.adobe.acrobat.reader.DC.reader.app.pkg.MUI", "com.adobe.armdc.app.pkg"}, + expectedPackageIDs: []string{ + "com.adobe.acrobat.DC.reader.app.pkg.MUI", "com.adobe.acrobat.DC.reader.appsupport.pkg.MUI", + "com.adobe.acrobat.reader.DC.reader.app.pkg.MUI", "com.adobe.armdc.app.pkg", + }, }, { file: "distribution-airtame.xml", @@ -138,8 +142,10 @@ func TestParseRealDistributionFiles(t *testing.T) { expectedName: "Box.app", expectedVersion: "2.38.173", expectedBundleID: "com.box.desktop", - expectedPackageIDs: []string{"com.box.desktop.installer.desktop", "com.box.desktop.installer.local.appsupport", - "com.box.desktop.installer.autoupdater", "com.box.desktop.installer.osxfuse"}, + expectedPackageIDs: []string{ + "com.box.desktop.installer.desktop", "com.box.desktop.installer.local.appsupport", + "com.box.desktop.installer.autoupdater", "com.box.desktop.installer.osxfuse", + }, }, { file: "distribution-iriunwebcam.xml", @@ -155,24 +161,30 @@ func TestParseRealDistributionFiles(t *testing.T) { expectedName: "Microsoft Excel.app", expectedVersion: "16.86", expectedBundleID: "com.microsoft.Excel", - expectedPackageIDs: []string{"com.microsoft.package.Microsoft_Excel.app", "com.microsoft.package.Microsoft_AutoUpdate.app", - "com.microsoft.pkg.licensing"}, + expectedPackageIDs: []string{ + "com.microsoft.package.Microsoft_Excel.app", "com.microsoft.package.Microsoft_AutoUpdate.app", + "com.microsoft.pkg.licensing", + }, }, { file: "distribution-microsoftword.xml", expectedName: "Microsoft Word.app", expectedVersion: "16.86", expectedBundleID: "com.microsoft.Word", - expectedPackageIDs: []string{"com.microsoft.package.Microsoft_Word.app", "com.microsoft.package.Microsoft_AutoUpdate.app", - "com.microsoft.pkg.licensing"}, + expectedPackageIDs: []string{ + "com.microsoft.package.Microsoft_Word.app", "com.microsoft.package.Microsoft_AutoUpdate.app", + "com.microsoft.pkg.licensing", + }, }, { file: "distribution-miscrosoftpowerpoint.xml", expectedName: "Microsoft PowerPoint.app", expectedVersion: "16.86", expectedBundleID: "com.microsoft.Powerpoint", - expectedPackageIDs: []string{"com.microsoft.package.Microsoft_PowerPoint.app", "com.microsoft.package.Microsoft_AutoUpdate.app", - "com.microsoft.pkg.licensing"}, + expectedPackageIDs: []string{ + "com.microsoft.package.Microsoft_PowerPoint.app", "com.microsoft.package.Microsoft_AutoUpdate.app", + "com.microsoft.pkg.licensing", + }, }, { file: "distribution-ringcentral.xml", @@ -195,6 +207,13 @@ func TestParseRealDistributionFiles(t *testing.T) { expectedBundleID: "com.bozo.zeroinstallsize", expectedPackageIDs: []string{"com.bozo.zeroinstallsize.app"}, }, + { + file: "distribution-sentinelone.xml", + expectedName: "com.sentinelone.pkg.sentinel-agent", + expectedVersion: "24.3.2.7753", + expectedBundleID: "com.sentinelone.pkg.sentinel-agent", + expectedPackageIDs: []string{"com.sentinelone.pkg.sentinel-agent"}, + }, } for _, tt := range tests { @@ -232,8 +251,10 @@ func TestParsePackageInfoFiles(t *testing.T) { expectedName: "IriunWebcam.app", expectedVersion: "2.8.10", expectedBundleID: "com.iriun.macwebcam", - expectedPackageIDs: []string{"com.iriun.macwebcam", "com.iriun.macwebcam.extension4", "com.iriun.macwebcam.extension", - "com.iriun.mic"}, + expectedPackageIDs: []string{ + "com.iriun.macwebcam", "com.iriun.macwebcam.extension4", "com.iriun.macwebcam.extension", + "com.iriun.mic", + }, }, { file: "packageInfo-scriptOnly.xml", From 28714612e3f9697f4bacc82a51c7e6faef0fdf5d Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Thu, 9 Jan 2025 14:37:30 -0500 Subject: [PATCH 2/8] feat: use the tag list to find a better name --- pkg/file/xar.go | 27 ++++++++++++++++++++++++--- pkg/file/xar_test.go | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pkg/file/xar.go b/pkg/file/xar.go index 8303121fdedd..c68a7f87fafe 100644 --- a/pkg/file/xar.go +++ b/pkg/file/xar.go @@ -110,6 +110,7 @@ type distributionXML struct { Title string `xml:"title"` Product distributionProduct `xml:"product"` PkgRefs []distributionPkgRef `xml:"pkg-ref"` + Choices []distributionChoice `xml:"choice"` } type packageInfoXML struct { @@ -135,6 +136,11 @@ type distributionPkgRef struct { InstallKBytes string `xml:"installKBytes,attr"` } +type distributionChoice struct { + PkgRef distributionPkgRef `xml:"pkg-ref"` + Title string `xml:"title,attr"` +} + // distributionBundleVersion represents the bundle-version element type distributionBundleVersion struct { Bundles []distributionBundle `xml:"bundle"` @@ -272,6 +278,14 @@ func parseDistributionFile(rawXML []byte) (*InstallerMetadata, error) { }, nil } +// Set of package names we know are incorrect. If we see these in the Distribution file we should +// try to get the name some other way. +var knownBadNames = map[string]struct{}{ + "DISTRIBUTION_TITLE": {}, + "MacFULL": {}, + "SU_TITLE": {}, +} + // getDistributionInfo gets the name, bundle identifier and version of a PKG distribution file func getDistributionInfo(d *distributionXML) (name string, identifier string, version string, packageIDs []string) { var appVersion string @@ -367,10 +381,17 @@ out: if name == "" && d.Title != "" { name = d.Title } - // "DISTRIBUTION_TITLE" is a default title that is sometimes left in the Distribution file. Fall - // back to the bundle ID if this is the case. - if name == "" || name == "DISTRIBUTION_TITLE" { + + if _, ok := knownBadNames[name]; name == "" || ok { name = identifier + + // Try to find a tag that matches the bundle ID for this app. It might have the app + // name, so if we find it we can use that. + for _, c := range d.Choices { + if c.PkgRef.ID == identifier && c.Title != "" { + name = c.Title + } + } } // for the version, try to use the top-level product version, if not, diff --git a/pkg/file/xar_test.go b/pkg/file/xar_test.go index 85002b675b5c..dcee06d9e473 100644 --- a/pkg/file/xar_test.go +++ b/pkg/file/xar_test.go @@ -209,7 +209,7 @@ func TestParseRealDistributionFiles(t *testing.T) { }, { file: "distribution-sentinelone.xml", - expectedName: "com.sentinelone.pkg.sentinel-agent", + expectedName: "SentinelOne", expectedVersion: "24.3.2.7753", expectedBundleID: "com.sentinelone.pkg.sentinel-agent", expectedPackageIDs: []string{"com.sentinelone.pkg.sentinel-agent"}, From 2d66bd5d0cac6a5eecf4cf2d731cb6c2b054a2bd Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Thu, 9 Jan 2025 14:43:28 -0500 Subject: [PATCH 3/8] chore: changes file --- changes/24873-pkg-name | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/24873-pkg-name diff --git a/changes/24873-pkg-name b/changes/24873-pkg-name new file mode 100644 index 000000000000..1e19f9bb1196 --- /dev/null +++ b/changes/24873-pkg-name @@ -0,0 +1 @@ +- Added a workaround that attempts to get the correct app name for .pkg installers that have default or incorrect app names in their metadata. \ No newline at end of file From 8c8500c195417ed52a229cf0f17d5e42a238c465 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Thu, 9 Jan 2025 17:10:16 -0500 Subject: [PATCH 4/8] Update changes/24873-pkg-name Co-authored-by: Ian Littman --- changes/24873-pkg-name | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/24873-pkg-name b/changes/24873-pkg-name index 1e19f9bb1196..a15f6b616f39 100644 --- a/changes/24873-pkg-name +++ b/changes/24873-pkg-name @@ -1 +1 @@ -- Added a workaround that attempts to get the correct app name for .pkg installers that have default or incorrect app names in their metadata. \ No newline at end of file +- Added a fallback for extracting app name from .pkg installers that have default or incorrect title attributes in their distribution file. \ No newline at end of file From e8916e6146c7404e1f30e8a63eade3a59e61848d Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Fri, 10 Jan 2025 14:38:50 -0500 Subject: [PATCH 5/8] fix: update identifier search to use the choices list if present --- .../distribution/distribution-cold-turkey.xml | 228 ++++++++++++++++++ pkg/file/xar.go | 45 +++- pkg/file/xar_test.go | 7 + 3 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 pkg/file/testdata/distribution/distribution-cold-turkey.xml diff --git a/pkg/file/testdata/distribution/distribution-cold-turkey.xml b/pkg/file/testdata/distribution/distribution-cold-turkey.xml new file mode 100644 index 000000000000..a174d3585da6 --- /dev/null +++ b/pkg/file/testdata/distribution/distribution-cold-turkey.xml @@ -0,0 +1,228 @@ + + + + + + DISTRIBUTION_TITLE + + + + + + + + + + + + + + + + + + + + + + #NMHFirefox.pkg + #NMHEdge.pkg + #NMHChrome.pkg + #Cold_Turkey_Blocker.pkg + + + \ No newline at end of file diff --git a/pkg/file/xar.go b/pkg/file/xar.go index c68a7f87fafe..48e54ab9edc9 100644 --- a/pkg/file/xar.go +++ b/pkg/file/xar.go @@ -107,10 +107,11 @@ type xmlFile struct { // distributionXML represents the structure of the distributionXML.xml type distributionXML struct { - Title string `xml:"title"` - Product distributionProduct `xml:"product"` - PkgRefs []distributionPkgRef `xml:"pkg-ref"` - Choices []distributionChoice `xml:"choice"` + Title string `xml:"title"` + Product distributionProduct `xml:"product"` + PkgRefs []distributionPkgRef `xml:"pkg-ref"` + Choices []distributionChoice `xml:"choice"` + ChoicesOutline distributionChoicesOutline `xml:"choices-outline"` } type packageInfoXML struct { @@ -139,6 +140,15 @@ type distributionPkgRef struct { type distributionChoice struct { PkgRef distributionPkgRef `xml:"pkg-ref"` Title string `xml:"title,attr"` + ID string `xml:"id,attr"` +} + +type distributionChoicesOutline struct { + Lines []distributionLine `xml:"line"` +} + +type distributionLine struct { + Choice string `xml:"choice,attr"` } // distributionBundleVersion represents the bundle-version element @@ -352,6 +362,33 @@ out: } } + // Try to get the identifier based on the choices list, if we have one. Some .pkgs have multiple + // sub-pkgs inside, so the choices list helps us be a bit smarter. + if identifier == "" && len(d.ChoicesOutline.Lines) > 0 { + choicesByID := make(map[string]distributionChoice, len(d.Choices)) + for _, c := range d.Choices { + choicesByID[c.ID] = c + } + + for _, l := range d.ChoicesOutline.Lines { + c := choicesByID[l.Choice] + for _, p := range d.PkgRefs { + if p.ID == c.PkgRef.ID { + identifier = p.PackageIdentifier + if identifier == "" { + identifier = p.ID + } + break + } + } + + if identifier != "" { + // we found it, so we can quit looping + break + } + } + } + if identifier == "" { for _, pkg := range d.PkgRefs { if pkg.PackageIdentifier != "" { diff --git a/pkg/file/xar_test.go b/pkg/file/xar_test.go index dcee06d9e473..bd31b0eb6352 100644 --- a/pkg/file/xar_test.go +++ b/pkg/file/xar_test.go @@ -214,6 +214,13 @@ func TestParseRealDistributionFiles(t *testing.T) { expectedBundleID: "com.sentinelone.pkg.sentinel-agent", expectedPackageIDs: []string{"com.sentinelone.pkg.sentinel-agent"}, }, + { + file: "distribution-cold-turkey.xml", + expectedName: "Cold Turkey Blocker", + expectedVersion: "4.5", + expectedBundleID: "com.getcoldturkey.coldturkeyblocker", + expectedPackageIDs: []string{"com.getcoldturkey.coldturkeyblocker", "com.getcoldturkey.blocker-firefox-ext", "com.getcoldturkey.blocker-edge-ext", "com.getcoldturkey.blocker-chrome-ext"}, + }, } for _, tt := range tests { From 073d0bc388998ae620c09b1fe5a5c4408bc6791c Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Fri, 10 Jan 2025 14:45:48 -0500 Subject: [PATCH 6/8] chore: remove unneeded code block --- pkg/file/xar.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pkg/file/xar.go b/pkg/file/xar.go index 48e54ab9edc9..5181caf45e49 100644 --- a/pkg/file/xar.go +++ b/pkg/file/xar.go @@ -389,20 +389,6 @@ out: } } - if identifier == "" { - for _, pkg := range d.PkgRefs { - if pkg.PackageIdentifier != "" { - identifier = pkg.PackageIdentifier - break - } - - if pkg.ID != "" { - identifier = pkg.ID - break - } - } - } - // if the identifier is still empty, try to use the product id if identifier == "" && d.Product.ID != "" { identifier = d.Product.ID From 126415bf9e72bd7ca76b8184595965cf4c3d68fb Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Fri, 10 Jan 2025 14:53:05 -0500 Subject: [PATCH 7/8] Revert "chore: remove unneeded code block" This reverts commit 073d0bc388998ae620c09b1fe5a5c4408bc6791c. --- pkg/file/xar.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/file/xar.go b/pkg/file/xar.go index 5181caf45e49..48e54ab9edc9 100644 --- a/pkg/file/xar.go +++ b/pkg/file/xar.go @@ -389,6 +389,20 @@ out: } } + if identifier == "" { + for _, pkg := range d.PkgRefs { + if pkg.PackageIdentifier != "" { + identifier = pkg.PackageIdentifier + break + } + + if pkg.ID != "" { + identifier = pkg.ID + break + } + } + } + // if the identifier is still empty, try to use the product id if identifier == "" && d.Product.ID != "" { identifier = d.Product.ID From 0ecdc5651b827c4aac666f8faeecd15afe0779b9 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Fri, 10 Jan 2025 15:48:18 -0500 Subject: [PATCH 8/8] chore: add clarifying comment --- pkg/file/xar.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/file/xar.go b/pkg/file/xar.go index 48e54ab9edc9..c2e5cc49bfe2 100644 --- a/pkg/file/xar.go +++ b/pkg/file/xar.go @@ -372,6 +372,9 @@ out: for _, l := range d.ChoicesOutline.Lines { c := choicesByID[l.Choice] + // Note: we can't create a map of pkg-refs by ID like we do for the choices above + // because different pkg-refs can have the same ID attribute. See distribution-go.xml + // for an example of this (this case is covered in tests). for _, p := range d.PkgRefs { if p.ID == c.PkgRef.ID { identifier = p.PackageIdentifier