Improve .pkg metadata extraction for names and bundle IDs, let custom package metadata extraction tool check an entire directory at a time (#29249)

For #24083, #26597.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [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/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)
- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Ian Littman
2025-05-19 10:32:36 -05:00
committed by GitHub
parent 7b20209f53
commit 980adc0c45
7 changed files with 192 additions and 119 deletions
+1
View File
@@ -0,0 +1 @@
* Revised PKG custom package parsing to pick the correct app name and bundle ID in more instances.
+47 -16
View File
@@ -29,6 +29,7 @@ import (
"fmt"
"io"
"path/filepath"
"slices"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
@@ -362,20 +363,33 @@ func getDistributionInfo(d *distributionXML) (name string, identifier string, ve
packageIDs = append(packageIDs, id)
}
out:
// look in all the bundle versions for one that has a `path` attribute
// that is not nested, this is generally the case for packages that distribute
// `.app` files, which are ultimately picked up as an installed app by osquery
var potentialBundles []distributionBundle
for _, pkg := range d.PkgRefs {
for _, versions := range pkg.BundleVersions {
for _, bundle := range versions.Bundles {
if base, isValid := isValidAppFilePath(bundle.Path); isValid {
identifier = bundle.ID
name = base
appVersion = bundle.CFBundleShortVersionString
break out
}
}
potentialBundles = append(potentialBundles, versions.Bundles...)
}
}
// Prefer paths that refer to Applications for name, bundle ID, etc.
slices.SortFunc(potentialBundles, func(a distributionBundle, b distributionBundle) int {
if strings.HasPrefix(a.Path, "Applications/") && !strings.HasPrefix(b.Path, "Applications/") {
return -1
}
if strings.HasPrefix(b.Path, "Applications/") && !strings.HasPrefix(a.Path, "Applications/") {
return 1
}
return 0
})
for _, bundle := range potentialBundles {
if base, isValid := isValidAppFilePath(bundle.Path); isValid {
identifier = bundle.ID
name = strings.TrimSuffix(base, ".app")
appVersion = bundle.CFBundleShortVersionString
break
}
}
@@ -391,6 +405,14 @@ out:
}
}
// if the identifier is still empty, try to use the product id, and make sure it's in the package IDs list
if identifier == "" && d.Product.ID != "" {
identifier = d.Product.ID
if !slices.Contains(packageIDs, identifier) {
packageIDs = append(packageIDs, identifier)
}
}
// 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 {
@@ -435,11 +457,6 @@ out:
}
}
// if the identifier is still empty, try to use the product id
if identifier == "" && d.Product.ID != "" {
identifier = d.Product.ID
}
// if package IDs are still empty, use the identifier as the package ID
if len(packageIDs) == 0 && identifier != "" {
packageIDs = append(packageIDs, identifier)
@@ -451,9 +468,11 @@ out:
name = d.Title
}
if _, ok := knownBadNames[name]; name == "" || ok {
name = identifier
if _, ok := knownBadNames[name]; ok {
name = ""
}
if name == "" {
// Try to find a <choice> 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 {
@@ -463,6 +482,18 @@ out:
}
}
if name == "" { // Fall back to any bundle ID in packages for name matching vs. choices
for _, c := range d.Choices {
if slices.Contains(packageIDs, c.PkgRef.ID) && c.Title != "" {
name = c.Title
}
}
}
if name == "" { // fall back to bundle ID
name = identifier
}
// for the version, try to use the top-level product version, if not,
// fallback to any version definition alongside the name or the first
// version in a pkg-ref we find.
+17 -17
View File
@@ -63,28 +63,28 @@ func TestParseRealDistributionFiles(t *testing.T) {
}{
{
file: "distribution-1password.xml",
expectedName: "1Password.app",
expectedName: "1Password",
expectedVersion: "8.10.34",
expectedBundleID: "com.1password.1password",
expectedPackageIDs: []string{"com.1password.1password"},
},
{
file: "distribution-chrome.xml",
expectedName: "Google Chrome.app",
expectedName: "Google Chrome",
expectedVersion: "126.0.6478.62",
expectedBundleID: "com.google.Chrome",
expectedPackageIDs: []string{"com.google.Chrome"},
},
{
file: "distribution-edge.xml",
expectedName: "Microsoft Edge.app",
expectedName: "Microsoft Edge",
expectedVersion: "126.0.2592.56",
expectedBundleID: "com.microsoft.edgemac",
expectedPackageIDs: []string{"com.microsoft.edgemac"},
},
{
file: "distribution-firefox.xml",
expectedName: "Firefox.app",
expectedName: "Firefox",
expectedVersion: "99.0",
expectedBundleID: "org.mozilla.firefox",
expectedPackageIDs: []string{"org.mozilla.firefox"},
@@ -105,7 +105,7 @@ func TestParseRealDistributionFiles(t *testing.T) {
},
{
file: "distribution-microsoft-teams.xml",
expectedName: "Microsoft Teams.app",
expectedName: "Microsoft Teams",
expectedVersion: "24124.1412.2911.3341",
expectedBundleID: "com.microsoft.teams2",
expectedPackageIDs: []string{
@@ -115,14 +115,14 @@ func TestParseRealDistributionFiles(t *testing.T) {
},
{
file: "distribution-zoom.xml",
expectedName: "zoom.us.app",
expectedName: "zoom.us",
expectedVersion: "6.0.11.35001",
expectedBundleID: "us.zoom.xos",
expectedPackageIDs: []string{"us.zoom.pkg.videomeeting"},
},
{
file: "distribution-acrobatreader.xml",
expectedName: "Adobe Acrobat Reader.app",
expectedName: "Adobe Acrobat Reader",
expectedVersion: "24.002.20857",
expectedBundleID: "com.adobe.Reader",
expectedPackageIDs: []string{
@@ -132,14 +132,14 @@ func TestParseRealDistributionFiles(t *testing.T) {
},
{
file: "distribution-airtame.xml",
expectedName: "Airtame.app",
expectedName: "Airtame",
expectedVersion: "4.10.1",
expectedBundleID: "com.airtame.airtame-application",
expectedPackageIDs: []string{"com.airtame.airtame-application"},
},
{
file: "distribution-boxdrive.xml",
expectedName: "Box.app",
expectedName: "Box",
expectedVersion: "2.38.173",
expectedBundleID: "com.box.desktop",
expectedPackageIDs: []string{
@@ -149,7 +149,7 @@ func TestParseRealDistributionFiles(t *testing.T) {
},
{
file: "distribution-iriunwebcam.xml",
expectedName: "IriunWebcam.app",
expectedName: "IriunWebcam",
expectedVersion: "2.8.8",
expectedBundleID: "com.iriun.macwebcam",
// Note: "com.iriun.pkg.multicam" is part of the installer package, but it is not actually installed by default.
@@ -158,7 +158,7 @@ func TestParseRealDistributionFiles(t *testing.T) {
},
{
file: "distribution-microsoftexcel.xml",
expectedName: "Microsoft Excel.app",
expectedName: "Microsoft Excel",
expectedVersion: "16.86",
expectedBundleID: "com.microsoft.Excel",
expectedPackageIDs: []string{
@@ -168,7 +168,7 @@ func TestParseRealDistributionFiles(t *testing.T) {
},
{
file: "distribution-microsoftword.xml",
expectedName: "Microsoft Word.app",
expectedName: "Microsoft Word",
expectedVersion: "16.86",
expectedBundleID: "com.microsoft.Word",
expectedPackageIDs: []string{
@@ -178,7 +178,7 @@ func TestParseRealDistributionFiles(t *testing.T) {
},
{
file: "distribution-miscrosoftpowerpoint.xml",
expectedName: "Microsoft PowerPoint.app",
expectedName: "Microsoft PowerPoint",
expectedVersion: "16.86",
expectedBundleID: "com.microsoft.Powerpoint",
expectedPackageIDs: []string{
@@ -188,7 +188,7 @@ func TestParseRealDistributionFiles(t *testing.T) {
},
{
file: "distribution-ringcentral.xml",
expectedName: "RingCentral.app",
expectedName: "RingCentral",
expectedVersion: "24.1.32.9774",
expectedBundleID: "com.ringcentral.glip",
expectedPackageIDs: []string{"com.ringcentral.glip"},
@@ -202,7 +202,7 @@ func TestParseRealDistributionFiles(t *testing.T) {
},
{
file: "test-zero-installkbytes.xml",
expectedName: "ZeroInstallSize.app",
expectedName: "ZeroInstallSize",
expectedVersion: "1.2.3",
expectedBundleID: "com.bozo.zeroinstallsize",
expectedPackageIDs: []string{"com.bozo.zeroinstallsize.app"},
@@ -211,8 +211,8 @@ func TestParseRealDistributionFiles(t *testing.T) {
file: "distribution-sentinelone.xml",
expectedName: "SentinelOne",
expectedVersion: "24.3.2.7753",
expectedBundleID: "com.sentinelone.pkg.sentinel-agent",
expectedPackageIDs: []string{"com.sentinelone.pkg.sentinel-agent"},
expectedBundleID: "com.sentinelone.sentinel-agent",
expectedPackageIDs: []string{"com.sentinelone.pkg.sentinel-agent", "com.sentinelone.sentinel-agent"},
},
{
file: "distribution-cold-turkey.xml",
+25 -25
View File
@@ -12484,7 +12484,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
// Upload another package for another platform
payloadDummy := &fleet.UploadSoftwareInstallerPayload{
Filename: "dummy_installer.pkg",
Title: "DummyApp.app",
Title: "DummyApp",
TeamID: teamID,
}
s.uploadSoftwareInstaller(t, payloadDummy, http.StatusOK, "")
@@ -13546,7 +13546,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGNewSoftwareTitleFlow() {
{Name: "foo", Version: "0.0.1", Source: "homebrew"},
{Name: "foo", Version: "0.0.3", Source: "homebrew"},
{Name: "bar", Version: "0.0.4", Source: "apps"},
{Name: "DummyApp.app", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.example.dummy"},
{Name: "DummyApp", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.example.dummy"},
}
_, err = s.ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
@@ -13577,7 +13577,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGNewSoftwareTitleFlow() {
require.Len(t, resp.SoftwareTitles, 3)
require.ElementsMatch(
t,
[]string{"foo", "bar", "DummyApp.app"},
[]string{"foo", "bar", "DummyApp"},
[]string{
resp.SoftwareTitles[0].Name,
resp.SoftwareTitles[1].Name,
@@ -13591,8 +13591,8 @@ func (s *integrationEnterpriseTestSuite) TestPKGNewSoftwareTitleFlow() {
{Name: "foo", Version: "0.0.1", Source: "homebrew"},
{Name: "foo", Version: "0.0.3", Source: "homebrew"},
{Name: "bar", Version: "0.0.4", Source: "apps"},
{Name: "DummyApp.app", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.example.dummy"},
{Name: "AppDummy.app", Version: "2.0.0", Source: "apps", BundleIdentifier: "com.example.dummy"},
{Name: "DummyApp", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.example.dummy"},
{Name: "AppDummy", Version: "2.0.0", Source: "apps", BundleIdentifier: "com.example.dummy"},
}
_, err = s.ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
@@ -13613,7 +13613,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGNewSoftwareTitleFlow() {
require.Len(t, resp.SoftwareTitles, 3)
require.ElementsMatch(
t,
[]string{"foo", "bar", "DummyApp.app"},
[]string{"foo", "bar", "DummyApp"},
[]string{
resp.SoftwareTitles[0].Name,
resp.SoftwareTitles[1].Name,
@@ -13645,7 +13645,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGNoVersion() {
"team_id", fmt.Sprintf("%d", team.ID),
)
require.Len(t, resp.SoftwareTitles, 1)
require.Equal(t, "NoVersion.app", resp.SoftwareTitles[0].Name)
require.Equal(t, "NoVersion", resp.SoftwareTitles[0].Name)
require.Equal(t, "", resp.SoftwareTitles[0].SoftwarePackage.Version)
}
@@ -13759,7 +13759,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGSoftwareAlreadyReported() {
{Name: "foo", Version: "0.0.3", Source: "homebrew"},
{Name: "bar", Version: "0.0.4", Source: "apps"},
// note: the source is not "apps"
{Name: "DummyApp.app", Version: "1.0.0", Source: "homebrew", BundleIdentifier: "com.example.dummy"},
{Name: "DummyApp", Version: "1.0.0", Source: "homebrew", BundleIdentifier: "com.example.dummy"},
}
_, err = s.ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
@@ -13780,7 +13780,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGSoftwareAlreadyReported() {
require.Len(t, resp.SoftwareTitles, 3)
require.ElementsMatch(
t,
[]string{"foo", "bar", "DummyApp.app"},
[]string{"foo", "bar", "DummyApp"},
[]string{
resp.SoftwareTitles[0].Name,
resp.SoftwareTitles[1].Name,
@@ -13805,7 +13805,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGSoftwareAlreadyReported() {
require.Len(t, resp.SoftwareTitles, 3)
require.ElementsMatch(
t,
[]string{"foo", "bar", "DummyApp.app"},
[]string{"foo", "bar", "DummyApp"},
[]string{
resp.SoftwareTitles[0].Name,
resp.SoftwareTitles[1].Name,
@@ -13845,7 +13845,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGSoftwareReconciliation() {
{Name: "foo", Version: "0.0.3", Source: "homebrew"},
{Name: "bar", Version: "0.0.4", Source: "apps"},
// note: the source is not "apps"
{Name: "DummyApp.app", Version: "1.0.0", Source: "homebrew", BundleIdentifier: "com.example.dummy"},
{Name: "DummyApp", Version: "1.0.0", Source: "homebrew", BundleIdentifier: "com.example.dummy"},
}
_, err = s.ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
@@ -13870,7 +13870,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGSoftwareReconciliation() {
require.Len(t, resp.SoftwareTitles, 1)
require.ElementsMatch(
t,
[]string{"DummyApp.app"},
[]string{"DummyApp"},
[]string{resp.SoftwareTitles[0].Name},
)
@@ -13888,7 +13888,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGSoftwareReconciliation() {
require.Len(t, resp.SoftwareTitles, 3)
require.ElementsMatch(
t,
[]string{"foo", "bar", "DummyApp.app"},
[]string{"foo", "bar", "DummyApp"},
[]string{
resp.SoftwareTitles[0].Name,
resp.SoftwareTitles[1].Name,
@@ -14787,7 +14787,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
"GET", "/api/latest/fleet/software/titles",
listSoftwareTitlesRequest{},
http.StatusOK, &resp,
"query", "DummyApp.app",
"query", "DummyApp",
"team_id", fmt.Sprintf("%d", team1.ID),
)
require.Len(t, resp.SoftwareTitles, 1)
@@ -14938,7 +14938,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
// Populate software for host1Team1 (to have a software title
// that doesn't have an associated installer)
software := []fleet.Software{
{Name: "Foobar.app", Version: "0.0.1", Source: "apps"},
{Name: "Foobar", Version: "0.0.1", Source: "apps"},
}
_, err = s.ds.UpdateHostSoftware(ctx, host1Team1.ID, software)
require.NoError(t, err)
@@ -14951,7 +14951,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
"GET", "/api/latest/fleet/software/titles",
listSoftwareTitlesRequest{},
http.StatusOK, &resp,
"query", "Foobar.app",
"query", "Foobar",
"team_id", fmt.Sprintf("%d", team1.ID),
)
require.Len(t, resp.SoftwareTitles, 1)
@@ -15383,7 +15383,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
"status": "installed",
"policy_id": %d,
"policy_name": "%s"
}`, host1Team1.ID, host1Team1.DisplayName(), "DummyApp.app", "dummy_installer.pkg", host1LastInstall.ExecutionID, policy1Team1.ID, policy1Team1.Name), 0)
}`, host1Team1.ID, host1Team1.DisplayName(), "DummyApp", "dummy_installer.pkg", host1LastInstall.ExecutionID, policy1Team1.ID, policy1Team1.Name), 0)
// host2Team1 posts the installation result for ruby.deb.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
@@ -16315,7 +16315,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallersWithoutBundleIden
require.NoError(t, err)
software := []fleet.Software{
{Name: "DummyApp.app", Version: "0.0.2", Source: "apps"},
{Name: "DummyApp", Version: "0.0.2", Source: "apps"},
}
// we must ingest the title with an empty bundle identifier for this
// test to be valid
@@ -17132,7 +17132,7 @@ func (s *integrationEnterpriseTestSuite) TestAutomaticPolicies() {
require.Len(t, ts.InheritedPolicies, 0)
// Delete and try again with automatic policy turned on.
pkgTitleID := getSoftwareTitleID(t, s.ds, "DummyApp.app", "apps")
pkgTitleID := getSoftwareTitleID(t, s.ds, "DummyApp", "apps")
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", pkgTitleID), nil, http.StatusNoContent,
"team_id", fmt.Sprintf("%d", team1.ID))
@@ -17145,20 +17145,20 @@ func (s *integrationEnterpriseTestSuite) TestAutomaticPolicies() {
}
s.uploadSoftwareInstaller(t, pkgPayload, http.StatusOK, "")
pkgTitleID = getSoftwareTitleID(t, s.ds, "DummyApp.app", "apps")
pkgTitleID = getSoftwareTitleID(t, s.ds, "DummyApp", "apps")
respTitle := getSoftwareTitleResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d?team_id=%d", pkgTitleID, team1.ID), listSoftwareTitlesRequest{}, http.StatusOK, &respTitle)
require.NotNil(t, respTitle.SoftwareTitle)
require.NotNil(t, respTitle.SoftwareTitle.SoftwarePackage)
require.Len(t, respTitle.SoftwareTitle.SoftwarePackage.AutomaticInstallPolicies, 1)
require.Equal(t, "[Install software] DummyApp.app (pkg)", respTitle.SoftwareTitle.SoftwarePackage.AutomaticInstallPolicies[0].Name)
require.Equal(t, "[Install software] DummyApp (pkg)", respTitle.SoftwareTitle.SoftwarePackage.AutomaticInstallPolicies[0].Name)
// Check a policy was created on team1.
ts = listTeamPoliciesResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), nil, http.StatusOK, &ts)
require.Len(t, ts.Policies, 1)
require.Len(t, ts.InheritedPolicies, 0)
require.Equal(t, "[Install software] DummyApp.app (pkg)", ts.Policies[0].Name)
require.Equal(t, "[Install software] DummyApp (pkg)", ts.Policies[0].Name)
// Upload dummy_installer.pkg to team2 with automatic policy.
pkgPayload = &fleet.UploadSoftwareInstallerPayload{
@@ -17174,7 +17174,7 @@ func (s *integrationEnterpriseTestSuite) TestAutomaticPolicies() {
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team2.ID), nil, http.StatusOK, &ts)
require.Len(t, ts.Policies, 1)
require.Len(t, ts.InheritedPolicies, 0)
require.Equal(t, "[Install software] DummyApp.app (pkg)", ts.Policies[0].Name)
require.Equal(t, "[Install software] DummyApp (pkg)", ts.Policies[0].Name)
// Upload ruby.deb to team1 with automatic policy.
payloadRubyDEB := &fleet.UploadSoftwareInstallerPayload{
@@ -17561,7 +17561,7 @@ func (s *integrationEnterpriseTestSuite) TestBatchSoftwareUploadWithSHAs() {
require.NotNil(t, pkgTitleID)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *pkgTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team2.ID))
require.NotNil(t, stResp.SoftwareTitle.SoftwarePackage)
require.Equal(t, "DummyApp.app", stResp.SoftwareTitle.Name)
require.Equal(t, "DummyApp", stResp.SoftwareTitle.Name)
require.Equal(t, pkgURL, stResp.SoftwareTitle.SoftwarePackage.URL)
require.Equal(t, softwareToInstall[2].InstallScript, stResp.SoftwareTitle.SoftwarePackage.InstallScript)
require.Equal(t, softwareToInstall[2].UninstallScript, stResp.SoftwareTitle.SoftwarePackage.UninstallScript)
@@ -17630,7 +17630,7 @@ done
require.NotNil(t, pkgTitleID)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *pkgTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team2.ID))
require.NotNil(t, stResp.SoftwareTitle.SoftwarePackage)
require.Equal(t, "DummyApp.app", stResp.SoftwareTitle.Name)
require.Equal(t, "DummyApp", stResp.SoftwareTitle.Name)
require.Equal(t, pkgURL, stResp.SoftwareTitle.SoftwarePackage.URL)
require.Equal(t, file.GetInstallScript("pkg"), stResp.SoftwareTitle.SoftwarePackage.InstallScript)
require.Equal(t, expectedUninstallScript, stResp.SoftwareTitle.SoftwarePackage.UninstallScript)
+16 -16
View File
@@ -2050,7 +2050,7 @@ func (s *integrationMDMTestSuite) createTeamDeviceForSetupExperienceWithProfileS
payloadDummy := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
Filename: "dummy_installer.pkg",
Title: "DummyApp.app",
Title: "DummyApp",
TeamID: &tm.ID,
}
s.uploadSoftwareInstaller(t, payloadDummy, http.StatusOK, "")
@@ -2236,7 +2236,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -2300,7 +2300,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
statusResp = getOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
// Software is now running, script is still pending
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -2328,7 +2328,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
// no MDM command got enqueued due to the /status call (device not released yet)
@@ -2339,7 +2339,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
// Software is installed, now we should run the script
statusResp = getOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -2410,7 +2410,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
// release of the device, as all setup experience steps are now complete.
statusResp = getOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -2570,7 +2570,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptFo
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -3309,7 +3309,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
// Add the VPP app to setup experience
vppTitleID := getSoftwareTitleID(t, s.ds, "App 5", "apps")
installerTitleID := getSoftwareTitleID(t, s.ds, "DummyApp.app", "apps")
installerTitleID := getSoftwareTitleID(t, s.ds, "DummyApp", "apps")
var swInstallResp putSetupExperienceSoftwareResponse
s.DoJSON("PUT", "/api/v1/fleet/setup_experience/software", putSetupExperienceSoftwareRequest{TeamID: team.ID, TitleIDs: []uint{vppTitleID, installerTitleID}}, http.StatusOK, &swInstallResp)
@@ -3404,7 +3404,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 2)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -3452,7 +3452,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 2)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
require.Equal(t, "App 5", statusResp.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[1].Status)
@@ -3466,7 +3466,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 2)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
// App 5 has no licenses available, so we should get a status failed here and setup experience
@@ -3477,7 +3477,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
// Software installations are done, now we should run the script
statusResp = getOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -3508,7 +3508,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
// release of the device, as all setup experience steps are now complete.
statusResp = getOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -3597,7 +3597,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowCancelScript() {
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -3644,7 +3644,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowCancelScript() {
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
@@ -3688,7 +3688,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowCancelScript() {
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
+3 -3
View File
@@ -15934,7 +15934,7 @@ func (s *integrationMDMTestSuite) TestCancelUpcomingActivity() {
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
Filename: "dummy_installer.pkg",
Title: "DummyApp.app",
Title: "DummyApp",
}
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
swTitleID := getSoftwareTitleID(t, s.ds, payload.Title, "apps")
@@ -16011,7 +16011,7 @@ func (s *integrationMDMTestSuite) TestCancelUpcomingActivity() {
// cancel the uninstall, confirm canceled activity
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", mdmHost.ID, hostActivitiesResp.Activities[1].UUID), nil, http.StatusNoContent)
lastCanceledActID = s.lastActivityOfTypeMatches(fleet.ActivityTypeCanceledUninstallSoftware{}.ActivityName(),
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "DummyApp.app", "software_title_id": %d}`, mdmHost.ID, mdmHost.DisplayName(), swTitleID), 0)
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "DummyApp", "software_title_id": %d}`, mdmHost.ID, mdmHost.DisplayName(), swTitleID), 0)
// record an uninstall result post-cancelation
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
@@ -16064,7 +16064,7 @@ func (s *integrationMDMTestSuite) TestCancelUpcomingActivity() {
// cancel the software install, confirm canceled activity
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", mdmHost.ID, hostActivitiesResp.Activities[1].UUID), nil, http.StatusNoContent)
lastCanceledActID = s.lastActivityOfTypeMatches(fleet.ActivityTypeCanceledInstallSoftware{}.ActivityName(),
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "DummyApp.app", "software_title_id": %d}`, mdmHost.ID, mdmHost.DisplayName(), swTitleID), 0)
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "DummyApp", "software_title_id": %d}`, mdmHost.ID, mdmHost.DisplayName(), swTitleID), 0)
// record a software install result post-cancelation
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
+83 -42
View File
@@ -6,6 +6,8 @@ import (
"log"
"net/http"
"os"
"path/filepath"
"slices"
"strings"
"github.com/fleetdm/fleet/v4/pkg/file"
@@ -15,62 +17,101 @@ import (
func main() {
url := flag.String("url", "", "URL of the custom package")
path := flag.String("path", "", "File path of the custom package")
path := flag.String("path", "", "File path of the custom package (or a directory of packages)")
flag.Parse()
if *url == "" && *path == "" {
log.Fatal("missing -url or -path argument")
}
if *url != "" && *path != "" {
log.Fatal("cannot set both -url and -path")
}
switch {
case *url != "" && *path != "":
log.Fatalf("-url and -path are mutually exclusive")
case *url != "":
metadata, err := processPackageFromUrl(*url)
if err != nil {
log.Fatal(err)
}
output(metadata)
case *path != "":
pathInfo, err := os.Stat(*path)
if err != nil {
log.Fatal(err)
}
if pathInfo.IsDir() {
files, err := os.ReadDir(*path)
if err != nil {
log.Fatal(err)
}
metadata, err := processPackage(*url, *path)
if err != nil {
log.Fatal(err)
}
for _, selectedFile := range files {
if selectedFile.IsDir() || strings.HasPrefix(selectedFile.Name(), ".") {
continue
}
fmt.Printf("File: %s\n", selectedFile.Name())
metadata, err := processPackageFromLocal(filepath.Join(*path, selectedFile.Name()))
if err != nil {
log.Fatal(err)
}
output(metadata)
}
} else {
metadata, err := processPackageFromLocal(*path)
if err != nil {
log.Fatal(err)
}
output(metadata)
}
default:
flag.Usage()
}
}
func output(metadata *file.InstallerMetadata) {
slices.Sort(metadata.PackageIDs)
fmt.Printf(
"- Name: '%s'\n- Bundle Identifier: '%s'\n- Package IDs: '%s'\n- Version: %s\n\n",
metadata.Name, metadata.BundleIdentifier, strings.Join(metadata.PackageIDs, ","), metadata.Version,
)
}
func processPackage(url, path string) (*file.InstallerMetadata, error) {
var tfr *fleet.TempFileReader
if url != "" {
client := fleethttp.NewClient()
client.Transport = fleethttp.NewSizeLimitTransport(fleet.MaxSoftwareInstallerSize)
func processPackageFromUrl(url string) (*file.InstallerMetadata, error) {
client := fleethttp.NewClient()
client.Transport = fleethttp.NewSizeLimitTransport(fleet.MaxSoftwareInstallerSize)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("create http request: %s", err)
}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("create http request: %s", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("get request: %s", err)
}
defer resp.Body.Close()
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("get request: %s", err)
}
defer resp.Body.Close()
// Allow all 2xx and 3xx status codes in this pass.
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("get request failed with status: %d", resp.StatusCode)
}
// Allow all 2xx and 3xx status codes in this pass.
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("get request failed with status: %d", resp.StatusCode)
}
tfr, err = fleet.NewTempFileReader(resp.Body, nil)
if err != nil {
return nil, fmt.Errorf("reading custom package: %d", resp.StatusCode)
}
defer tfr.Close()
} else { // -path
fp, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open file: %s", err)
}
tfr = &fleet.TempFileReader{
File: fp,
}
tfr, err := fleet.NewTempFileReader(resp.Body, nil)
if err != nil {
return nil, fmt.Errorf("reading custom package: %d", resp.StatusCode)
}
defer tfr.Close()
metadata, err := file.ExtractInstallerMetadata(tfr)
if err != nil {
return nil, fmt.Errorf("extract installer metadata: %s", err)
}
return metadata, nil
}
func processPackageFromLocal(path string) (*file.InstallerMetadata, error) {
fp, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open file: %s", err)
}
tfr := &fleet.TempFileReader{
File: fp,
}
metadata, err := file.ExtractInstallerMetadata(tfr)