diff --git a/changes/24873-pkg-name b/changes/24873-pkg-name new file mode 100644 index 000000000000..a15f6b616f39 --- /dev/null +++ b/changes/24873-pkg-name @@ -0,0 +1 @@ +- 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 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/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..c2e5cc49bfe2 100644 --- a/pkg/file/xar.go +++ b/pkg/file/xar.go @@ -107,9 +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"` + 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 { @@ -135,6 +137,20 @@ type distributionPkgRef struct { InstallKBytes string `xml:"installKBytes,attr"` } +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 type distributionBundleVersion struct { Bundles []distributionBundle `xml:"bundle"` @@ -270,7 +286,14 @@ func parseDistributionFile(rawXML []byte) (*InstallerMetadata, error) { BundleIdentifier: identifier, PackageIDs: packageIDs, }, 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 @@ -278,7 +301,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 @@ -339,6 +362,36 @@ 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] + // 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 + 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 != "" { @@ -368,8 +421,17 @@ out: if name == "" && d.Title != "" { name = d.Title } - if name == "" { + + 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, @@ -405,12 +467,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..bd31b0eb6352 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,20 @@ func TestParseRealDistributionFiles(t *testing.T) { expectedBundleID: "com.bozo.zeroinstallsize", expectedPackageIDs: []string{"com.bozo.zeroinstallsize.app"}, }, + { + file: "distribution-sentinelone.xml", + expectedName: "SentinelOne", + expectedVersion: "24.3.2.7753", + 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 { @@ -232,8 +258,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",