From 669d68f70e3bc63212f14d161b57156ae9fb2250 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Wed, 2 Oct 2024 16:43:51 -0300 Subject: [PATCH] fma missing pieces --- ee/server/service/maintained_apps.go | 30 +- ee/server/service/software_installers.go | 4 +- frontend/interfaces/package_type.ts | 15 +- .../PackageAdvancedOptions.tsx | 5 + frontend/services/entities/software.ts | 2 + server/datastore/mysql/maintained_apps.go | 4 +- server/fleet/service.go | 2 +- server/mdm/maintainedapps/ingest.go | 40 +- server/mdm/maintainedapps/scripts.go | 437 ++++++++++++++++++ server/mdm/maintainedapps/scripts_test.go | 72 +++ .../scripts/1password_install.golden.sh | 10 + .../scripts/1password_uninstall.golden.sh | 54 +++ .../adobe-acrobat-reader_install.golden.sh | 13 + .../adobe-acrobat-reader_uninstall.golden.sh | 124 +++++ .../scripts/box-drive_install.golden.sh | 8 + .../scripts/box-drive_uninstall.golden.sh | 122 +++++ .../scripts/brave-browser_install.golden.sh | 13 + .../scripts/brave-browser_uninstall.golden.sh | 33 ++ .../scripts/cloudflare-warp_install.golden.sh | 8 + .../cloudflare-warp_uninstall.golden.sh | 120 +++++ .../testdata/scripts/docker_install.golden.sh | 21 + .../scripts/docker_uninstall.golden.sh | 150 ++++++ .../testdata/scripts/figma_install.golden.sh | 10 + .../scripts/figma_uninstall.golden.sh | 33 ++ .../scripts/firefox_install.golden.sh | 13 + .../scripts/firefox_uninstall.golden.sh | 83 ++++ .../scripts/google-chrome_install.golden.sh | 13 + .../scripts/google-chrome_uninstall.golden.sh | 100 ++++ .../scripts/microsoft-edge_install.golden.sh | 32 ++ .../microsoft-edge_uninstall.golden.sh | 89 ++++ .../scripts/microsoft-excel_install.golden.sh | 32 ++ .../microsoft-excel_uninstall.golden.sh | 119 +++++ .../scripts/microsoft-teams_install.golden.sh | 32 ++ .../microsoft-teams_uninstall.golden.sh | 139 ++++++ .../scripts/microsoft-word_install.golden.sh | 32 ++ .../microsoft-word_uninstall.golden.sh | 118 +++++ .../testdata/scripts/notion_install.golden.sh | 13 + .../scripts/notion_uninstall.golden.sh | 36 ++ .../scripts/postman_install.golden.sh | 10 + .../scripts/postman_uninstall.golden.sh | 36 ++ .../testdata/scripts/slack_install.golden.sh | 13 + .../scripts/slack_uninstall.golden.sh | 80 ++++ .../scripts/teamviewer_install.golden.sh | 8 + .../scripts/teamviewer_uninstall.golden.sh | 131 ++++++ .../visual-studio-code_install.golden.sh | 10 + .../visual-studio-code_uninstall.golden.sh | 122 +++++ .../scripts/whatsapp_install.golden.sh | 10 + .../scripts/whatsapp_uninstall.golden.sh | 33 ++ .../testdata/scripts/zoom_install.golden.sh | 8 + .../testdata/scripts/zoom_uninstall.golden.sh | 104 +++++ server/service/maintained_apps.go | 14 +- 51 files changed, 2737 insertions(+), 23 deletions(-) create mode 100644 server/mdm/maintainedapps/scripts.go create mode 100644 server/mdm/maintainedapps/scripts_test.go create mode 100644 server/mdm/maintainedapps/testdata/scripts/1password_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/1password_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/adobe-acrobat-reader_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/adobe-acrobat-reader_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/box-drive_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/box-drive_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/brave-browser_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/brave-browser_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/docker_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/docker_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/figma_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/figma_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/firefox_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/firefox_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/google-chrome_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/google-chrome_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/microsoft-edge_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/microsoft-edge_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/microsoft-excel_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/microsoft-excel_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/microsoft-teams_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/microsoft-teams_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/microsoft-word_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/microsoft-word_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/notion_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/notion_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/postman_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/postman_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/slack_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/slack_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/teamviewer_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/teamviewer_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/visual-studio-code_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/visual-studio-code_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/whatsapp_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/whatsapp_uninstall.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/zoom_install.golden.sh create mode 100644 server/mdm/maintainedapps/testdata/scripts/zoom_uninstall.golden.sh diff --git a/ee/server/service/maintained_apps.go b/ee/server/service/maintained_apps.go index 95e5e46ee395..1c5b00347d93 100644 --- a/ee/server/service/maintained_apps.go +++ b/ee/server/service/maintained_apps.go @@ -5,9 +5,13 @@ import ( "context" "crypto/sha256" "encoding/hex" + "net/url" "os" + "path/filepath" + "strings" "time" + "github.com/fleetdm/fleet/v4/pkg/file" "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/contexts/viewer" @@ -15,7 +19,13 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" ) -func (svc *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript string, selfService bool) error { +func (svc *Service) AddFleetMaintainedApp( + ctx context.Context, + teamID *uint, + appID uint, + installScript, preInstallQuery, postInstallScript, uninstallScript string, + selfService bool, +) error { if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil { return err } @@ -59,6 +69,21 @@ func (svc *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, app filename = app.Name } + installScript = file.Dos2UnixNewlines(installScript) + if installScript == "" { + installScript = app.InstallScript + } + + uninstallScript = file.Dos2UnixNewlines(uninstallScript) + if uninstallScript == "" { + uninstallScript = app.UninstallScript + } + + installerURL, err := url.Parse(app.InstallerURL) + if err != nil { + return err + } + installerReader := bytes.NewReader(installerBytes) payload := &fleet.UploadSoftwareInstallerPayload{ InstallerFile: installerReader, @@ -68,6 +93,8 @@ func (svc *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, app Version: app.Version, Filename: filename, Platform: string(app.Platform), + Source: "apps", + Extension: strings.TrimPrefix(filepath.Ext(installerURL.Path), "."), BundleIdentifier: app.BundleIdentifier, StorageID: app.SHA256, FleetLibraryAppID: &app.ID, @@ -75,6 +102,7 @@ func (svc *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, app PostInstallScript: postInstallScript, SelfService: selfService, InstallScript: installScript, + UninstallScript: uninstallScript, } // Create record in software installers table diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index a2dea22138bb..f3d443af5206 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -104,6 +104,8 @@ func preProcessUninstallScript(payload *fleet.UploadSoftwareInstallerPayload) { // Replace $PACKAGE_ID in the uninstall script with the package ID(s). var packageID string switch payload.Extension { + case "dmg", "zip": + return case "pkg": var sb strings.Builder _, _ = sb.WriteString("(\n") @@ -1515,7 +1517,7 @@ func packageExtensionToPlatform(ext string) string { switch ext { case ".msi", ".exe": requiredPlatform = "windows" - case ".pkg": + case ".pkg", ".dmg", ".zip": requiredPlatform = "darwin" case ".deb", ".rpm": requiredPlatform = "linux" diff --git a/frontend/interfaces/package_type.ts b/frontend/interfaces/package_type.ts index b8b83b93fe9d..e9a467585f4f 100644 --- a/frontend/interfaces/package_type.ts +++ b/frontend/interfaces/package_type.ts @@ -1,4 +1,5 @@ -const unixPackageTypes = ["pkg", "deb", "rpm"] as const; +const fleetMaintainedPackageTypes = ["dmg", "zip"] as const; +const unixPackageTypes = ["pkg", "deb", "rpm", "dmg", "zip"] as const; const windowsPackageTypes = ["msi", "exe"] as const; export const packageTypes = [ ...unixPackageTypes, @@ -7,7 +8,11 @@ export const packageTypes = [ export type WindowsPackageType = typeof windowsPackageTypes[number]; export type UnixPackageType = typeof unixPackageTypes[number]; -export type PackageType = WindowsPackageType | UnixPackageType; +export type FleetMaintainedPackageType = typeof fleetMaintainedPackageTypes[number]; +export type PackageType = + | WindowsPackageType + | UnixPackageType + | FleetMaintainedPackageType; export const isWindowsPackageType = (s: any): s is WindowsPackageType => { return windowsPackageTypes.includes(s); @@ -17,6 +22,12 @@ export const isUnixPackageType = (s: any): s is UnixPackageType => { return unixPackageTypes.includes(s); }; +export const isFleetMaintainedPackageType = ( + s: any +): s is FleetMaintainedPackageType => { + return fleetMaintainedPackageTypes.includes(s); +}; + export const isPackageType = (s: any): s is PackageType => { return packageTypes.includes(s); }; diff --git a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx index 8966852a2726..e4ed568c5695 100644 --- a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx +++ b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx @@ -6,6 +6,7 @@ import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; import { isPackageType, isWindowsPackageType, + isFleetMaintainedPackageType, PackageType, } from "interfaces/package_type"; @@ -46,6 +47,10 @@ const getPostInstallHelpText = (pkgType: PackageType) => { }; const getUninstallHelpText = (pkgType: PackageType) => { + if (isFleetMaintainedPackageType(pkgType)) { + return "Currently, shell scripts are supported"; + } + return ( <> $PACKAGE_ID will be populated with the {PKG_TYPE_TO_ID_TEXT[pkgType]} from diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts index bc5ab98db40d..1557e169654c 100644 --- a/frontend/services/entities/software.ts +++ b/frontend/services/entities/software.ts @@ -136,6 +136,7 @@ interface IAddFleetMaintainedAppPostBody { pre_install_query?: string; install_script?: string; post_install_script?: string; + uninstall_script?: string; self_service?: boolean; } @@ -376,6 +377,7 @@ export default { pre_install_query: formData.preInstallQuery, install_script: formData.installScript, post_install_script: formData.postInstallScript, + uninstall_script: formData.uninstallScript, self_service: formData.selfService, }; diff --git a/server/datastore/mysql/maintained_apps.go b/server/datastore/mysql/maintained_apps.go index 6be77a458362..06ebd4703d40 100644 --- a/server/datastore/mysql/maintained_apps.go +++ b/server/datastore/mysql/maintained_apps.go @@ -122,9 +122,9 @@ WHERE NOT EXISTS ( WHERE st.bundle_identifier = fla.bundle_identifier AND ( - (si.platform = fla.platform AND si.team_id = ?) + (si.platform = fla.platform AND si.global_or_team_id = ?) OR - (va.platform = fla.platform AND vat.team_id = ?) + (va.platform = fla.platform AND vat.global_or_team_id = ?) ) )` diff --git a/server/fleet/service.go b/server/fleet/service.go index bd3ba0bbce37..b555fec06ecf 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1124,7 +1124,7 @@ type Service interface { // Fleet-maintained apps // AddFleetMaintainedApp adds a Fleet-maintained app to the given team. - AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript string, selfService bool) error + AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript, uninstallScript string, selfService bool) error // ListFleetMaintainedApps lists Fleet-maintained apps available to a specific team ListFleetMaintainedApps(ctx context.Context, teamID uint, opts ListOptions) ([]MaintainedApp, *PaginationMetadata, error) // GetFleetMaintainedApp returns a Fleet-maintained app by ID diff --git a/server/mdm/maintainedapps/ingest.go b/server/mdm/maintainedapps/ingest.go index c699c22b8583..16b3cffa83b6 100644 --- a/server/mdm/maintainedapps/ingest.go +++ b/server/mdm/maintainedapps/ingest.go @@ -137,8 +137,11 @@ func (i ingester) ingestOne(ctx context.Context, app maintainedApp, client *http return ctxerr.Wrapf(ctx, err, "parse URL for cask %s", app.Identifier) } - installScript := installScriptForApp(app, &cask) - uninstallScript := uninstallScriptForApp(app, &cask) + installScript, err := installScriptForApp(app, &cask) + if err != nil { + return ctxerr.Wrapf(ctx, err, "create install script for cask %s", app.Identifier) + } + uninstallScript := uninstallScriptForApp(&cask) _, err = i.ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ Name: cask.Name[0], @@ -155,16 +158,6 @@ func (i ingester) ingestOne(ctx context.Context, app maintainedApp, client *http return ctxerr.Wrap(ctx, err, "upsert maintained app") } -func installScriptForApp(app maintainedApp, cask *brewCask) string { - // TODO: implement install script based on cask and app installer format - return "install" -} - -func uninstallScriptForApp(app maintainedApp, cask *brewCask) string { - // TODO: implement uninstall script based on cask and app installer format - return "uninstall" -} - type brewCask struct { Token string `json:"token"` FullToken string `json:"full_token"` @@ -195,9 +188,28 @@ type brewArtifact struct { Binary []optjson.StringOr[*brewBinaryTarget] `json:"binary"` } +// The choiceChanges file is a property list containing an array of dictionaries. Each dictionary has the following three keys: +// +// Key Description +// choiceIdentifier Identifier for the choice to be modified (string) +// choiceAttribute One of the attribute names described below (string) +// attributeSetting A setting that depends on the choiceAttribute, described below (number or string) +// +// The choiceAttribute and attributeSetting values are as follows: +// +// choiceAttribute attributeSetting Description +// selected (number) 1 to select the choice, 0 to deselect it +// enabled (number) 1 to enable the choice, 0 to disable it +// visible (number) 1 to show the choice, 0 to hide it +// customLocation (string) path at which to install the choice (see below) +type brewPkgConfig struct { + ChoiceIdentifier string `json:"choiceIdentifier" plist:"choiceIdentifier"` + ChoiceAttribute string `json:"choiceAttribute" plist:"choiceAttribute"` + AttributeSetting int `json:"attributeSetting" plist:"attributeSetting"` +} + type brewPkgChoices struct { - // At the moment we don't care about the "choices" field on the pkg. - Choices []any `json:"choices"` + Choices []brewPkgConfig `json:"choices"` } type brewBinaryTarget struct { diff --git a/server/mdm/maintainedapps/scripts.go b/server/mdm/maintainedapps/scripts.go new file mode 100644 index 000000000000..e4a6e54703e5 --- /dev/null +++ b/server/mdm/maintainedapps/scripts.go @@ -0,0 +1,437 @@ +package maintainedapps + +import ( + "fmt" + "slices" + "sort" + "strings" + + "github.com/fleetdm/fleet/v4/pkg/optjson" + "github.com/groob/plist" +) + +func installScriptForApp(app maintainedApp, cask *brewCask) (string, error) { + sb := newScriptBuilder() + + sb.AddVariable("TMPDIR", `$(dirname "$(realpath $INSTALLER_PATH)")`) + sb.AddVariable("APPDIR", `"/Applications/"`) + + formats := strings.Split(app.InstallerFormat, ":") + sb.Extract(formats[0]) + + for _, artifact := range cask.Artifacts { + switch { + case len(artifact.App) > 0: + sb.Write("# copy to the applications folder") + for _, appPath := range artifact.App { + sb.Copy(appPath, "$APPDIR") + } + + case len(artifact.Pkg) > 0: + sb.Write("# install pkg files") + switch len(artifact.Pkg) { + case 1: + if err := sb.InstallPkg(artifact.Pkg[0].String); err != nil { + return "", fmt.Errorf("building statement to install pkg: %w", err) + } + case 2: + if err := sb.InstallPkg(artifact.Pkg[0].String, artifact.Pkg[1].Other.Choices); err != nil { + return "", fmt.Errorf("building statement to install pkg with choices: %w", err) + } + default: + return "", fmt.Errorf("application %s has unknown directive format for pkg", app.Identifier) + } + + case len(artifact.Binary) > 0: + if len(artifact.Binary) == 2 { + source := artifact.Binary[0].String + target := artifact.Binary[1].Other.Target + + if !strings.Contains(target, "$HOMEBREW_PREFIX") && + !strings.Contains(source, "$HOMEBREW_PREFIX") { + sb.Symlink(source, target) + } + } + } + } + + return sb.String(), nil +} + +func uninstallScriptForApp(cask *brewCask) string { + sb := newScriptBuilder() + + for _, artifact := range cask.Artifacts { + switch { + case len(artifact.App) > 0: + sb.AddVariable("APPDIR", `"/Applications/"`) + for _, appPath := range artifact.App { + sb.RemoveFile(fmt.Sprintf(`"$APPDIR/%s"`, appPath)) + } + case len(artifact.Binary) > 0: + if len(artifact.Binary) == 2 { + target := artifact.Binary[1].Other.Target + if !strings.Contains(target, "$HOMEBREW_PREFIX") { + sb.RemoveFile(fmt.Sprintf(`'%s'`, target)) + } + } + case len(artifact.Uninstall) > 0: + sortUninstall(artifact.Uninstall) + for _, u := range artifact.Uninstall { + processUninstallArtifact(u, sb) + } + case len(artifact.Zap) > 0: + sortUninstall(artifact.Zap) + for _, z := range artifact.Zap { + processUninstallArtifact(z, sb) + } + } + } + + return sb.String() +} + +// priority of uninstall directives is defined by homebrew here: +// https://github.com/Homebrew/brew/blob/e1ff668957dd8a66304c0290dfa66083e6c7444e/Library/Homebrew/cask/artifact/abstract_uninstall.rb#L18-L30 +const ( + PriorityEarlyScript = iota + PriorityLaunchctl + PriorityQuit + PrioritySignal + PriorityLoginItem + PriorityKext + PriorityScript + PriorityPkgutil + PriorityDelete + PriorityTrash + PriorityRmdir +) + +// uninstallArtifactOrder returns an integer representing the priority of the +// artifact based on the uninstall directives it contains. Lower number means +// higher priority +func uninstallArtifactOrder(artifact *brewUninstall) int { + switch { + case len(artifact.LaunchCtl.String)+len(artifact.LaunchCtl.Other) > 0: + return PriorityLaunchctl + case len(artifact.Quit.String)+len(artifact.Quit.Other) > 0: + return PriorityQuit + case len(artifact.Signal.String)+len(artifact.Signal.Other) > 0: + return PrioritySignal + case len(artifact.LoginItem.String)+len(artifact.LoginItem.Other) > 0: + return PriorityLoginItem + case len(artifact.Kext.String)+len(artifact.Kext.Other) > 0: + return PriorityKext + case len(artifact.Script.String)+len(artifact.Script.Other) > 0: + return PriorityScript + case len(artifact.PkgUtil.String)+len(artifact.PkgUtil.Other) > 0: + return PriorityPkgutil + case len(artifact.Delete.String)+len(artifact.Delete.Other) > 0: + return PriorityDelete + case len(artifact.Trash.String)+len(artifact.Trash.Other) > 0: + return PriorityTrash + case len(artifact.RmDir.String)+len(artifact.RmDir.Other) > 0: + return PriorityRmdir + default: + return 999 + } +} + +func sortUninstall(artifacts []*brewUninstall) { + slices.SortFunc(artifacts, func(a, b *brewUninstall) int { + return uninstallArtifactOrder(a) - uninstallArtifactOrder(b) + }) +} + +func processUninstallArtifact(u *brewUninstall, sb *scriptBuilder) { + process := func(target optjson.StringOr[[]string], f func(path string)) { + if target.IsOther { + for _, path := range target.Other { + f(path) + } + } else if len(target.String) > 0 { + f(target.String) + } + } + + process(u.LaunchCtl, func(lc string) { + sb.AddFunction("remove_launchctl_service", removeLaunchctlServiceFunc) + sb.Writef("remove_launchctl_service '%s'", lc) + }) + + process(u.Quit, func(appName string) { + sb.AddFunction("quit_application", quitApplicationFunc) + sb.Writef("quit_application '%s'", appName) + }) + + process(u.PkgUtil, func(pkgID string) { + sb.Writef("sudo pkgutil --forget '%s'", pkgID) + }) + + process(u.Delete, func(path string) { + sb.RemoveFile(path) + }) + + process(u.RmDir, func(dir string) { + sb.Writef("sudo rmdir '%s'", dir) + }) + + process(u.Trash, func(path string) { + sb.AddVariable("LOGGED_IN_USER", `$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }')`) + sb.AddFunction("trash", trashFunc) + sb.Writef("trash $LOGGED_IN_USER '%s'", path) + }) +} + +type scriptBuilder struct { + statements []string + variables map[string]string + functions map[string]string +} + +func newScriptBuilder() *scriptBuilder { + return &scriptBuilder{ + statements: []string{}, + variables: map[string]string{}, + functions: map[string]string{}, + } +} + +// AddVariable adds a variable definition to the script +func (s *scriptBuilder) AddVariable(name, definition string) { + s.variables[name] = definition +} + +// AddFunction adds a shell function to the script. +func (s *scriptBuilder) AddFunction(name, definition string) { + s.functions[name] = definition +} + +// Write appends a raw shell command or statement to the script. +func (s *scriptBuilder) Write(in string) { + s.statements = append(s.statements, in) +} + +// Writef formats a string according to the specified format and arguments, +// then appends it to the script. +func (s *scriptBuilder) Writef(format string, args ...any) { + s.statements = append(s.statements, fmt.Sprintf(format, args...)) +} + +// Extract writes shell commands to extract the contents of an installer based +// on the given format. +// +// Supported formats are "dmg" and "zip". It adds the necessary extraction +// commands to the script. +func (s *scriptBuilder) Extract(format string) { + + switch format { + case "dmg": + s.Write("# extract contents") + s.Write(`MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX) +hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" +sudo cp -R "$MOUNT_POINT"/* "$TMPDIR" +hdiutil detach "$MOUNT_POINT"`) + + case "zip": + s.Write("# extract contents") + s.Write(`unzip "$INSTALLER_PATH" -d "$TMPDIR"`) + } +} + +// Copy writes a command to copy a file from the temporary directory to a +// destination. +func (s *scriptBuilder) Copy(file, dest string) { + s.Writef(`sudo cp -R "$TMPDIR/%s" "%s"`, file, dest) +} + +// RemoveFile writes a command to remove a file or directory with sudo +// privileges. +func (s *scriptBuilder) RemoveFile(file string) { + s.Writef(`sudo rm -rf %s`, file) +} + +// InstallPkg writes a command to install a package using the macOS `installer` utility. +// 'pkg' is the package file to install. Optionally, 'choices' can be provided to specify +// installation options. +// +// If no choices are provided, a simple install command is written. +// +// Returns an error if generating the XML for choices fails. +func (s *scriptBuilder) InstallPkg(pkg string, choices ...[]brewPkgConfig) error { + if len(choices) == 0 { + s.Writef(`sudo installer -pkg "$TMPDIR/%s" -target /`, pkg) + return nil + } + + choiceXML, err := plist.MarshalIndent(choices, " ") + if err != nil { + return err + } + + s.Writef(` +CHOICE_XML=$(mktemp /tmp/choice_xml) + +cat << EOF > "$CHOICE_XML" +%s +EOF + +sudo installer -pkg "$temp_dir"/%s -target / -applyChoiceChangesXML "$CHOICE_XML" +`, choiceXML, pkg) + + return nil +} + +// Symlink writes a command to create a symbolic link from 'source' to 'target'. +func (s *scriptBuilder) Symlink(source, target string) { + s.Writef(`/bin/ln -h -f -s -- "%s" "%s"`, source, target) +} + +// String generates the final script as a string. +// +// It includes the shebang, any variables, functions, and statements in the +// correct order. +func (s *scriptBuilder) String() string { + var script strings.Builder + script.WriteString("#!/bin/sh\n\n") + + if len(s.variables) > 0 { + // write variables, order them alphabetically to produce deterministic + // scripts. + script.WriteString("# variables\n") + keys := make([]string, 0, len(s.variables)) + for name := range s.variables { + keys = append(keys, name) + } + sort.Strings(keys) + for _, name := range keys { + script.WriteString(fmt.Sprintf("%s=%s\n", name, s.variables[name])) + } + } + + if len(s.functions) > 0 { + for _, fn := range s.functions { + script.WriteString("\n") + script.WriteString(fn) + script.WriteString("\n") + } + } + + // write any statements + if len(s.statements) > 0 { + script.WriteString("\n") + script.WriteString(strings.Join(s.statements, "\n")) + script.WriteString("\n") + } + + return script.String() +} + +// removeLaunchctlServiceFunc removes a launchctl service, it's a direct port +// of the homebrew implementation +// https://github.com/Homebrew/brew/blob/e1ff668957dd8a66304c0290dfa66083e6c7444e/Library/Homebrew/cask/artifact/abstract_uninstall.rb#L92 +const removeLaunchctlServiceFunc = `remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +}` + +// quitApplicationFunc quits a running application. It's a direct port of the +// homebrew implementation +// https://github.com/Homebrew/brew/blob/e1ff668957dd8a66304c0290dfa66083e6c7444e/Library/Homebrew/cask/artifact/abstract_uninstall.rb#L192 +const quitApplicationFunc = `quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} +` + +const trashFunc = `trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +}` diff --git a/server/mdm/maintainedapps/scripts_test.go b/server/mdm/maintainedapps/scripts_test.go new file mode 100644 index 000000000000..6c785a67f6b1 --- /dev/null +++ b/server/mdm/maintainedapps/scripts_test.go @@ -0,0 +1,72 @@ +package maintainedapps + +import ( + "encoding/json" + "flag" + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + update = flag.Bool("update", false, "update the golden files of this test") +) + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +func TestScriptGeneration(t *testing.T) { + appsJSON, err := os.ReadFile("apps.json") + require.NoError(t, err) + + var apps []maintainedApp + err = json.Unmarshal(appsJSON, &apps) + require.NoError(t, err) + + for _, app := range apps { + caskJSON, err := os.ReadFile(filepath.Join("testdata", app.Identifier+".json")) + require.NoError(t, err) + + var cask brewCask + err = json.Unmarshal(caskJSON, &cask) + require.NoError(t, err) + + t.Run(app.Identifier, func(t *testing.T) { + installScript, err := installScriptForApp(app, &cask) + require.NoError(t, err) + assertGoldenMatches(t, app.Identifier+"_install", installScript, *update) + assertGoldenMatches(t, app.Identifier+"_uninstall", uninstallScriptForApp(&cask), *update) + }) + } + +} + +func assertGoldenMatches(t *testing.T, goldenFile string, actual string, update bool) { + t.Helper() + goldenPath := filepath.Join("testdata", "scripts", goldenFile+".golden.sh") + + var f *os.File + var err error + if update { + f, err = os.OpenFile(goldenPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + } else { + f, err = os.OpenFile(goldenPath, os.O_RDONLY, 0644) + } + require.NoError(t, err) + defer f.Close() + + if update { + _, err := f.WriteString(actual) + require.NoError(t, err) + return + } + + content, err := io.ReadAll(f) + require.NoError(t, err) + require.Equal(t, string(content), actual) +} diff --git a/server/mdm/maintainedapps/testdata/scripts/1password_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/1password_install.golden.sh new file mode 100644 index 000000000000..892fe48d9e3d --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/1password_install.golden.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +unzip "$INSTALLER_PATH" -d "$TMPDIR" +# copy to the applications folder +sudo cp -R "$TMPDIR/1Password.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/1password_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/1password_uninstall.golden.sh new file mode 100644 index 000000000000..ad9a4e5d0d53 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/1password_uninstall.golden.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +sudo rm -rf "$APPDIR/1Password.app" +trash $LOGGED_IN_USER '~/Library/Application Scripts/2BUA8C4S2C.com.1password*' +trash $LOGGED_IN_USER '~/Library/Application Scripts/2BUA8C4S2C.com.agilebits' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.1password.1password-launcher' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.1password.browser-support' +trash $LOGGED_IN_USER '~/Library/Application Support/1Password' +trash $LOGGED_IN_USER '~/Library/Application Support/Arc/User Data/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.1password.1password.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/1Password*' +trash $LOGGED_IN_USER '~/Library/Application Support/Google/Chrome Beta/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/Google/Chrome Canary/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/Google/Chrome Dev/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/Microsoft Edge Beta/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/Microsoft Edge Canary/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/Microsoft Edge Dev/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/Microsoft Edge/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/Mozilla/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Application Support/Vivaldi/NativeMessagingHosts/com.1password.1password.json' +trash $LOGGED_IN_USER '~/Library/Containers/2BUA8C4S2C.com.1password.browser-helper' +trash $LOGGED_IN_USER '~/Library/Containers/com.1password.1password*' +trash $LOGGED_IN_USER '~/Library/Containers/com.1password.browser-support' +trash $LOGGED_IN_USER '~/Library/Group Containers/2BUA8C4S2C.com.1password' +trash $LOGGED_IN_USER '~/Library/Group Containers/2BUA8C4S2C.com.agilebits' +trash $LOGGED_IN_USER '~/Library/Logs/1Password' +trash $LOGGED_IN_USER '~/Library/Preferences/com.1password.1password.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/group.com.1password.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.1password.1password.savedState' diff --git a/server/mdm/maintainedapps/testdata/scripts/adobe-acrobat-reader_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/adobe-acrobat-reader_install.golden.sh new file mode 100644 index 000000000000..1c975a9950aa --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/adobe-acrobat-reader_install.golden.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX) +hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" +sudo cp -R "$MOUNT_POINT"/* "$TMPDIR" +hdiutil detach "$MOUNT_POINT" +# install pkg files +sudo installer -pkg "$TMPDIR/AcroRdrDC_2400221005_MUI.pkg" -target / diff --git a/server/mdm/maintainedapps/testdata/scripts/adobe-acrobat-reader_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/adobe-acrobat-reader_uninstall.golden.sh new file mode 100644 index 000000000000..9b7e702858b7 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/adobe-acrobat-reader_uninstall.golden.sh @@ -0,0 +1,124 @@ +#!/bin/sh + +# variables +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service 'com.adobe.ARMDC.Communicator' +remove_launchctl_service 'com.adobe.ARMDC.SMJobBlessHelper' +remove_launchctl_service 'com.adobe.ARMDCHelper.cc24aef4a1b90ed56a725c38014c95072f92651fb65e1bf9c8e43c37a23d420d' +quit_application 'com.adobe.AdobeRdrCEF' +quit_application 'com.adobe.AdobeRdrCEFHelper' +quit_application 'com.adobe.Reader' +sudo pkgutil --forget 'com.adobe.acrobat.DC.reader.*' +sudo pkgutil --forget 'com.adobe.armdc.app.pkg' +sudo pkgutil --forget 'com.adobe.RdrServicesUpdater' +sudo rm -rf /Applications/Adobe Acrobat Reader.app +sudo rm -rf /Library/Preferences/com.adobe.reader.DC.WebResource.plist +trash $LOGGED_IN_USER '~/Library/Caches/com.adobe.Reader' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.adobe.Reader.binarycookies' +trash $LOGGED_IN_USER '~/Library/Preferences/com.adobe.AdobeRdrCEFHelper.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.adobe.crashreporter.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.adobe.Reader.plist' diff --git a/server/mdm/maintainedapps/testdata/scripts/box-drive_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/box-drive_install.golden.sh new file mode 100644 index 000000000000..1cd05d3dd6e3 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/box-drive_install.golden.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# install pkg files +sudo installer -pkg "$TMPDIR/BoxDrive-2.40.345.pkg" -target / diff --git a/server/mdm/maintainedapps/testdata/scripts/box-drive_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/box-drive_uninstall.golden.sh new file mode 100644 index 000000000000..1fc226a0aaf7 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/box-drive_uninstall.golden.sh @@ -0,0 +1,122 @@ +#!/bin/sh + +# variables +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service 'com.box.desktop.helper' +quit_application 'com.box.Box-Local-Com-Server' +quit_application 'com.box.desktop' +quit_application 'com.box.desktop.findersyncext' +quit_application 'com.box.desktop.helper' +quit_application 'com.box.desktop.ui' +sudo pkgutil --forget 'com.box.desktop.installer.*' +trash $LOGGED_IN_USER '~/.Box_*' +trash $LOGGED_IN_USER '~/Library/Application Support/Box/Box' +trash $LOGGED_IN_USER '~/Library/Application Support/FileProvider/com.box.desktop.boxfileprovider' +trash $LOGGED_IN_USER '~/Library/Containers/com.box.desktop.findersyncext' +trash $LOGGED_IN_USER '~/Library/Logs/Box/Box' +trash $LOGGED_IN_USER '~/Library/Preferences/com.box.desktop.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.box.desktop.ui.plist' diff --git a/server/mdm/maintainedapps/testdata/scripts/brave-browser_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/brave-browser_install.golden.sh new file mode 100644 index 000000000000..4dae8ce08582 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/brave-browser_install.golden.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX) +hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" +sudo cp -R "$MOUNT_POINT"/* "$TMPDIR" +hdiutil detach "$MOUNT_POINT" +# copy to the applications folder +sudo cp -R "$TMPDIR/Brave Browser.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/brave-browser_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/brave-browser_uninstall.golden.sh new file mode 100644 index 000000000000..304ff813f6a8 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/brave-browser_uninstall.golden.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +sudo rm -rf "$APPDIR/Brave Browser.app" +trash $LOGGED_IN_USER '~/Library/Application Support/BraveSoftware' +trash $LOGGED_IN_USER '~/Library/Caches/BraveSoftware' +trash $LOGGED_IN_USER '~/Library/Caches/com.brave.Browser' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.brave.Browser' +trash $LOGGED_IN_USER '~/Library/Preferences/com.brave.Browser.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.brave.Browser.savedState' diff --git a/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_install.golden.sh new file mode 100644 index 000000000000..88210167e9a0 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_install.golden.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# install pkg files +sudo installer -pkg "$TMPDIR/Cloudflare_WARP_2024.6.474.0.pkg" -target / diff --git a/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh new file mode 100644 index 000000000000..fda2d5147e80 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh @@ -0,0 +1,120 @@ +#!/bin/sh + +# variables +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service 'com.cloudflare.1dot1dot1dot1.macos.loginlauncherapp' +remove_launchctl_service 'com.cloudflare.1dot1dot1dot1.macos.warp.daemon' +quit_application 'com.cloudflare.1dot1dot1dot1.macos' +sudo pkgutil --forget 'com.cloudflare.1dot1dot1dot1.macos' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.cloudflare.1dot1dot1dot1.macos.loginlauncherapp' +trash $LOGGED_IN_USER '~/Library/Application Support/com.cloudflare.1dot1dot1dot1.macos' +trash $LOGGED_IN_USER '~/Library/Caches/com.cloudflare.1dot1dot1dot1.macos' +trash $LOGGED_IN_USER '~/Library/Caches/com.plausiblelabs.crashreporter.data/com.cloudflare.1dot1dot1dot1.macos' +trash $LOGGED_IN_USER '~/Library/Containers/com.cloudflare.1dot1dot1dot1.macos.loginlauncherapp' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.cloudflare.1dot1dot1dot1.macos' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.cloudflare.1dot1dot1dot1.macos.binarycookies' +trash $LOGGED_IN_USER '~/Library/Preferences/com.cloudflare.1dot1dot1dot1.macos.plist' diff --git a/server/mdm/maintainedapps/testdata/scripts/docker_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/docker_install.golden.sh new file mode 100644 index 000000000000..4d6dd3f76e06 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/docker_install.golden.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX) +hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" +sudo cp -R "$MOUNT_POINT"/* "$TMPDIR" +hdiutil detach "$MOUNT_POINT" +# copy to the applications folder +sudo cp -R "$TMPDIR/Docker.app" "$APPDIR" +/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker" "/usr/local/bin/docker" +/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-desktop" "/usr/local/bin/docker-credential-desktop" +/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-ecr-login" "/usr/local/bin/docker-credential-ecr-login" +/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-osxkeychain" "/usr/local/bin/docker-credential-osxkeychain" +/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-index" "/usr/local/bin/docker-index" +/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/kubectl" "/usr/local/bin/kubectl.docker" +/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/cli-plugins/docker-compose" "/usr/local/cli-plugins/docker-compose" +/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/hub-tool" "/usr/local/bin/hub-tool" diff --git a/server/mdm/maintainedapps/testdata/scripts/docker_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/docker_uninstall.golden.sh new file mode 100644 index 000000000000..1a7ec4731276 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/docker_uninstall.golden.sh @@ -0,0 +1,150 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service 'com.docker.helper' +remove_launchctl_service 'com.docker.socket' +remove_launchctl_service 'com.docker.vmnetd' +quit_application 'com.docker.docker' +sudo rm -rf /Library/PrivilegedHelperTools/com.docker.socket +sudo rm -rf /Library/PrivilegedHelperTools/com.docker.vmnetd +sudo rmdir '~/.docker/bin' +sudo rm -rf "$APPDIR/Docker.app" +sudo rm -rf '/usr/local/bin/docker' +sudo rm -rf '/usr/local/bin/docker-credential-desktop' +sudo rm -rf '/usr/local/bin/docker-credential-ecr-login' +sudo rm -rf '/usr/local/bin/docker-credential-osxkeychain' +sudo rm -rf '/usr/local/bin/docker-index' +sudo rm -rf '/usr/local/bin/kubectl.docker' +sudo rm -rf '/usr/local/cli-plugins/docker-compose' +sudo rm -rf '/usr/local/bin/hub-tool' +sudo rmdir '~/Library/Caches/com.plausiblelabs.crashreporter.data' +sudo rmdir '~/Library/Caches/KSCrashReports' +trash $LOGGED_IN_USER '/usr/local/bin/docker-compose.backup' +trash $LOGGED_IN_USER '/usr/local/bin/docker.backup' +trash $LOGGED_IN_USER '~/.docker' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.docker.helper' +trash $LOGGED_IN_USER '~/Library/Application Scripts/group.com.docker' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.docker.helper.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.electron.dockerdesktop.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/com.bugsnag.Bugsnag/com.docker.docker' +trash $LOGGED_IN_USER '~/Library/Application Support/Docker Desktop' +trash $LOGGED_IN_USER '~/Library/Caches/com.docker.docker' +trash $LOGGED_IN_USER '~/Library/Caches/com.plausiblelabs.crashreporter.data/com.docker.docker' +trash $LOGGED_IN_USER '~/Library/Caches/KSCrashReports/Docker' +trash $LOGGED_IN_USER '~/Library/Containers/com.docker.docker' +trash $LOGGED_IN_USER '~/Library/Containers/com.docker.helper' +trash $LOGGED_IN_USER '~/Library/Group Containers/group.com.docker' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.docker.docker' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.docker.docker.binarycookies' +trash $LOGGED_IN_USER '~/Library/Logs/Docker Desktop' +trash $LOGGED_IN_USER '~/Library/Preferences/com.docker.docker.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.electron.docker-frontend.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.electron.dockerdesktop.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.electron.docker-frontend.savedState' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.electron.dockerdesktop.savedState' diff --git a/server/mdm/maintainedapps/testdata/scripts/figma_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/figma_install.golden.sh new file mode 100644 index 000000000000..0afbf1dbc286 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/figma_install.golden.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +unzip "$INSTALLER_PATH" -d "$TMPDIR" +# copy to the applications folder +sudo cp -R "$TMPDIR/Figma.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/figma_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/figma_uninstall.golden.sh new file mode 100644 index 000000000000..7a3538d52156 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/figma_uninstall.golden.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +sudo rm -rf "$APPDIR/Figma.app" +trash $LOGGED_IN_USER '~/Library/Application Support/Figma' +trash $LOGGED_IN_USER '~/Library/Application Support/figma-desktop' +trash $LOGGED_IN_USER '~/Library/Caches/com.figma.agent' +trash $LOGGED_IN_USER '~/Library/Caches/com.figma.Desktop' +trash $LOGGED_IN_USER '~/Library/Preferences/com.figma.Desktop.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.figma.Desktop.savedState' diff --git a/server/mdm/maintainedapps/testdata/scripts/firefox_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/firefox_install.golden.sh new file mode 100644 index 000000000000..5de47f50aa98 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/firefox_install.golden.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX) +hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" +sudo cp -R "$MOUNT_POINT"/* "$TMPDIR" +hdiutil detach "$MOUNT_POINT" +# copy to the applications folder +sudo cp -R "$TMPDIR/Firefox.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/firefox_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/firefox_uninstall.golden.sh new file mode 100644 index 000000000000..f649d7b650c1 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/firefox_uninstall.golden.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +quit_application 'org.mozilla.firefox' +sudo rm -rf "$APPDIR/Firefox.app" +sudo rm -rf 'firefox' +sudo rmdir '~/Library/Application Support/Mozilla' +sudo rmdir '~/Library/Caches/Mozilla' +sudo rmdir '~/Library/Caches/Mozilla/updates' +sudo rmdir '~/Library/Caches/Mozilla/updates/Applications' +trash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/firefox_*' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/org.mozilla.firefox.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/firefox_*' +trash $LOGGED_IN_USER '~/Library/Application Support/Firefox' +trash $LOGGED_IN_USER '~/Library/Caches/Firefox' +trash $LOGGED_IN_USER '~/Library/Caches/Mozilla/updates/Applications/Firefox' +trash $LOGGED_IN_USER '~/Library/Caches/org.mozilla.crashreporter' +trash $LOGGED_IN_USER '~/Library/Caches/org.mozilla.firefox' +trash $LOGGED_IN_USER '~/Library/Preferences/org.mozilla.crashreporter.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/org.mozilla.firefox.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/org.mozilla.firefox.savedState' +trash $LOGGED_IN_USER '~/Library/WebKit/org.mozilla.firefox' diff --git a/server/mdm/maintainedapps/testdata/scripts/google-chrome_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/google-chrome_install.golden.sh new file mode 100644 index 000000000000..8ce14c6b9136 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/google-chrome_install.golden.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX) +hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" +sudo cp -R "$MOUNT_POINT"/* "$TMPDIR" +hdiutil detach "$MOUNT_POINT" +# copy to the applications folder +sudo cp -R "$TMPDIR/Google Chrome.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/google-chrome_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/google-chrome_uninstall.golden.sh new file mode 100644 index 000000000000..52f4afdf292c --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/google-chrome_uninstall.golden.sh @@ -0,0 +1,100 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +sudo rm -rf "$APPDIR/Google Chrome.app" +remove_launchctl_service 'com.google.keystone.agent' +remove_launchctl_service 'com.google.keystone.daemon' +sudo rmdir '/Library/Google' +sudo rmdir '~/Library/Application Support/Google' +sudo rmdir '~/Library/Caches/Google' +sudo rmdir '~/Library/Google' +trash $LOGGED_IN_USER '/Library/Caches/com.google.SoftwareUpdate.*' +trash $LOGGED_IN_USER '/Library/Google/Google Chrome Brand.plist' +trash $LOGGED_IN_USER '/Library/Google/GoogleSoftwareUpdate' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.google.chrome.app.*.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.google.chrome.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/Google/Chrome' +trash $LOGGED_IN_USER '~/Library/Caches/com.google.Chrome' +trash $LOGGED_IN_USER '~/Library/Caches/com.google.Chrome.helper.*' +trash $LOGGED_IN_USER '~/Library/Caches/com.google.Keystone' +trash $LOGGED_IN_USER '~/Library/Caches/com.google.Keystone.Agent' +trash $LOGGED_IN_USER '~/Library/Caches/com.google.SoftwareUpdate' +trash $LOGGED_IN_USER '~/Library/Caches/Google/Chrome' +trash $LOGGED_IN_USER '~/Library/Google/Google Chrome Brand.plist' +trash $LOGGED_IN_USER '~/Library/Google/GoogleSoftwareUpdate' +trash $LOGGED_IN_USER '~/Library/LaunchAgents/com.google.keystone.agent.plist' +trash $LOGGED_IN_USER '~/Library/LaunchAgents/com.google.keystone.xpcservice.plist' +trash $LOGGED_IN_USER '~/Library/Logs/GoogleSoftwareUpdateAgent.log' +trash $LOGGED_IN_USER '~/Library/Preferences/com.google.Chrome.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.google.Keystone.Agent.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.google.Chrome.app.*.savedState' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.google.Chrome.savedState' +trash $LOGGED_IN_USER '~/Library/WebKit/com.google.Chrome' diff --git a/server/mdm/maintainedapps/testdata/scripts/microsoft-edge_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/microsoft-edge_install.golden.sh new file mode 100644 index 000000000000..fa6887a8aa4b --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/microsoft-edge_install.golden.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# install pkg files + +CHOICE_XML=$(mktemp /tmp/choice_xml) + +cat << EOF > "$CHOICE_XML" + + + + + + + attributeSetting + 0 + choiceAttribute + selected + choiceIdentifier + com.microsoft.package.Microsoft_AutoUpdate.app + + + + + +EOF + +sudo installer -pkg "$temp_dir"/MicrosoftEdge-128.0.2739.67.pkg -target / -applyChoiceChangesXML "$CHOICE_XML" + diff --git a/server/mdm/maintainedapps/testdata/scripts/microsoft-edge_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/microsoft-edge_uninstall.golden.sh new file mode 100644 index 000000000000..aaa780bc8784 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/microsoft-edge_uninstall.golden.sh @@ -0,0 +1,89 @@ +#!/bin/sh + +# variables +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +remove_launchctl_service 'com.microsoft.EdgeUpdater.update-internal.109.0.1518.89.system' +remove_launchctl_service 'com.microsoft.EdgeUpdater.update.system' +remove_launchctl_service 'com.microsoft.EdgeUpdater.wake.system' +sudo pkgutil --forget 'com.microsoft.edgemac' +sudo rm -rf /Library/Application Support/Microsoft/EdgeUpdater +sudo rmdir '/Library/Application Support/Microsoft' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.microsoft.edgemac.wdgExtension' +trash $LOGGED_IN_USER '~/Library/Application Support/Microsoft Edge' +trash $LOGGED_IN_USER '~/Library/Application Support/Microsoft/EdgeUpdater' +trash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.edgemac' +trash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.EdgeUpdater' +trash $LOGGED_IN_USER '~/Library/Caches/Microsoft Edge' +trash $LOGGED_IN_USER '~/Library/Containers/com.microsoft.edgemac.wdgExtension' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.edge*' +trash $LOGGED_IN_USER '~/Library/LaunchAgents/com.microsoft.EdgeUpdater.*.plist' +trash $LOGGED_IN_USER '~/Library/Microsoft/EdgeUpdater' +trash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.edgemac.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.edgemac.*' +trash $LOGGED_IN_USER '~/Library/WebKit/com.microsoft.edgemac' diff --git a/server/mdm/maintainedapps/testdata/scripts/microsoft-excel_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/microsoft-excel_install.golden.sh new file mode 100644 index 000000000000..cc88f6bc2e70 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/microsoft-excel_install.golden.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# install pkg files + +CHOICE_XML=$(mktemp /tmp/choice_xml) + +cat << EOF > "$CHOICE_XML" + + + + + + + attributeSetting + 0 + choiceAttribute + selected + choiceIdentifier + com.microsoft.autoupdate + + + + + +EOF + +sudo installer -pkg "$temp_dir"/Microsoft_Excel_16.88.24081116_Installer.pkg -target / -applyChoiceChangesXML "$CHOICE_XML" + diff --git a/server/mdm/maintainedapps/testdata/scripts/microsoft-excel_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/microsoft-excel_uninstall.golden.sh new file mode 100644 index 000000000000..8fa7759f54cb --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/microsoft-excel_uninstall.golden.sh @@ -0,0 +1,119 @@ +#!/bin/sh + +# variables +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service 'com.microsoft.office.licensingV2.helper' +quit_application 'com.microsoft.autoupdate2' +sudo pkgutil --forget 'com.microsoft.package.Microsoft_Excel.app' +sudo pkgutil --forget 'com.microsoft.pkg.licensing' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.microsoft.Excel' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.microsoft.excel.sfl*' +trash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.Excel' +trash $LOGGED_IN_USER '~/Library/Containers/com.microsoft.Excel' +trash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.Excel.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.Excel.savedState' +trash $LOGGED_IN_USER '~/Library/Webkit/com.microsoft.Excel' diff --git a/server/mdm/maintainedapps/testdata/scripts/microsoft-teams_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/microsoft-teams_install.golden.sh new file mode 100644 index 000000000000..58222137e17f --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/microsoft-teams_install.golden.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# install pkg files + +CHOICE_XML=$(mktemp /tmp/choice_xml) + +cat << EOF > "$CHOICE_XML" + + + + + + + attributeSetting + 0 + choiceAttribute + selected + choiceIdentifier + com.microsoft.autoupdate + + + + + +EOF + +sudo installer -pkg "$temp_dir"/MicrosoftTeams.pkg -target / -applyChoiceChangesXML "$CHOICE_XML" + diff --git a/server/mdm/maintainedapps/testdata/scripts/microsoft-teams_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/microsoft-teams_uninstall.golden.sh new file mode 100644 index 000000000000..4500ed1bdc3b --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/microsoft-teams_uninstall.golden.sh @@ -0,0 +1,139 @@ +#!/bin/sh + +# variables +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service 'com.microsoft.teams.TeamsUpdaterDaemon' +quit_application 'com.microsoft.autoupdate2' +sudo pkgutil --forget 'com.microsoft.MSTeamsAudioDevice' +sudo pkgutil --forget 'com.microsoft.package.Microsoft_AutoUpdate.app' +sudo pkgutil --forget 'com.microsoft.teams2' +sudo rm -rf /Applications/Microsoft Teams.app +sudo rm -rf /Library/Application Support/Microsoft/TeamsUpdaterDaemon +sudo rm -rf /Library/Logs/Microsoft/MSTeams +sudo rm -rf /Library/Logs/Microsoft/Teams +sudo rm -rf /Library/Preferences/com.microsoft.teams.plist +sudo rmdir '~/Library/Application Support/Microsoft' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.microsoft.teams2' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.microsoft.teams2.launcher' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.microsoft.teams2.notificationcenter' +trash $LOGGED_IN_USER '~/Library/Application Support/com.microsoft.teams' +trash $LOGGED_IN_USER '~/Library/Application Support/Microsoft/Teams' +trash $LOGGED_IN_USER '~/Library/Application Support/Teams' +trash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.teams' +trash $LOGGED_IN_USER '~/Library/Containers/com.microsoft.teams2' +trash $LOGGED_IN_USER '~/Library/Containers/com.microsoft.teams2.launcher' +trash $LOGGED_IN_USER '~/Library/Containers/com.microsoft.teams2.notificationcenter' +trash $LOGGED_IN_USER '~/Library/Cookies/com.microsoft.teams.binarycookies' +trash $LOGGED_IN_USER '~/Library/Group Containers/*.com.microsoft.teams' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.teams' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.teams.binarycookies' +trash $LOGGED_IN_USER '~/Library/Logs/Microsoft Teams Helper (Renderer)' +trash $LOGGED_IN_USER '~/Library/Logs/Microsoft Teams' +trash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.teams.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.teams.savedState' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.teams2.savedState' +trash $LOGGED_IN_USER '~/Library/WebKit/com.microsoft.teams' diff --git a/server/mdm/maintainedapps/testdata/scripts/microsoft-word_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/microsoft-word_install.golden.sh new file mode 100644 index 000000000000..5686532766d7 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/microsoft-word_install.golden.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# install pkg files + +CHOICE_XML=$(mktemp /tmp/choice_xml) + +cat << EOF > "$CHOICE_XML" + + + + + + + attributeSetting + 0 + choiceAttribute + selected + choiceIdentifier + com.microsoft.autoupdate + + + + + +EOF + +sudo installer -pkg "$temp_dir"/Microsoft_Word_16.88.24081116_Installer.pkg -target / -applyChoiceChangesXML "$CHOICE_XML" + diff --git a/server/mdm/maintainedapps/testdata/scripts/microsoft-word_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/microsoft-word_uninstall.golden.sh new file mode 100644 index 000000000000..bde954b5ea0d --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/microsoft-word_uninstall.golden.sh @@ -0,0 +1,118 @@ +#!/bin/sh + +# variables +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service 'com.microsoft.office.licensingV2.helper' +quit_application 'com.microsoft.autoupdate2' +sudo pkgutil --forget 'com.microsoft.package.Microsoft_Word.app' +sudo pkgutil --forget 'com.microsoft.pkg.licensing' +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.microsoft.Word' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.microsoft.word.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/Microsoft Word_*.plist' +trash $LOGGED_IN_USER '~/Library/Containers/com.microsoft.Word' +trash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.Word.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.Word.savedState' diff --git a/server/mdm/maintainedapps/testdata/scripts/notion_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/notion_install.golden.sh new file mode 100644 index 000000000000..99a374eb95e2 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/notion_install.golden.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX) +hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" +sudo cp -R "$MOUNT_POINT"/* "$TMPDIR" +hdiutil detach "$MOUNT_POINT" +# copy to the applications folder +sudo cp -R "$TMPDIR/Notion.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/notion_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/notion_uninstall.golden.sh new file mode 100644 index 000000000000..f92e6cd17235 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/notion_uninstall.golden.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +sudo rm -rf "$APPDIR/Notion.app" +trash $LOGGED_IN_USER '~/Library/Application Support/Caches/notion-updater' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/notion.id.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/Notion' +trash $LOGGED_IN_USER '~/Library/Caches/notion.id*' +trash $LOGGED_IN_USER '~/Library/Logs/Notion' +trash $LOGGED_IN_USER '~/Library/Preferences/ByHost/notion.id.*' +trash $LOGGED_IN_USER '~/Library/Preferences/notion.id.*' +trash $LOGGED_IN_USER '~/Library/Saved Application State/notion.id.savedState' +trash $LOGGED_IN_USER '~/Library/WebKit/notion.id' diff --git a/server/mdm/maintainedapps/testdata/scripts/postman_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/postman_install.golden.sh new file mode 100644 index 000000000000..1b411372b58a --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/postman_install.golden.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +unzip "$INSTALLER_PATH" -d "$TMPDIR" +# copy to the applications folder +sudo cp -R "$TMPDIR/Postman.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/postman_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/postman_uninstall.golden.sh new file mode 100644 index 000000000000..b6a2589c960a --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/postman_uninstall.golden.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +sudo rm -rf "$APPDIR/Postman.app" +trash $LOGGED_IN_USER '~/Library/Application Support/com.postmanlabs.mac.ShipIt' +trash $LOGGED_IN_USER '~/Library/Application Support/Postman' +trash $LOGGED_IN_USER '~/Library/Caches/com.postmanlabs.mac' +trash $LOGGED_IN_USER '~/Library/Caches/com.postmanlabs.mac.ShipIt' +trash $LOGGED_IN_USER '~/Library/Caches/Postman' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.postmanlabs.mac' +trash $LOGGED_IN_USER '~/Library/Preferences/ByHost/com.postmanlabs.mac.ShipIt.*.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.postmanlabs.mac.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.postmanlabs.mac.savedState' diff --git a/server/mdm/maintainedapps/testdata/scripts/slack_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/slack_install.golden.sh new file mode 100644 index 000000000000..6ba8f9ecd2e0 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/slack_install.golden.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX) +hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" +sudo cp -R "$MOUNT_POINT"/* "$TMPDIR" +hdiutil detach "$MOUNT_POINT" +# copy to the applications folder +sudo cp -R "$TMPDIR/Slack.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/slack_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/slack_uninstall.golden.sh new file mode 100644 index 000000000000..dc94111042d7 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/slack_uninstall.golden.sh @@ -0,0 +1,80 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +quit_application 'com.tinyspeck.slackmacgap' +sudo rm -rf "$APPDIR/Slack.app" +trash $LOGGED_IN_USER '~/Library/Application Scripts/com.tinyspeck.slackmacgap' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.tinyspeck.slackmacgap.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/Slack' +trash $LOGGED_IN_USER '~/Library/Caches/com.tinyspeck.slackmacgap*' +trash $LOGGED_IN_USER '~/Library/Containers/com.tinyspeck.slackmacgap*' +trash $LOGGED_IN_USER '~/Library/Cookies/com.tinyspeck.slackmacgap.binarycookies' +trash $LOGGED_IN_USER '~/Library/Group Containers/*.com.tinyspeck.slackmacgap' +trash $LOGGED_IN_USER '~/Library/Group Containers/*.slack' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.tinyspeck.slackmacgap*' +trash $LOGGED_IN_USER '~/Library/Logs/Slack' +trash $LOGGED_IN_USER '~/Library/Preferences/ByHost/com.tinyspeck.slackmacgap.ShipIt.*.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.tinyspeck.slackmacgap*' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.tinyspeck.slackmacgap.savedState' +trash $LOGGED_IN_USER '~/Library/WebKit/com.tinyspeck.slackmacgap' diff --git a/server/mdm/maintainedapps/testdata/scripts/teamviewer_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/teamviewer_install.golden.sh new file mode 100644 index 000000000000..cc6a0b4e32eb --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/teamviewer_install.golden.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# install pkg files +sudo installer -pkg "$TMPDIR/TeamViewer.pkg" -target / diff --git a/server/mdm/maintainedapps/testdata/scripts/teamviewer_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/teamviewer_uninstall.golden.sh new file mode 100644 index 000000000000..b229a14d5483 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/teamviewer_uninstall.golden.sh @@ -0,0 +1,131 @@ +#!/bin/sh + +# variables +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +remove_launchctl_service 'com.teamviewer.desktop' +remove_launchctl_service 'com.teamviewer.Helper' +remove_launchctl_service 'com.teamviewer.service' +remove_launchctl_service 'com.teamviewer.teamviewer' +remove_launchctl_service 'com.teamviewer.teamviewer_desktop' +remove_launchctl_service 'com.teamviewer.teamviewer_service' +remove_launchctl_service 'com.teamviewer.UninstallerHelper' +remove_launchctl_service 'com.teamviewer.UninstallerWatcher' +quit_application 'com.teamviewer.TeamViewer' +quit_application 'com.teamviewer.TeamViewerUninstaller' +sudo pkgutil --forget 'com.teamviewer.AuthorizationPlugin' +sudo pkgutil --forget 'com.teamviewer.remoteaudiodriver' +sudo pkgutil --forget 'com.teamviewer.teamviewer.*' +sudo pkgutil --forget 'TeamViewerUninstaller' +sudo rm -rf /Applications/TeamViewer.app +sudo rm -rf /Library/Preferences/com.teamviewer* +trash $LOGGED_IN_USER '~/Library/Application Support/TeamViewer' +trash $LOGGED_IN_USER '~/Library/Caches/com.teamviewer.TeamViewer' +trash $LOGGED_IN_USER '~/Library/Caches/TeamViewer' +trash $LOGGED_IN_USER '~/Library/Cookies/com.teamviewer.TeamViewer.binarycookies' +trash $LOGGED_IN_USER '~/Library/Logs/TeamViewer' +trash $LOGGED_IN_USER '~/Library/Preferences/com.teamviewer*' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.teamviewer.TeamViewer.savedState' diff --git a/server/mdm/maintainedapps/testdata/scripts/visual-studio-code_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/visual-studio-code_install.golden.sh new file mode 100644 index 000000000000..ba1b1e7158a3 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/visual-studio-code_install.golden.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +unzip "$INSTALLER_PATH" -d "$TMPDIR" +# copy to the applications folder +sudo cp -R "$TMPDIR/Visual Studio Code.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/visual-studio-code_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/visual-studio-code_uninstall.golden.sh new file mode 100644 index 000000000000..edc9192da180 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/visual-studio-code_uninstall.golden.sh @@ -0,0 +1,122 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +quit_application() { + local bundle_id="$1" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + return + fi + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} + + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service 'com.microsoft.VSCode.ShipIt' +quit_application 'com.microsoft.VSCode' +sudo rm -rf "$APPDIR/Visual Studio Code.app" +trash $LOGGED_IN_USER '~/.vscode' +trash $LOGGED_IN_USER '~/Library/Application Support/Code' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.microsoft.vscode.sfl*' +trash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.VSCode' +trash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.VSCode.ShipIt' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.VSCode' +trash $LOGGED_IN_USER '~/Library/Preferences/ByHost/com.microsoft.VSCode.ShipIt.*.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.VSCode.helper.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.VSCode.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.VSCode.savedState' diff --git a/server/mdm/maintainedapps/testdata/scripts/whatsapp_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/whatsapp_install.golden.sh new file mode 100644 index 000000000000..6a7bfccdf474 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/whatsapp_install.golden.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# extract contents +unzip "$INSTALLER_PATH" -d "$TMPDIR" +# copy to the applications folder +sudo cp -R "$TMPDIR/WhatsApp.app" "$APPDIR" diff --git a/server/mdm/maintainedapps/testdata/scripts/whatsapp_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/whatsapp_uninstall.golden.sh new file mode 100644 index 000000000000..56286533a843 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/whatsapp_uninstall.golden.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +sudo rm -rf "$APPDIR/WhatsApp.app" +trash $LOGGED_IN_USER '~/Library/Application Scripts/net.whatsapp.WhatsApp*' +trash $LOGGED_IN_USER '~/Library/Caches/net.whatsapp.WhatsApp' +trash $LOGGED_IN_USER '~/Library/Containers/net.whatsapp.WhatsApp*' +trash $LOGGED_IN_USER '~/Library/Group Containers/group.com.facebook.family' +trash $LOGGED_IN_USER '~/Library/Group Containers/group.net.whatsapp*' +trash $LOGGED_IN_USER '~/Library/Saved Application State/net.whatsapp.WhatsApp.savedState' diff --git a/server/mdm/maintainedapps/testdata/scripts/zoom_install.golden.sh b/server/mdm/maintainedapps/testdata/scripts/zoom_install.golden.sh new file mode 100644 index 000000000000..da33a9173972 --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/zoom_install.golden.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# variables +APPDIR="/Applications/" +TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") + +# install pkg files +sudo installer -pkg "$TMPDIR/zoomusInstallerFull.pkg" -target / diff --git a/server/mdm/maintainedapps/testdata/scripts/zoom_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/zoom_uninstall.golden.sh new file mode 100644 index 000000000000..977a1ce000ca --- /dev/null +++ b/server/mdm/maintainedapps/testdata/scripts/zoom_uninstall.golden.sh @@ -0,0 +1,104 @@ +#!/bin/sh + +# variables +LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +remove_launchctl_service() { + local service="$1" + local booleans=("true" "false") + local plist_status + local paths + local sudo + + echo "Removing launchctl service ${service}" + + for sudo in "${booleans[@]}"; do + plist_status=$(launchctl list "${service}" 2>/dev/null) + + if [[ $plist_status == \{* ]]; then + if [[ $sudo == "true" ]]; then + sudo launchctl remove "${service}" + else + launchctl remove "${service}" + fi + sleep 1 + fi + + paths=( + "/Library/LaunchAgents/${service}.plist" + "/Library/LaunchDaemons/${service}.plist" + ) + + # if not using sudo, prepend the home directory to the paths + if [[ $sudo == "false" ]]; then + for i in "${!paths[@]}"; do + paths[i]="${HOME}${paths[i]}" + done + fi + + for path in "${paths[@]}"; do + if [[ -e "$path" ]]; then + if [[ $sudo == "true" ]]; then + sudo rm -f -- "$path" + else + rm -f -- "$path" + fi + fi + done + done +} + +trash() { + local logged_in_user="$1" + local target_file="$2" + + # replace ~ with /Users/$logged_in_user + if [[ "$target_file" == ~* ]]; then + target_file="/Users/$logged_in_user${target_file:1}" + fi + + local trash="/Users/$logged_in_user/.Trash" + local file_name="$(basename "${target_file}")" + + if [[ -e "$target_file" ]]; then + echo "removing $target_file." + mv -f "$target_file" "$trash/${file_name}" + else + echo "$target_file doesn't exist." + fi +} + +remove_launchctl_service 'us.zoom.ZoomDaemon' +sudo pkgutil --forget 'us.zoom.pkg.videomeeting' +sudo rm -rf /Applications/zoom.us.app +sudo rm -rf /Library/Internet Plug-Ins/ZoomUsPlugIn.plugin +sudo rm -rf /Library/Logs/DiagnosticReports/zoom.us* +sudo rm -rf /Library/PrivilegedHelperTools/us.zoom.ZoomDaemon +trash $LOGGED_IN_USER '~/.zoomus' +trash $LOGGED_IN_USER '~/Desktop/Zoom' +trash $LOGGED_IN_USER '~/Documents/Zoom' +trash $LOGGED_IN_USER '~/Library/Application Scripts/*.ZoomClient3rd' +trash $LOGGED_IN_USER '~/Library/Application Support/CloudDocs/session/containers/iCloud.us.zoom.videomeetings' +trash $LOGGED_IN_USER '~/Library/Application Support/CloudDocs/session/containers/iCloud.us.zoom.videomeetings.plist' +trash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/us.zoom*.sfl*' +trash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/zoom.us*' +trash $LOGGED_IN_USER '~/Library/Application Support/zoom.us' +trash $LOGGED_IN_USER '~/Library/Caches/us.zoom.xos' +trash $LOGGED_IN_USER '~/Library/Cookies/us.zoom.xos.binarycookies' +trash $LOGGED_IN_USER '~/Library/Group Containers/*.ZoomClient3rd' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/us.zoom.xos' +trash $LOGGED_IN_USER '~/Library/HTTPStorages/us.zoom.xos.binarycookies' +trash $LOGGED_IN_USER '~/Library/Internet Plug-Ins/ZoomUsPlugIn.plugin' +trash $LOGGED_IN_USER '~/Library/Logs/zoom.us' +trash $LOGGED_IN_USER '~/Library/Logs/zoominstall.log' +trash $LOGGED_IN_USER '~/Library/Logs/ZoomPhone' +trash $LOGGED_IN_USER '~/Library/Preferences/us.zoom.airhost.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/us.zoom.caphost.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/us.zoom.Transcode.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/us.zoom.xos.Hotkey.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/us.zoom.xos.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/us.zoom.ZoomAutoUpdater.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/us.zoom.ZoomClips.plist' +trash $LOGGED_IN_USER '~/Library/Preferences/ZoomChat.plist' +trash $LOGGED_IN_USER '~/Library/Saved Application State/us.zoom.xos.savedState' +trash $LOGGED_IN_USER '~/Library/WebKit/us.zoom.xos' diff --git a/server/service/maintained_apps.go b/server/service/maintained_apps.go index e4d0309318e4..1dd11dda772c 100644 --- a/server/service/maintained_apps.go +++ b/server/service/maintained_apps.go @@ -15,6 +15,7 @@ type addFleetMaintainedAppRequest struct { PreInstallQuery string `json:"pre_install_query"` PostInstallScript string `json:"post_install_script"` SelfService bool `json:"self_service"` + UninstallScript string `json:"uninstall_script"` } type addFleetMaintainedAppResponse struct { @@ -27,7 +28,16 @@ func addFleetMaintainedAppEndpoint(ctx context.Context, request interface{}, svc req := request.(*addFleetMaintainedAppRequest) ctx, cancel := context.WithTimeout(ctx, maintainedapps.InstallerTimeout) defer cancel() - err := svc.AddFleetMaintainedApp(ctx, req.TeamID, req.AppID, req.InstallScript, req.PreInstallQuery, req.PostInstallScript, req.SelfService) + err := svc.AddFleetMaintainedApp( + ctx, + req.TeamID, + req.AppID, + req.InstallScript, + req.PreInstallQuery, + req.PostInstallScript, + req.UninstallScript, + req.SelfService, + ) if err != nil { if errors.Is(err, context.DeadlineExceeded) { err = fleet.NewGatewayTimeoutError("Couldn't upload. Request timeout. Please make sure your server and load balancer timeout is long enough.", err) @@ -38,7 +48,7 @@ func addFleetMaintainedAppEndpoint(ctx context.Context, request interface{}, svc return &addFleetMaintainedAppResponse{}, nil } -func (svc *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript string, selfService bool) error { +func (svc *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript, uninstallScript string, selfService bool) error { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx)