diff --git a/.github/workflows/generate-desktop-app-tar-gz.yml b/.github/workflows/generate-desktop-app-tar-gz.yml
new file mode 100644
index 0000000000..efcaecb808
--- /dev/null
+++ b/.github/workflows/generate-desktop-app-tar-gz.yml
@@ -0,0 +1,62 @@
+name: Generate desktop.app.tar.gz for Orbit
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ # The workflow can be triggered by modifying FLEET_DESKTOP_VERSION env.
+ - '.github/workflows/generate-desktop-app-tar-gz.yml'
+ pull_request:
+ paths:
+ # The workflow can be triggered by modifying FLEET_DESKTOP_VERSION env.
+ - '.github/workflows/generate-desktop-app-tar-gz.yml'
+ workflow_dispatch:
+
+env:
+ FLEET_DESKTOP_VERSION: 0.0.1
+
+jobs:
+ release:
+ runs-on: macos-latest
+ steps:
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: '^1.17.0'
+
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Import signing keys
+ env:
+ APPLE_APPLICATION_CERTIFICATE: ${{ secrets.APPLE_APPLICATION_CERTIFICATE }}
+ APPLE_APPLICATION_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERTIFICATE_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ run: |
+ echo "$APPLE_APPLICATION_CERTIFICATE" | base64 --decode > certificate.p12
+ security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
+ security default-keychain -s build.keychain
+ security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
+ security import certificate.p12 -k build.keychain -P $APPLE_APPLICATION_CERTIFICATE_PASSWORD -T /usr/bin/codesign
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
+ security find-identity -vv
+ rm certificate.p12
+
+ - name: Generate desktop.app.tar.gz
+ env:
+ AC_USERNAME: ${{ secrets.APPLE_USERNAME }}
+ AC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+ CODESIGN_IDENTITY: 51049B247B25B3119FAE7E9C0CC4375A43E47237
+ run: |
+ AC_USERNAME=$AC_USERNAME \
+ AC_PASSWORD=$AC_PASSWORD \
+ FLEET_DESKTOP_APPLE_AUTHORITY=$CODESIGN_IDENTITY \
+ FLEET_DESKTOP_VERSION=$FLEET_DESKTOP_VERSION \
+ make desktop-app-tar-gz
+
+ - name: Upload desktop.app.tar.gz
+ uses: actions/upload-artifact@v2
+ with:
+ name: desktop.app.tar.gz
+ path: desktop.app.tar.gz
diff --git a/.gitignore b/.gitignore
index f7cb030721..feb9d737fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,7 @@ terraform.tfstate*
# generated installers
fleet-osquery*
+desktop.app.tar.gz
# residual files when running the cpe command
cmd/cpe/etagenv
@@ -65,4 +66,4 @@ cmd/cpe/cpe*.sqlite
cmd/cpe/cpe*.sqlite.gz
# Testing TUF server
-test_tuf
\ No newline at end of file
+test_tuf
diff --git a/Makefile b/Makefile
index 1df2cbe967..f3e75a27a8 100644
--- a/Makefile
+++ b/Makefile
@@ -300,3 +300,14 @@ endif
tar xf $(TMP_DIR)/osquery_pkg_expanded/Payload --directory $(TMP_DIR)/osquery_pkg_payload_expanded
tar czf $(out-path)/osqueryd.app.tar.gz -C $(TMP_DIR)/osquery_pkg_payload_expanded/opt/osquery/lib osquery.app
rm -r $(TMP_DIR)
+
+# Build and generate desktop.app.tar.gz bundle.
+#
+# Usage:
+# FLEET_DESKTOP_APPLE_AUTHORITY=foo FLEET_DESKTOP_VERSION=0.0.1 make desktop-app-tar-gz
+desktop-app-tar-gz:
+ifneq ($(shell uname), Darwin)
+ @echo "Makefile target desktop-app-tar-gz is only supported on macOS"
+ @exit 1
+endif
+ go run ./tools/desktop macos
diff --git a/changes/issue-3914-device-auth-token b/changes/issue-3914-device-auth-token
new file mode 100644
index 0000000000..4d7152eb6d
--- /dev/null
+++ b/changes/issue-3914-device-auth-token
@@ -0,0 +1 @@
+* (Beta) Introduce `Fleet Desktop` to macOS fleet installer with a tray icon that allows accessing a user device in Fleet.
diff --git a/changes/issue-4429-fleet-desktop-packaging b/changes/issue-4429-fleet-desktop-packaging
new file mode 100644
index 0000000000..07a3b6cadc
--- /dev/null
+++ b/changes/issue-4429-fleet-desktop-packaging
@@ -0,0 +1 @@
+* (Beta) Add `--fleet-desktop` flag to fleetctl for `--type=pkg` to generate a Fleet-osquery installer for macOS with "Fleet Desktop" support.
diff --git a/cmd/fleetctl/fleetctl.go b/cmd/fleetctl/fleetctl.go
index a017c6c4ea..8715862a19 100644
--- a/cmd/fleetctl/fleetctl.go
+++ b/cmd/fleetctl/fleetctl.go
@@ -13,7 +13,7 @@ import (
)
const (
- defaultFileMode = 0600
+ defaultFileMode = 0o600
)
func init() {
diff --git a/cmd/fleetctl/package.go b/cmd/fleetctl/package.go
index f43acc696c..8eaddd47b5 100644
--- a/cmd/fleetctl/package.go
+++ b/cmd/fleetctl/package.go
@@ -82,6 +82,12 @@ func packageCommand() *cli.Command {
Value: "stable",
Destination: &opt.OsquerydChannel,
},
+ &cli.StringFlag{
+ Name: "desktop-channel",
+ Usage: "Update channel of desktop to use",
+ Value: "stable",
+ Destination: &opt.DesktopChannel,
+ },
&cli.StringFlag{
Name: "orbit-channel",
Usage: "Update channel of Orbit to use",
@@ -118,6 +124,11 @@ func packageCommand() *cli.Command {
Name: "verbose",
Usage: "Log detailed information when building the package",
},
+ &cli.BoolFlag{
+ Name: "fleet-desktop",
+ Usage: "Include the Fleet Desktop Application in the package",
+ Destination: &opt.Desktop,
+ },
},
Action: func(c *cli.Context) error {
if opt.FleetURL != "" || opt.EnrollSecret != "" {
diff --git a/cmd/fleetctl/preview.go b/cmd/fleetctl/preview.go
index 41190a2d88..1da907a141 100644
--- a/cmd/fleetctl/preview.go
+++ b/cmd/fleetctl/preview.go
@@ -22,6 +22,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/packaging"
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
+ "github.com/fleetdm/fleet/v4/pkg/open"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/mitchellh/go-ps"
@@ -108,10 +109,10 @@ Use the stop and reset subcommands to manage the server and dependencies once st
// Make sure the logs directory is writable, otherwise the Fleet
// server errors on startup. This can be a problem when running on
// Linux with a non-root user inside the container.
- if err := os.Chmod(filepath.Join(previewDir, "logs"), 0777); err != nil {
+ if err := os.Chmod(filepath.Join(previewDir, "logs"), 0o777); err != nil {
return fmt.Errorf("make logs writable: %w", err)
}
- if err := os.Chmod(filepath.Join(previewDir, "vulndb"), 0777); err != nil {
+ if err := os.Chmod(filepath.Join(previewDir, "vulndb"), 0o777); err != nil {
return fmt.Errorf("make vulndb writable: %w", err)
}
@@ -269,7 +270,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
return fmt.Errorf("wait for current host: %w", err)
}
- if err := openBrowser("http://localhost:1337/previewlogin"); err != nil {
+ if err := open.Browser("http://localhost:1337/previewlogin"); err != nil {
fmt.Println("Automatic browser open failed. Please navigate to http://localhost:1337/previewlogin.")
}
@@ -286,7 +287,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
return errors.New("Failed to run docker-compose")
}
} else {
- if err := openBrowser("http://localhost:1337/previewlogin"); err != nil {
+ if err := open.Browser("http://localhost:1337/previewlogin"); err != nil {
fmt.Println("Automatic browser open failed. Please navigate to http://localhost:1337/previewlogin.")
}
}
@@ -582,7 +583,7 @@ func previewResetCommand() *cli.Command {
func storePidFile(destDir string, pid int) error {
pidFilePath := path.Join(destDir, "orbit.pid")
- err := os.WriteFile(pidFilePath, []byte(fmt.Sprint(pid)), os.FileMode(0644))
+ err := os.WriteFile(pidFilePath, []byte(fmt.Sprint(pid)), os.FileMode(0o644))
if err != nil {
return fmt.Errorf("error writing pidfile %s: %s", pidFilePath, err)
}
@@ -638,12 +639,8 @@ func downloadOrbitAndStart(destDir, enrollSecret, address, orbitChannel, osquery
}
// Override default channels with the provided values.
- orbit := updateOpt.Targets["orbit"]
- orbit.Channel = orbitChannel
- updateOpt.Targets["orbit"] = orbit
- osqueryd := updateOpt.Targets["osqueryd"]
- osqueryd.Channel = osquerydChannel
- updateOpt.Targets["osqueryd"] = osqueryd
+ updateOpt.Targets.SetTargetChannel("orbit", orbitChannel)
+ updateOpt.Targets.SetTargetChannel("osqueryd", osquerydChannel)
updateOpt.RootDirectory = destDir
@@ -712,20 +709,3 @@ func killFromPIDFile(destDir string, pidFileName string, expectedExecName string
}
return nil
}
-
-func openBrowser(url string) error {
- var cmd *exec.Cmd
- switch runtime.GOOS {
- case "windows":
- cmd = exec.Command("cmd", "/c", "start", url)
- case "darwin":
- cmd = exec.Command("open", url)
- default: // xdg-open is available on most Linux-y systems
- cmd = exec.Command("xdg-open", url)
- }
-
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to open in browser: %w", err)
- }
- return nil
-}
diff --git a/ee/fleetctl/updates_test.go b/ee/fleetctl/updates_test.go
index 6745076373..cac9826f84 100644
--- a/ee/fleetctl/updates_test.go
+++ b/ee/fleetctl/updates_test.go
@@ -267,7 +267,7 @@ func TestUpdatesIntegration(t *testing.T) {
require.NoError(t, err)
other, err := updater.Get("other")
require.NoError(t, err)
- require.Equal(t, filepath.Base(other), filepath.Base(testPath))
+ require.Equal(t, filepath.Base(other.ExecPath), filepath.Base(testPath))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
@@ -342,12 +342,12 @@ func TestUpdatesIntegration(t *testing.T) {
// Remove the old other copy first
o, err := updater.Get("other")
require.NoError(t, err)
- require.NoError(t, os.RemoveAll(filepath.Join(filepath.Dir(o), "other.app.tar.gz")))
- require.NoError(t, os.RemoveAll(filepath.Join(filepath.Dir(o), filepath.Base(testPath))))
+ require.NoError(t, os.RemoveAll(filepath.Join(filepath.Dir(o.ExecPath), "other.app.tar.gz")))
+ require.NoError(t, os.RemoveAll(filepath.Join(filepath.Dir(o.ExecPath), filepath.Base(testPath))))
o2, err := updater.Get("other")
require.NoError(t, err)
require.Equal(t, o, o2)
- _, err = os.Stat(o2)
+ _, err = os.Stat(o2.ExecPath)
require.NoError(t, err)
// Update client should be able to initialize with new root
diff --git a/go.mod b/go.mod
index 8b2341f60f..a718054114 100644
--- a/go.mod
+++ b/go.mod
@@ -29,6 +29,7 @@ require (
github.com/facebookincubator/nvdtools v0.1.4
github.com/fatih/color v1.12.0
github.com/fleetdm/goose v0.0.0-20220214194029-91b5e5eb8e77
+ github.com/getlantern/systray v1.2.0
github.com/getsentry/sentry-go v0.12.0
github.com/ghodss/yaml v1.0.0
github.com/go-kit/kit v0.9.0
@@ -37,7 +38,7 @@ require (
github.com/gocolly/colly v1.2.0
github.com/golang-jwt/jwt/v4 v4.0.0
github.com/gomodule/redigo v1.8.5
- github.com/google/go-cmp v0.5.6
+ github.com/google/go-cmp v0.5.7
github.com/google/go-github/v37 v37.0.0
github.com/google/uuid v1.3.0
github.com/goreleaser/goreleaser v1.1.0
@@ -77,6 +78,7 @@ require (
github.com/rs/zerolog v1.20.0
github.com/russellhaering/goxmldsig v1.1.0
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
+ github.com/shirou/gopsutil/v3 v3.22.2
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cast v1.3.1
github.com/spf13/cobra v1.2.1
diff --git a/go.sum b/go.sum
index 574d638605..e00d5993aa 100644
--- a/go.sum
+++ b/go.sum
@@ -402,6 +402,20 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
+github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
+github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
+github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
+github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
+github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
+github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
+github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
+github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
+github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
+github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
+github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
+github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
+github.com/getlantern/systray v1.2.0 h1:MsAdOcmOnm4V+r3HFONDszdZeoj7E3q2dEvsPdsxXtI=
+github.com/getlantern/systray v1.2.0/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk=
github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c=
github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -447,6 +461,8 @@ github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
github.com/go-logr/stdr v1.2.0 h1:j4LrlVXgrbIWO83mmQUnK0Hi+YnbD+vzrE1z/EphbFE=
github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
@@ -543,8 +559,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-github/v37 v37.0.0 h1:rCspN8/6kB1BAJWZfuafvHhyfIo5fkAulaP/3bOQ/tM=
github.com/google/go-github/v37 v37.0.0/go.mod h1:LM7in3NmXDrX58GbEHy7FtNLbI2JijX93RnMKvWG3m4=
github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
@@ -819,6 +836,8 @@ github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/macadmins/osquery-extension v0.0.5 h1:bF0xgFRU5oYU4BF5xwOCnQIi70RxeoJN8vzrDWv5CCU=
github.com/macadmins/osquery-extension v0.0.5/go.mod h1:GJLCEZWld1jM8fwLQJ4xixl8XkvWjWgQugRXFvlc/FA=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -936,6 +955,8 @@ github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je4
github.com/open-policy-agent/opa v0.24.0 h1:fnGOIux+TTGZsC0du1bRBtV8F+KPN55Hks12uE3Fq3E=
github.com/open-policy-agent/opa v0.24.0/go.mod h1:qEyD/i8j+RQettHGp4f86yjrjvv+ZYia+JHCMv2G7wA=
github.com/opencensus-integrations/ocsql v0.1.1/go.mod h1:ozPYpNVBHZsX33jfoQPO5TlI5lqh0/3R36kirEqJKAM=
+github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
+github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/oschwald/geoip2-golang v1.6.1 h1:GKxT3yaWWNXSb7vj6D7eoJBns+lGYgx08QO0UcNm0YY=
github.com/oschwald/geoip2-golang v1.6.1/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s=
github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk=
@@ -961,6 +982,8 @@ github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUI
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.0.0-20181025174421-f30f42803563/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
@@ -1029,6 +1052,8 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/shirou/gopsutil/v3 v3.22.2 h1:wCrArWFkHYIdDxx/FSfF5RB4dpJYW6t7rcp3+zL8uks=
+github.com/shirou/gopsutil/v3 v3.22.2/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@@ -1110,6 +1135,10 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
+github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo=
+github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
+github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
+github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
@@ -1158,6 +1187,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
+github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw=
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@@ -1455,6 +1486,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1479,10 +1511,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
diff --git a/orbit/cmd/desktop/desktop_darwin.go b/orbit/cmd/desktop/desktop_darwin.go
new file mode 100644
index 0000000000..f48af4f04c
--- /dev/null
+++ b/orbit/cmd/desktop/desktop_darwin.go
@@ -0,0 +1,107 @@
+package main
+
+import (
+ "crypto/tls"
+ _ "embed"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "time"
+
+ "github.com/fleetdm/fleet/v4/pkg/open"
+ "github.com/getlantern/systray"
+)
+
+//go:embed icon_white.png
+var icoBytes []byte
+
+func main() {
+ // Our TUF provided targets must support launching with "--help".
+ if len(os.Args) > 1 && os.Args[1] == "--help" {
+ fmt.Println("Fleet Desktop application executable")
+ return
+ }
+
+ devURL := os.Getenv("FLEET_DESKTOP_DEVICE_URL")
+ if devURL == "" {
+ log.Println("missing URL environment FLEET_DESKTOP_DEVICE_URL")
+ os.Exit(1)
+ }
+ deviceURL, err := url.Parse(devURL)
+ if err != nil {
+ log.Printf("invalid URL argument: %s\n", err)
+ os.Exit(1)
+ }
+ devTestPath := os.Getenv("FLEET_DESKTOP_DEVICE_API_TEST_PATH")
+ if devTestPath == "" {
+ log.Println("missing URL environment FLEET_DESKTOP_DEVICE_API_TEST_PATH")
+ os.Exit(1)
+ }
+ devTestURL := *deviceURL
+ devTestURL.Path = devTestPath
+
+ onReady := func() {
+ log.Println("ready")
+
+ systray.SetIcon(icoBytes)
+ systray.SetTooltip("Fleet Device Management Menu.")
+ myDeviceItem := systray.AddMenuItem("Initializing...", "")
+ myDeviceItem.Disable()
+ transparencyItem := systray.AddMenuItem("Transparency", "")
+
+ // Perform API test call to enable the "My device" item as soon
+ // as the device auth token is registered by Fleet.
+ go func() {
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ tr := http.DefaultTransport.(*http.Transport)
+ if os.Getenv("FLEET_DESKTOP_INSECURE") != "" {
+ tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+ }
+ client := &http.Client{
+ Transport: tr,
+ }
+
+ for {
+ resp, err := client.Get(devTestURL.String())
+ if err != nil {
+ // To ease troubleshooting we set the tooltip as the error.
+ myDeviceItem.SetTooltip(err.Error())
+ log.Printf("get device URL: %s", err)
+ } else {
+ resp.Body.Close()
+ if resp.StatusCode == http.StatusOK {
+ myDeviceItem.SetTitle("My device")
+ myDeviceItem.Enable()
+ myDeviceItem.SetTooltip("")
+ return
+ }
+ }
+ <-ticker.C
+ }
+ }()
+
+ go func() {
+ for {
+ select {
+ case <-myDeviceItem.ClickedCh:
+ if err := open.Browser(deviceURL.String()); err != nil {
+ log.Printf("open browser my device: %s", err)
+ }
+ case <-transparencyItem.ClickedCh:
+ if err := open.Browser("https://fleetdm.com/transparency"); err != nil {
+ log.Printf("open browser transparency: %s", err)
+ }
+ }
+ }
+ }()
+ }
+ onExit := func() {
+ log.Println("exit")
+ }
+
+ systray.Run(onReady, onExit)
+}
diff --git a/orbit/cmd/desktop/icon_white.png b/orbit/cmd/desktop/icon_white.png
new file mode 100644
index 0000000000..0a3942b229
Binary files /dev/null and b/orbit/cmd/desktop/icon_white.png differ
diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go
index 786c4679a9..1ed663e72e 100644
--- a/orbit/cmd/orbit/orbit.go
+++ b/orbit/cmd/orbit/orbit.go
@@ -9,6 +9,7 @@ import (
"io/ioutil"
"net/url"
"os"
+ "path"
"path/filepath"
"runtime"
"strings"
@@ -22,11 +23,13 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/update/filestore"
"github.com/fleetdm/fleet/v4/pkg/certificate"
"github.com/fleetdm/fleet/v4/pkg/file"
+ "github.com/fleetdm/fleet/v4/pkg/open"
"github.com/fleetdm/fleet/v4/pkg/secure"
"github.com/google/uuid"
"github.com/oklog/run"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
+ gopsutil_process "github.com/shirou/gopsutil/v3/process"
"github.com/urfave/cli/v2"
"gopkg.in/natefinch/lumberjack.v2"
)
@@ -96,6 +99,12 @@ func main() {
Value: "stable",
EnvVars: []string{"ORBIT_ORBIT_CHANNEL"},
},
+ &cli.StringFlag{
+ Name: "desktop-channel",
+ Usage: "Update channel of Fleet Desktop to use",
+ Value: "stable",
+ EnvVars: []string{"ORBIT_DESKTOP_CHANNEL"},
+ },
&cli.BoolFlag{
Name: "disable-updates",
Usage: "Disables auto updates",
@@ -123,6 +132,11 @@ func main() {
Name: "dev-darwin-legacy-targets",
Usage: "Use darwin legacy target (flag only used on darwin)",
},
+ &cli.BoolFlag{
+ Name: "fleet-desktop",
+ Usage: "Launch Fleet Desktop application (flag currently only used on darwin)",
+ EnvVars: []string{"ORBIT_FLEET_DESKTOP"},
+ },
}
app.Action = func(c *cli.Context) error {
if c.Bool("version") {
@@ -193,7 +207,7 @@ func main() {
localStore, err := filestore.New(filepath.Join(c.String("root-dir"), "tuf-metadata.json"))
if err != nil {
- log.Fatal().Err(err).Msg("failed to create local metadata store")
+ log.Fatal().Err(err).Msg("create local metadata store")
}
opt := update.DefaultOptions
@@ -202,13 +216,15 @@ func main() {
opt.Targets = update.DarwinLegacyTargets
}
+ if runtime.GOOS == "darwin" && c.Bool("fleet-desktop") {
+ opt.Targets["desktop"] = update.DesktopMacOSTarget
+ // Override default channel with the provided value.
+ opt.Targets.SetTargetChannel("desktop", c.String("desktop-channel"))
+ }
+
// Override default channels with the provided values.
- orbit := opt.Targets["orbit"]
- orbit.Channel = c.String("orbit-channel")
- opt.Targets["orbit"] = orbit
- osqueryd := opt.Targets["osqueryd"]
- osqueryd.Channel = c.String("osqueryd-channel")
- opt.Targets["osqueryd"] = osqueryd
+ opt.Targets.SetTargetChannel("orbit", c.String("orbit-channel"))
+ opt.Targets.SetTargetChannel("osqueryd", c.String("osqueryd-channel"))
opt.RootDirectory = c.String("root-dir")
opt.ServerURL = c.String("update-url")
@@ -216,8 +232,9 @@ func main() {
opt.InsecureTransport = c.Bool("insecure")
var (
- updater *update.Updater
- osquerydPath string
+ updater *update.Updater
+ osquerydPath string
+ desktopAppPath string
)
// NOTE: When running in dev-mode, even if `disable-updates` is set,
@@ -225,21 +242,35 @@ func main() {
if !c.Bool("disable-updates") || c.Bool("dev-mode") {
updater, err = update.New(opt)
if err != nil {
- return fmt.Errorf("failed to create updater: %w", err)
+ return fmt.Errorf("create updater: %w", err)
}
if err := updater.UpdateMetadata(); err != nil {
- log.Info().Err(err).Msg("failed to update metadata. using saved metadata.")
+ log.Info().Err(err).Msg("update metadata. using saved metadata.")
}
- osquerydPath, err = updater.Get("osqueryd")
+ osquerydLocalTarget, err := updater.Get("osqueryd")
if err != nil {
- return fmt.Errorf("failed to get osqueryd target: %w", err)
+ return fmt.Errorf("get osqueryd target: %w", err)
+ }
+ osquerydPath = osquerydLocalTarget.ExecPath
+ if runtime.GOOS == "darwin" && c.Bool("fleet-desktop") {
+ fleetDesktopLocalTarget, err := updater.Get("desktop")
+ if err != nil {
+ return fmt.Errorf("get desktop target: %w", err)
+ }
+ desktopAppPath = fleetDesktopLocalTarget.DirPath
}
} else {
log.Info().Msg("running with auto updates disabled")
updater = update.NewDisabled(opt)
osquerydPath, err = updater.ExecutableLocalPath("osqueryd")
if err != nil {
- log.Fatal().Err(err).Msg("failed to locate osqueryd")
+ log.Fatal().Err(err).Msg("locate osqueryd")
+ }
+ if runtime.GOOS == "darwin" && c.Bool("fleet-desktop") {
+ desktopAppPath, err = updater.DirLocalPath("desktop")
+ if err != nil {
+ return fmt.Errorf("get desktop target: %w", err)
+ }
}
}
@@ -251,7 +282,7 @@ func main() {
}
if err := os.RemoveAll(path); err != nil {
- log.Info().Err(err).Msg("failed to remove .old")
+ log.Info().Err(err).Msg("remove .old")
return nil
}
log.Debug().Str("path", path).Msg("cleaned up old")
@@ -264,9 +295,13 @@ func main() {
var g run.Group
if !c.Bool("disable-updates") {
+ targets := []string{"orbit", "osqueryd"}
+ if runtime.GOOS == "darwin" && c.Bool("fleet-desktop") {
+ targets = append(targets, "desktop")
+ }
updateRunner, err := update.NewRunner(updater, update.RunnerOptions{
CheckInterval: 10 * time.Second,
- Targets: []string{"orbit", "osqueryd"},
+ Targets: targets,
})
if err != nil {
return err
@@ -414,7 +449,7 @@ func main() {
// Create an osquery runner with the provided options.
r, err := osquery.NewRunner(osquerydPath, options...)
if err != nil {
- return fmt.Errorf("failed to create osquery runner: %w", err)
+ return fmt.Errorf("create osquery runner: %w", err)
}
g.Add(r.Execute, r.Interrupt)
@@ -423,6 +458,11 @@ func main() {
}))
g.Add(ext.Execute, ext.Interrupt)
+ if runtime.GOOS == "darwin" && c.Bool("fleet-desktop") {
+ desktopRunner := newDesktopRunner(desktopAppPath, fleetURL, deviceAuthToken, c.Bool("insecure"))
+ g.Add(desktopRunner.actor())
+ }
+
// Install a signal handler
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -440,6 +480,71 @@ func main() {
}
}
+type desktopRunner struct {
+ appPath string
+ fleetURL string
+ deviceAuthToken string
+ insecure bool
+ done chan struct{}
+}
+
+func newDesktopRunner(appPath, fleetURL, deviceAuthToken string, insecure bool) *desktopRunner {
+ return &desktopRunner{
+ appPath: appPath,
+ fleetURL: fleetURL,
+ deviceAuthToken: deviceAuthToken,
+ insecure: insecure,
+ done: make(chan struct{}),
+ }
+}
+
+func (d *desktopRunner) actor() (func() error, func(error)) {
+ return d.execute, d.interrupt
+}
+
+func (d *desktopRunner) execute() error {
+ log.Info().Str("path", d.appPath).Msg("opening")
+ url, err := url.Parse(d.fleetURL)
+ if err != nil {
+ return fmt.Errorf("invalid fleet-url: %w", err)
+ }
+ url.Path = path.Join(url.Path, "device", d.deviceAuthToken)
+ opts := []open.AppOption{
+ open.AppWithEnv("FLEET_DESKTOP_DEVICE_URL", url.String()),
+ open.AppWithEnv("FLEET_DESKTOP_DEVICE_API_TEST_PATH", path.Join("api", "latest", "fleet", "device", d.deviceAuthToken)),
+ }
+ if d.insecure {
+ opts = append(opts, open.AppWithEnv("FLEET_DESKTOP_INSECURE", "1"))
+ }
+ if err := open.App(d.appPath, opts...); err != nil {
+ return fmt.Errorf("open desktop app: %w", err)
+ }
+
+ // Monitor the fleet-desktop application.
+ ticker := time.NewTicker(10 * time.Second)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-d.done:
+ return nil
+ case <-ticker.C:
+ if _, err := getProcessByName(constant.DesktopAppExecName); err != nil {
+ log.Err(err).Msg("desktopRunner.execute exit")
+ return fmt.Errorf("get desktop process: %w", err)
+ }
+ }
+ }
+}
+
+func (d *desktopRunner) interrupt(err error) {
+ log.Debug().Err(err).Msg("interrupt desktopRunner")
+ defer close(d.done)
+
+ if err := killProcessByName(constant.DesktopAppExecName); err != nil {
+ log.Error().Err(err).Msg("killProcess")
+ }
+}
+
func loadOrGenerateToken(rootDir string) (string, error) {
filePath := filepath.Join(rootDir, "identifier")
id, err := ioutil.ReadFile(filePath)
@@ -460,6 +565,42 @@ func loadOrGenerateToken(rootDir string) (string, error) {
}
}
+func killProcessByName(name string) error {
+ foundProcess, err := getProcessByName(name)
+ if err != nil {
+ return fmt.Errorf("get process: %w", err)
+ }
+ if err := foundProcess.Kill(); err != nil {
+ return fmt.Errorf("kill process %d: %w", foundProcess.Pid, err)
+ }
+ return nil
+}
+
+var errProcessNotFound = errors.New("process not found")
+
+func getProcessByName(name string) (*gopsutil_process.Process, error) {
+ processes, err := gopsutil_process.Processes()
+ if err != nil {
+ return nil, err
+ }
+ var foundProcess *gopsutil_process.Process
+ for _, process := range processes {
+ processName, err := process.Name()
+ if err != nil {
+ log.Debug().Err(err).Int32("pid", process.Pid).Msg("get process name")
+ continue
+ }
+ if processName == name {
+ foundProcess = process
+ break
+ }
+ }
+ if foundProcess == nil {
+ return nil, errProcessNotFound
+ }
+ return foundProcess, nil
+}
+
var versionCommand = &cli.Command{
Name: "version",
Usage: "Get the orbit version",
diff --git a/orbit/cmd/orbit/shell.go b/orbit/cmd/orbit/shell.go
index 560115400d..a5dadd7dda 100644
--- a/orbit/cmd/orbit/shell.go
+++ b/orbit/cmd/orbit/shell.go
@@ -53,10 +53,8 @@ var shellCommand = &cli.Command{
// Initialize updater and get expected version
opt := update.DefaultOptions
- // Override default channels with the provided values.
- osqueryd := opt.Targets["osqueryd"]
- osqueryd.Channel = c.String("osqueryd-channel")
- opt.Targets["osqueryd"] = osqueryd
+ // Override default channel with the provided value.
+ opt.Targets.SetTargetChannel("osqueryd", c.String("osqueryd-channel"))
opt.RootDirectory = c.String("root-dir")
opt.ServerURL = c.String("update-url")
@@ -70,10 +68,11 @@ var shellCommand = &cli.Command{
if err := updater.UpdateMetadata(); err != nil {
log.Info().Err(err).Msg("failed to update metadata. using saved metadata.")
}
- osquerydPath, err := updater.Get("osqueryd")
+ osquerydLocalTarget, err := updater.Get("osqueryd")
if err != nil {
return err
}
+ osquerydPath := osquerydLocalTarget.ExecPath
var g run.Group
diff --git a/orbit/pkg/constant/constant.go b/orbit/pkg/constant/constant.go
index dd65a0cf37..2feb781210 100644
--- a/orbit/pkg/constant/constant.go
+++ b/orbit/pkg/constant/constant.go
@@ -5,4 +5,6 @@ const (
DefaultDirMode = 0o755
// DefaultFileMode is the default file mode to apply to created files.
DefaultFileMode = 0o600
+ // DesktopAppExecName is the name of Fleet's Desktop executable.
+ DesktopAppExecName = "fleet-desktop"
)
diff --git a/orbit/pkg/packaging/linux_shared.go b/orbit/pkg/packaging/linux_shared.go
index 4801a91c53..49692fab9e 100644
--- a/orbit/pkg/packaging/linux_shared.go
+++ b/orbit/pkg/packaging/linux_shared.go
@@ -34,19 +34,14 @@ func buildNFPM(opt Options, pkger nfpm.Packager) (string, error) {
}
// Initialize autoupdate metadata
-
updateOpt := update.DefaultOptions
updateOpt.RootDirectory = orbitRoot
updateOpt.Targets = update.LinuxTargets
// Override default channels with the provided values.
- orbit := updateOpt.Targets["orbit"]
- orbit.Channel = opt.OrbitChannel
- updateOpt.Targets["orbit"] = orbit
- osqueryd := updateOpt.Targets["osqueryd"]
- osqueryd.Channel = opt.OsquerydChannel
- updateOpt.Targets["osqueryd"] = osqueryd
+ updateOpt.Targets.SetTargetChannel("orbit", opt.OrbitChannel)
+ updateOpt.Targets.SetTargetChannel("osqueryd", opt.OsquerydChannel)
updateOpt.ServerURL = opt.UpdateURL
if opt.UpdateRoots != "" {
diff --git a/orbit/pkg/packaging/macos.go b/orbit/pkg/packaging/macos.go
index 2f229f09f0..65da1ff44e 100644
--- a/orbit/pkg/packaging/macos.go
+++ b/orbit/pkg/packaging/macos.go
@@ -45,13 +45,15 @@ func BuildPkg(opt Options) (string, error) {
updateOpt.ServerURL = opt.UpdateURL
updateOpt.Targets = update.DarwinTargets
+ if opt.Desktop {
+ updateOpt.Targets["desktop"] = update.DesktopMacOSTarget
+ // Override default channel with the provided value.
+ updateOpt.Targets.SetTargetChannel("desktop", opt.DesktopChannel)
+ }
+
// Override default channels with the provided values.
- orbit := updateOpt.Targets["orbit"]
- orbit.Channel = opt.OrbitChannel
- updateOpt.Targets["orbit"] = orbit
- osqueryd := updateOpt.Targets["osqueryd"]
- osqueryd.Channel = opt.OsquerydChannel
- updateOpt.Targets["osqueryd"] = osqueryd
+ updateOpt.Targets.SetTargetChannel("orbit", opt.OrbitChannel)
+ updateOpt.Targets.SetTargetChannel("osqueryd", opt.OsquerydChannel)
if opt.UpdateRoots != "" {
updateOpt.RootKeys = opt.UpdateRoots
@@ -160,7 +162,7 @@ func writeScripts(opt Options, rootPath string) error {
return fmt.Errorf("execute template: %w", err)
}
- if err := ioutil.WriteFile(path, contents.Bytes(), 0744); err != nil {
+ if err := ioutil.WriteFile(path, contents.Bytes(), 0o744); err != nil {
return fmt.Errorf("write file: %w", err)
}
@@ -179,7 +181,7 @@ func writeLaunchd(opt Options, rootPath string) error {
return fmt.Errorf("execute template: %w", err)
}
- if err := ioutil.WriteFile(path, contents.Bytes(), 0644); err != nil {
+ if err := ioutil.WriteFile(path, contents.Bytes(), 0o644); err != nil {
return fmt.Errorf("write file: %w", err)
}
@@ -209,7 +211,7 @@ func writeCertificate(opt Options, orbitRoot string) error {
// Fleet TLS certificate
dstPath := filepath.Join(orbitRoot, "fleet.pem")
- if err := file.Copy(opt.FleetCertificate, dstPath, 0644); err != nil {
+ if err := file.Copy(opt.FleetCertificate, dstPath, 0o644); err != nil {
return fmt.Errorf("write orbit: %w", err)
}
@@ -299,7 +301,7 @@ func xarBom(opt Options, rootPath string) error {
func cpio(srcPath, dstPath string) error {
// This is the compression routine that is expected for pkg files.
- dst, err := secure.OpenFile(dstPath, os.O_RDWR|os.O_CREATE, 0755)
+ dst, err := secure.OpenFile(dstPath, os.O_RDWR|os.O_CREATE, 0o755)
if err != nil {
return fmt.Errorf("open dst: %w", err)
}
diff --git a/orbit/pkg/packaging/macos_templates.go b/orbit/pkg/packaging/macos_templates.go
index 6cca1a4059..1770e23bb1 100644
--- a/orbit/pkg/packaging/macos_templates.go
+++ b/orbit/pkg/packaging/macos_templates.go
@@ -94,6 +94,10 @@ var macosLaunchdTemplate = template.Must(template.New("").Option("missingkey=err
{{ .OsquerydChannel }}
ORBIT_UPDATE_URL
{{ .UpdateURL }}
+ {{- if .Desktop }}
+ ORBIT_FLEET_DESKTOP
+ true
+ {{- end }}
KeepAlive
diff --git a/orbit/pkg/packaging/packaging.go b/orbit/pkg/packaging/packaging.go
index b5bc179546..87549cafd9 100644
--- a/orbit/pkg/packaging/packaging.go
+++ b/orbit/pkg/packaging/packaging.go
@@ -45,6 +45,8 @@ type Options struct {
OrbitChannel string
// OsquerydChannel is the update channel to use for Osquery (osqueryd).
OsquerydChannel string
+ // DesktopChannel is the update channel to use for the Fleet Desktop application.
+ DesktopChannel string
// UpdateURL is the base URL of the update server (TUF repository).
UpdateURL string
// UpdateRoots is the root JSON metadata for update server (TUF repository).
@@ -53,6 +55,8 @@ type Options struct {
OsqueryFlagfile string
// Debug determines whether to enable debug logging for the agent.
Debug bool
+ // Desktop determines whether to package the Fleet Desktop application.
+ Desktop bool
}
func initializeTempDir() (string, error) {
@@ -62,7 +66,7 @@ func initializeTempDir() (string, error) {
return "", fmt.Errorf("failed to create temp dir: %w", err)
}
- if err := os.Chmod(tmpDir, 0755); err != nil {
+ if err := os.Chmod(tmpDir, 0o755); err != nil {
_ = os.RemoveAll(tmpDir)
return "", fmt.Errorf("change temp directory permissions: %w", err)
}
@@ -77,6 +81,9 @@ type UpdatesData struct {
OsquerydPath string
OsquerydVersion string
+
+ DesktopPath string
+ DesktopVersion string
}
func (u UpdatesData) String() string {
@@ -101,10 +108,12 @@ func InitializeUpdates(updateOpt update.Options) (*UpdatesData, error) {
if err := updater.UpdateMetadata(); err != nil {
return nil, fmt.Errorf("failed to update metadata: %w", err)
}
- osquerydPath, err := updater.Get("osqueryd")
+
+ osquerydLocalTarget, err := updater.Get("osqueryd")
if err != nil {
return nil, fmt.Errorf("failed to get osqueryd: %w", err)
}
+ osquerydPath := osquerydLocalTarget.ExecPath
osquerydMeta, err := updater.Lookup("osqueryd")
if err != nil {
return nil, fmt.Errorf("failed to get osqueryd metadata: %w", err)
@@ -116,10 +125,12 @@ func InitializeUpdates(updateOpt update.Options) (*UpdatesData, error) {
if err := json.Unmarshal(*osquerydMeta.Custom, &osquerydCustom); err != nil {
return nil, fmt.Errorf("failed to get osqueryd version: %w", err)
}
- orbitPath, err := updater.Get("orbit")
+
+ orbitLocalTarget, err := updater.Get("orbit")
if err != nil {
return nil, fmt.Errorf("failed to get orbit: %w", err)
}
+ orbitPath := orbitLocalTarget.ExecPath
orbitMeta, err := updater.Lookup("orbit")
if err != nil {
return nil, fmt.Errorf("failed to get orbit metadata: %w", err)
@@ -129,6 +140,25 @@ func InitializeUpdates(updateOpt update.Options) (*UpdatesData, error) {
return nil, fmt.Errorf("failed to get orbit version: %w", err)
}
+ var (
+ desktopPath string
+ desktopCustom custom
+ )
+ if _, ok := updateOpt.Targets["desktop"]; ok {
+ desktopLocalTarget, err := updater.Get("desktop")
+ if err != nil {
+ return nil, fmt.Errorf("failed to get desktop: %w", err)
+ }
+ desktopPath = desktopLocalTarget.ExecPath
+ desktopMeta, err := updater.Lookup("desktop")
+ if err != nil {
+ return nil, fmt.Errorf("failed to get orbit metadata: %w", err)
+ }
+ if err := json.Unmarshal(*desktopMeta.Custom, &desktopCustom); err != nil {
+ return nil, fmt.Errorf("failed to get orbit version: %w", err)
+ }
+ }
+
if devBuildPath := os.Getenv("FLEETCTL_ORBIT_DEV_BUILD_PATH"); devBuildPath != "" {
updater.CopyDevBuild("orbit", devBuildPath)
}
@@ -139,6 +169,9 @@ func InitializeUpdates(updateOpt update.Options) (*UpdatesData, error) {
OsquerydPath: osquerydPath,
OsquerydVersion: osquerydCustom.Version,
+
+ DesktopPath: desktopPath,
+ DesktopVersion: desktopCustom.Version,
}, nil
}
@@ -149,7 +182,7 @@ func writeSecret(opt Options, orbitRoot string) error {
return fmt.Errorf("mkdir: %w", err)
}
- if err := ioutil.WriteFile(path, []byte(opt.EnrollSecret), 0600); err != nil {
+ if err := ioutil.WriteFile(path, []byte(opt.EnrollSecret), 0o600); err != nil {
return fmt.Errorf("write file: %w", err)
}
@@ -183,7 +216,7 @@ var osqueryCerts []byte
func writeOsqueryCertPEM(opt Options, orbitRoot string) error {
dstPath := filepath.Join(orbitRoot, "certs.pem")
- if err := ioutil.WriteFile(dstPath, osqueryCerts, 0644); err != nil {
+ if err := ioutil.WriteFile(dstPath, osqueryCerts, 0o644); err != nil {
return fmt.Errorf("write file: %w", err)
}
diff --git a/orbit/pkg/packaging/windows.go b/orbit/pkg/packaging/windows.go
index c097623a50..0b476ab4af 100644
--- a/orbit/pkg/packaging/windows.go
+++ b/orbit/pkg/packaging/windows.go
@@ -42,12 +42,8 @@ func BuildMSI(opt Options) (string, error) {
updateOpt.Targets = update.WindowsTargets
// Override default channels with the provided values.
- orbit := updateOpt.Targets["orbit"]
- orbit.Channel = opt.OrbitChannel
- updateOpt.Targets["orbit"] = orbit
- osqueryd := updateOpt.Targets["osqueryd"]
- osqueryd.Channel = opt.OsquerydChannel
- updateOpt.Targets["osqueryd"] = osqueryd
+ updateOpt.Targets.SetTargetChannel("orbit", opt.OrbitChannel)
+ updateOpt.Targets.SetTargetChannel("osqueryd", opt.OsquerydChannel)
updateOpt.ServerURL = opt.UpdateURL
if opt.UpdateRoots != "" {
diff --git a/orbit/pkg/update/options.go b/orbit/pkg/update/options.go
index 952edca4b2..2d5411ccfd 100644
--- a/orbit/pkg/update/options.go
+++ b/orbit/pkg/update/options.go
@@ -1,5 +1,7 @@
package update
+import "github.com/fleetdm/fleet/v4/orbit/pkg/constant"
+
// DefaultOptions are the default options to use when creating an update
// client.
var DefaultOptions = defaultOptions
@@ -57,4 +59,11 @@ var (
TargetFile: "osqueryd.exe",
},
}
+
+ DesktopMacOSTarget = TargetInfo{
+ Platform: "macos",
+ Channel: "stable",
+ TargetFile: "desktop.app.tar.gz",
+ ExtractedExecSubPath: []string{"Fleet Desktop.app", "Contents", "MacOS", constant.DesktopAppExecName},
+ }
)
diff --git a/orbit/pkg/update/runner.go b/orbit/pkg/update/runner.go
index 4102e4f5bd..3d5d17e924 100644
--- a/orbit/pkg/update/runner.go
+++ b/orbit/pkg/update/runner.go
@@ -54,7 +54,7 @@ func NewRunner(updater *Updater, opt RunnerOptions) (*Runner, error) {
if err != nil {
return nil, fmt.Errorf("failed to get local path for %s: %w", target, err)
}
- _, localHash, err := fileHashes(meta, localTarget.path)
+ _, localHash, err := fileHashes(meta, localTarget.Path)
if err != nil {
return nil, fmt.Errorf("%s file hash: %w", target, err)
}
@@ -135,10 +135,11 @@ func (r *Runner) updateAction() (bool, error) {
}
func (r *Runner) updateTarget(target string) error {
- path, err := r.updater.Get(target)
+ localTarget, err := r.updater.Get(target)
if err != nil {
return fmt.Errorf("get binary: %w", err)
}
+ path := localTarget.ExecPath
if target != "orbit" {
return nil
diff --git a/orbit/pkg/update/update.go b/orbit/pkg/update/update.go
index 93977c7c61..a87436b0fb 100644
--- a/orbit/pkg/update/update.go
+++ b/orbit/pkg/update/update.go
@@ -66,6 +66,13 @@ type Options struct {
// Targets is a map of target name and its tracking information.
type Targets map[string]TargetInfo
+// SetTargetChannel sets the channel of a target in the map.
+func (ts Targets) SetTargetChannel(target, channel string) {
+ t := ts[target]
+ t.Channel = channel
+ ts[target] = t
+}
+
// TargetInfo holds all the information to track target updates.
type TargetInfo struct {
// Platform is the target's platform string.
@@ -157,53 +164,70 @@ func (u *Updater) repoPath(target string) (string, error) {
return path.Join(target, t.Platform, t.Channel, t.TargetFile), nil
}
-// ExecutableLocalPath returns the configured local path of a target.
+// ExecutableLocalPath returns the configured executable local path of a target.
func (u *Updater) ExecutableLocalPath(target string) (string, error) {
localTarget, err := u.localTarget(target)
if err != nil {
return "", err
}
- return localTarget.execPath, nil
+ return localTarget.ExecPath, nil
}
-// localTarget holds local paths of a target.
+// DirLocalPath returns the configured root directory local path of a tar.gz target.
+//
+// Returns empty for a non tar.gz target.
+func (u *Updater) DirLocalPath(target string) (string, error) {
+ localTarget, err := u.localTarget(target)
+ if err != nil {
+ return "", err
+ }
+ return localTarget.DirPath, nil
+}
+
+// LocalTarget holds local paths of a target.
//
// E.g., for a osqueryd target:
//
-// localTarget{
-// info: TargetInfo{
+// LocalTarget{
+// Info: TargetInfo{
// Platform: "macos-app",
// Channel: "stable",
// TargetFile: "osqueryd.app.tar.gz",
// ExtractedExecSubPath: []string{"osquery.app", "Contents", "MacOS", "osqueryd"},
// },
-// path: "/local/path/to/osqueryd.app.tar.gz",
-// dirPath: "/local/path/to/osqueryd.app",
-// execPath: "/local/path/to/osqueryd.app/Contents/MacOS/osqueryd",
+// Path: "/local/path/to/osqueryd.app.tar.gz",
+// DirPath: "/local/path/to/osqueryd.app",
+// ExecPath: "/local/path/to/osqueryd.app/Contents/MacOS/osqueryd",
// }
-type localTarget struct {
- info TargetInfo
- path string
- dirPath string // empty for non-tar.gz targets.
- execPath string
+type LocalTarget struct {
+ // Info holds the TUF target and package structure info.
+ Info TargetInfo
+ // Path holds the location of the target as downloaded from TUF.
+ Path string
+ // DirPath holds the path of the extracted target.
+ //
+ // DirPath is empty for non-tar.gz targets.
+ DirPath string
+ // ExecPath is the path of the executable.
+ ExecPath string
}
-// localPath returns the info and local path of a target.
-func (u *Updater) localTarget(target string) (*localTarget, error) {
+// localTarget returns the info and local path of a target.
+func (u *Updater) localTarget(target string) (*LocalTarget, error) {
t, ok := u.opt.Targets[target]
if !ok {
return nil, fmt.Errorf("unknown target: %s", target)
}
- lt := &localTarget{
- info: t,
- path: filepath.Join(
+ lt := &LocalTarget{
+ Info: t,
+ Path: filepath.Join(
u.opt.RootDirectory, binDir, target, t.Platform, t.Channel, t.TargetFile,
),
}
- lt.execPath = lt.path
- if strings.HasSuffix(lt.path, ".tar.gz") {
- lt.execPath = filepath.Join(append([]string{filepath.Dir(lt.path)}, t.ExtractedExecSubPath...)...)
- lt.dirPath = filepath.Join(filepath.Dir(lt.path), lt.info.ExtractedExecSubPath[0])
+ lt.ExecPath = lt.Path
+ if strings.HasSuffix(lt.Path, ".tar.gz") {
+ lt.ExecPath = filepath.Join(append([]string{filepath.Dir(lt.Path)}, t.ExtractedExecSubPath...)...)
+ lt.DirPath = filepath.Join(filepath.Dir(lt.Path), lt.Info.ExtractedExecSubPath[0])
}
return lt, nil
}
@@ -232,76 +256,74 @@ func (u *Updater) Targets() (data.TargetFiles, error) {
return targets, nil
}
-// Get returns the local path to the specified target. The target is downloaded
-// if it does not yet exist locally or the hash does not match.
-func (u *Updater) Get(target string) (string, error) {
+// Get downloads (if it doesn't exist) a target and returns its local information.
+func (u *Updater) Get(target string) (*LocalTarget, error) {
if target == "" {
- return "", errors.New("target is required")
+ return nil, errors.New("target is required")
}
localTarget, err := u.localTarget(target)
if err != nil {
- return "", fmt.Errorf("failed to load local path for target %s: %w", target, err)
+ return nil, fmt.Errorf("failed to load local path for target %s: %w", target, err)
}
repoPath, err := u.repoPath(target)
if err != nil {
- return "", fmt.Errorf("failed to load repository path for target %s: %w", target, err)
+ return nil, fmt.Errorf("failed to load repository path for target %s: %w", target, err)
}
- switch stat, err := os.Stat(localTarget.path); {
+ switch stat, err := os.Stat(localTarget.Path); {
case err == nil:
if !stat.Mode().IsRegular() {
- return "", fmt.Errorf("expected %s to be regular file", localTarget.path)
+ return nil, fmt.Errorf("expected %s to be regular file", localTarget.Path)
}
meta, err := u.Lookup(target)
if err != nil {
- return "", err
+ return nil, err
}
- if err := checkFileHash(meta, localTarget.path); err != nil {
+ if err := checkFileHash(meta, localTarget.Path); err != nil {
log.Debug().Str("info", err.Error()).Msg("change detected")
- if err := u.download(target, repoPath, localTarget.path); err != nil {
- return "", fmt.Errorf("download %q: %w", repoPath, err)
+ if err := u.download(target, repoPath, localTarget.Path); err != nil {
+ return nil, fmt.Errorf("download %q: %w", repoPath, err)
}
- if strings.HasSuffix(localTarget.path, ".tar.gz") {
- if err := os.RemoveAll(localTarget.dirPath); err != nil {
- return "", fmt.Errorf("failed to remove old extracted dir: %q: %w", localTarget.dirPath, err)
+ if strings.HasSuffix(localTarget.Path, ".tar.gz") {
+ if err := os.RemoveAll(localTarget.DirPath); err != nil {
+ return nil, fmt.Errorf("failed to remove old extracted dir: %q: %w", localTarget.DirPath, err)
}
}
} else {
- log.Debug().Str("path", localTarget.path).Str("target", target).Msg("found expected target locally")
+ log.Debug().Str("path", localTarget.Path).Str("target", target).Msg("found expected target locally")
}
case errors.Is(err, os.ErrNotExist):
log.Debug().Err(err).Msg("stat file")
- if err := u.download(target, repoPath, localTarget.path); err != nil {
- return "", fmt.Errorf("download %q: %w", repoPath, err)
+ if err := u.download(target, repoPath, localTarget.Path); err != nil {
+ return nil, fmt.Errorf("download %q: %w", repoPath, err)
}
default:
- return "", fmt.Errorf("stat %q: %w", localTarget.path, err)
+ return nil, fmt.Errorf("stat %q: %w", localTarget.Path, err)
}
- if strings.HasSuffix(localTarget.path, ".tar.gz") {
- switch s, err := os.Stat(localTarget.execPath); {
+ if strings.HasSuffix(localTarget.Path, ".tar.gz") {
+ s, err := os.Stat(localTarget.ExecPath)
+ switch {
case err == nil:
- if s.IsDir() {
- return "", fmt.Errorf("expected executable %q: %w", localTarget.execPath, err)
- }
+ // OK
case errors.Is(err, os.ErrNotExist):
- if err := extractTarGz(localTarget.path); err != nil {
- return "", fmt.Errorf("extract %q: %w", localTarget.path, err)
+ if err := extractTarGz(localTarget.Path); err != nil {
+ return nil, fmt.Errorf("extract %q: %w", localTarget.Path, err)
}
- s, err := os.Stat(localTarget.execPath)
+ s, err = os.Stat(localTarget.ExecPath)
if err != nil {
- return "", fmt.Errorf("stat %q: %w", localTarget.execPath, err)
- }
- if s.IsDir() {
- return "", fmt.Errorf("expected executable %q: %w", localTarget.execPath, err)
+ return nil, fmt.Errorf("stat %q: %w", localTarget.ExecPath, err)
}
default:
- return "", fmt.Errorf("stat %q: %w", localTarget.execPath, err)
+ return nil, fmt.Errorf("stat %q: %w", localTarget.ExecPath, err)
+ }
+ if !s.Mode().IsRegular() {
+ return nil, fmt.Errorf("expected a regular file: %q", localTarget.ExecPath)
}
}
- return localTarget.execPath, nil
+ return localTarget, nil
}
func writeDevWarningBanner(w io.Writer) {
@@ -431,7 +453,7 @@ func (u *Updater) checkExec(target, tmpPath string) error {
if err != nil {
return err
}
- platformGOOS, err := goosFromPlatform(localTarget.info.Platform)
+ platformGOOS, err := goosFromPlatform(localTarget.Info.Platform)
if err != nil {
return err
}
@@ -446,9 +468,9 @@ func (u *Updater) checkExec(target, tmpPath string) error {
if err := extractTarGz(tmpPath); err != nil {
return fmt.Errorf("extract %q: %w", tmpPath, err)
}
- tmpDirPath := filepath.Join(filepath.Dir(tmpPath), localTarget.info.ExtractedExecSubPath[0])
+ tmpDirPath := filepath.Join(filepath.Dir(tmpPath), localTarget.Info.ExtractedExecSubPath[0])
defer os.RemoveAll(tmpDirPath)
- tmpPath = filepath.Join(append([]string{filepath.Dir(tmpPath)}, localTarget.info.ExtractedExecSubPath...)...)
+ tmpPath = filepath.Join(append([]string{filepath.Dir(tmpPath)}, localTarget.Info.ExtractedExecSubPath...)...)
}
// Note that this would fail for any binary that returns nonzero for --help.
diff --git a/orbit/tools/cleanup/cleanup_macos.sh b/orbit/tools/cleanup/cleanup_macos.sh
index a410b054a3..51e1da2a54 100755
--- a/orbit/tools/cleanup/cleanup_macos.sh
+++ b/orbit/tools/cleanup/cleanup_macos.sh
@@ -3,4 +3,5 @@
sudo launchctl stop com.fleetdm.orbit
sudo launchctl unload /Library/LaunchDaemons/com.fleetdm.orbit.plist
+sudo pkill fleet-desktop || true
sudo rm -rf /Library/LaunchDaemons/com.fleetdm.orbit.plist /var/lib/orbit/ /usr/local/bin/orbit /var/log/orbit
diff --git a/pkg/open/open.go b/pkg/open/open.go
new file mode 100644
index 0000000000..5cd772ace3
--- /dev/null
+++ b/pkg/open/open.go
@@ -0,0 +1,84 @@
+package open
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "runtime"
+)
+
+// Browser opens the default browser at the given url and returns.
+func Browser(url string) error {
+ var cmd *exec.Cmd
+ switch runtime.GOOS {
+ case "windows":
+ cmd = exec.Command("cmd", "/c", "start", url)
+ case "darwin":
+ cmd = exec.Command("open", url)
+ default: // xdg-open is available on most Linux-y systems
+ cmd = exec.Command("xdg-open", url)
+ }
+
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("open in browser: %w", err)
+ }
+ return nil
+}
+
+type appOpts struct {
+ env [][2]string
+ stderrPath string
+}
+
+// AppOption are options to use when opening the application with App.
+type AppOption func(*appOpts)
+
+// AppWithEnv sets the environment for opening an application.
+func AppWithEnv(name, value string) AppOption {
+ return func(a *appOpts) {
+ a.env = append(a.env, [2]string{name, value})
+ }
+}
+
+// AppWithStderr sets the stderr destination for the application.
+func AppWithStderr(path string) AppOption {
+ return func(a *appOpts) {
+ a.stderrPath = path
+ }
+}
+
+// App opens an application at path with the default application.
+func App(path string, opts ...AppOption) error {
+ var o appOpts
+ for _, fn := range opts {
+ fn(&o)
+ }
+ info, err := os.Stat(path)
+ if err != nil {
+ return fmt.Errorf("stat path %q: %w", path, err)
+ }
+
+ switch runtime.GOOS {
+ case "darwin":
+ if !info.IsDir() {
+ return fmt.Errorf("path is not an .app directory: %s", path)
+ }
+ var arg []string
+ if o.stderrPath != "" {
+ arg = append(arg, "--stderr", o.stderrPath)
+ }
+ for _, nv := range o.env {
+ arg = append(arg, "--env", fmt.Sprintf("%s=%s", nv[0], nv[1]))
+ }
+ arg = append(arg, path)
+ cmd := exec.Command("open", arg...)
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = os.Stdout
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("open path: %w", err)
+ }
+ return nil
+ default:
+ return fmt.Errorf("platform unsupported: %s", runtime.GOOS)
+ }
+}
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 2aceee7f5c..272abf391e 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -335,6 +335,7 @@ func (ds *Datastore) DeleteHost(ctx context.Context, hid uint) error {
"policy_membership",
"host_mdm",
"host_munki_info",
+ "host_device_auth",
}
for _, table := range hostRefs {
diff --git a/tools/desktop/desktop.go b/tools/desktop/desktop.go
new file mode 100644
index 0000000000..ffaa84f03b
--- /dev/null
+++ b/tools/desktop/desktop.go
@@ -0,0 +1,234 @@
+package main
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+
+ "github.com/fleetdm/fleet/v4/orbit/pkg/constant"
+ "github.com/fleetdm/fleet/v4/pkg/secure"
+ "github.com/kolide/kit/version"
+ "github.com/rs/zerolog"
+ zlog "github.com/rs/zerolog/log"
+ "github.com/urfave/cli/v2"
+)
+
+func main() {
+ app := createApp(os.Stdin, os.Stdout, exitErrHandler)
+ app.Run(os.Args)
+}
+
+// exitErrHandler implements cli.ExitErrHandlerFunc. If there is an error, prints it to stderr and exits with status 1.
+func exitErrHandler(c *cli.Context, err error) {
+ if err == nil {
+ return
+ }
+ fmt.Fprintf(c.App.ErrWriter, "Error: %+v\n", err)
+ cli.OsExiter(1)
+}
+
+func createApp(reader io.Reader, writer io.Writer, exitErrHandler cli.ExitErrHandlerFunc) *cli.App {
+ app := cli.NewApp()
+ app.Name = "desktop"
+ app.Usage = "Tool to generate the Fleet Desktop application"
+ app.ExitErrHandler = exitErrHandler
+ cli.VersionPrinter = func(c *cli.Context) {
+ version.PrintFull()
+ }
+ app.Reader = reader
+ app.Writer = writer
+ app.ErrWriter = writer
+ app.Flags = []cli.Flag{
+ &cli.StringFlag{
+ Name: "version",
+ Usage: "Version of the Fleet Desktop application",
+ EnvVars: []string{"FLEET_DESKTOP_VERSION"},
+ },
+ &cli.BoolFlag{
+ Name: "verbose",
+ Usage: "Log detailed information when building the application",
+ EnvVars: []string{"FLEET_DESKTOP_VERBOSE"},
+ },
+ }
+
+ app.Commands = []*cli.Command{
+ macos(),
+ }
+ return app
+}
+
+func macos() *cli.Command {
+ return &cli.Command{
+ Name: "macos",
+ Usage: "Creates the Fleet Desktop Application for macOS",
+ Description: "Builds and signs the Fleet Desktop .app bundle for macOS",
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: "authority",
+ Usage: "Authority to use on the codesign invocation (if not set, app is not signed)",
+ EnvVars: []string{"FLEET_DESKTOP_APPLE_AUTHORITY"},
+ },
+ },
+ Action: func(c *cli.Context) error {
+ if !c.Bool("verbose") {
+ zlog.Logger = zerolog.Nop()
+ }
+ return createMacOSApp(c.String("version"), c.String("authority"))
+ },
+ }
+}
+
+func createMacOSApp(version, authority string) error {
+ const (
+ appDir = "Fleet Desktop.app"
+ bundleIdentifier = "com.fleetdm.desktop"
+ // infoPList is the Info.plist file to use for the macOS .app bundle.
+ //
+ // - NSHighResolutionCapable=true: avoid having a blurry icon and text.
+ // - LSUIElement=1: avoid showing the app on the Dock.
+ infoPList = `
+
+
+
+ CFBundleExecutable
+ fleet-desktop
+ CFBundleIdentifier
+ %s
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ fleet-desktop
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ %s
+ CFBundleVersion
+ %s
+ NSHighResolutionCapable
+ True
+ LSUIElement
+ 1
+
+
+`
+ )
+
+ if runtime.GOOS != "darwin" {
+ return errors.New(`the "Fleet Desktop" macOS app can only be created from macOS`)
+ }
+
+ defer os.RemoveAll(appDir)
+
+ contentsDir := filepath.Join(appDir, "Contents")
+ macOSDir := filepath.Join(contentsDir, "MacOS")
+ if err := secure.MkdirAll(macOSDir, constant.DefaultDirMode); err != nil {
+ return fmt.Errorf("create directory %q: %w", macOSDir, err)
+ }
+
+ infoFile := filepath.Join(contentsDir, "Info.plist")
+ infoPListContents := fmt.Sprintf(infoPList, bundleIdentifier, version, version)
+ if err := ioutil.WriteFile(infoFile, []byte(infoPListContents), 0o644); err != nil {
+ return fmt.Errorf("create Info.plist file %q: %w", infoFile, err)
+ }
+
+ /* #nosec G204 -- arguments are actually well defined */
+ buildExec := exec.Command("go", "build", "-o", filepath.Join(macOSDir, constant.DesktopAppExecName), "./"+filepath.Join("orbit", "cmd", "desktop"))
+ buildExec.Env = append(os.Environ(), "CGO_ENABLED=1")
+ buildExec.Stderr = os.Stderr
+ buildExec.Stdout = os.Stdout
+
+ zlog.Info().Str("command", buildExec.String()).Msg("Build fleet-desktop executable")
+
+ if err := buildExec.Run(); err != nil {
+ return fmt.Errorf("compile application: %w", err)
+ }
+
+ if authority != "" {
+ codeSign := exec.Command("codesign", "-s", authority, "-i", bundleIdentifier, "-f", "-v", "--timestamp", "--options", "runtime", appDir)
+
+ zlog.Info().Str("command", codeSign.String()).Msg("Sign Fleet Desktop.app")
+
+ codeSign.Stderr = os.Stderr
+ codeSign.Stdout = os.Stdout
+ if err := codeSign.Run(); err != nil {
+ return fmt.Errorf("sign application: %w", err)
+ }
+ }
+
+ const tarGzName = "desktop.app.tar.gz"
+ if err := compressDir(tarGzName, appDir); err != nil {
+ return fmt.Errorf("compress app: %w", err)
+ }
+ fmt.Printf("Generated %s successfully.\n", tarGzName)
+
+ return nil
+}
+
+func compressDir(outPath, dirPath string) error {
+ out, err := secure.OpenFile(outPath, os.O_CREATE|os.O_WRONLY, 0o600)
+ if err != nil {
+ return fmt.Errorf("open archive: %w", err)
+ }
+ defer out.Close()
+
+ gw := gzip.NewWriter(out)
+ defer gw.Close()
+
+ tw := tar.NewWriter(gw)
+ defer tw.Close()
+
+ if err := filepath.Walk(dirPath, func(file string, fi os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ header, err := tar.FileInfoHeader(fi, file)
+ if err != nil {
+ return err
+ }
+
+ // From https://golang.org/src/archive/tar/common.go?#L626
+ //
+ // "Since fs.FileInfo's Name method only returns the base name of
+ // the file it describes, it may be necessary to modify Header.Name
+ // to provide the full path name of the file."
+ header.Name = filepath.ToSlash(file)
+
+ if err := tw.WriteHeader(header); err != nil {
+ return err
+ }
+ if !fi.IsDir() {
+ f, err := os.Open(file)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ if _, err := io.Copy(tw, f); err != nil {
+ return err
+ }
+ }
+ return nil
+ }); err != nil {
+ return fmt.Errorf("walk directory: %w", err)
+ }
+
+ if err := tw.Close(); err != nil {
+ return fmt.Errorf("close tar: %w", err)
+ }
+ if err := gw.Close(); err != nil {
+ return fmt.Errorf("close gzip: %w", err)
+ }
+ if err := out.Close(); err != nil {
+ return fmt.Errorf("close file: %w", err)
+ }
+
+ return nil
+}
diff --git a/tools/tuf/README.md b/tools/tuf/README.md
index 98f83cb20f..23c050c701 100644
--- a/tools/tuf/README.md
+++ b/tools/tuf/README.md
@@ -19,7 +19,7 @@ To add new updates (osqueryd or orbit), use `push_target.sh`.
E.g. to add a new version of `orbit` for Windows:
```sh
# Compile a new version of Orbit:
-GOOS=windows go build -o orbit-windows.exe ./orbit/cmd/orbit
+GOOS=windows GOARCH=amd64 go build -o orbit-windows.exe ./orbit/cmd/orbit
# Push the compiled Orbit as a new version:
./tools/tuf/push_target.sh windows orbit orbit-windows.exe 43
diff --git a/tools/tuf/init_tuf.sh b/tools/tuf/init_tuf.sh
index 54f5596337..2371cd4a22 100755
--- a/tools/tuf/init_tuf.sh
+++ b/tools/tuf/init_tuf.sh
@@ -60,8 +60,22 @@ function create_repository() {
--platform $system \
--name orbit \
--version 42.0.0 -t 42.0 -t 42 -t stable
-
rm $orbit_target
+
+ # Add Fleet Desktop application on macos (if enabled).
+ if [[ $system == "macos" && -n "$FLEET_DESKTOP" ]]; then
+ FLEET_DESKTOP_VERBOSE=1 \
+ FLEET_DESKTOP_VERSION=42.0.0 \
+ make desktop-app-tar-gz
+ ./build/fleetctl updates add \
+ --path $TUF_PATH \
+ --target desktop.app.tar.gz \
+ --platform macos \
+ --name desktop \
+ --version 42.0.0 -t 42.0 -t 42 -t stable
+ rm desktop.app.tar.gz
+ fi
+
done
# Generate and add osqueryd .app bundle for macos-app.
@@ -106,6 +120,7 @@ if [ -n "$GENERATE_PKGS" ]; then
echo "Generating pkg..."
./build/fleetctl package \
--type=pkg \
+ ${FLEET_DESKTOP:+--fleet-desktop} \
--fleet-url=https://$PKG_HOSTNAME:8080 \
--enroll-secret=$ENROLL_SECRET \
--insecure \
@@ -113,6 +128,7 @@ if [ -n "$GENERATE_PKGS" ]; then
--update-roots="$root_keys" \
--update-url=http://$PKG_HOSTNAME:8081
+ echo "Generating deb..."
./build/fleetctl package \
--type=deb \
--fleet-url=https://$DEB_HOSTNAME:8080 \
@@ -122,6 +138,7 @@ if [ -n "$GENERATE_PKGS" ]; then
--update-roots="$root_keys" \
--update-url=http://$DEB_HOSTNAME:8081
+ echo "Generating rpm..."
./build/fleetctl package \
--type=rpm \
--fleet-url=https://$RPM_HOSTNAME:8080 \
@@ -131,6 +148,7 @@ if [ -n "$GENERATE_PKGS" ]; then
--update-roots="$root_keys" \
--update-url=http://$RPM_HOSTNAME:8081
+ echo "Generating msi..."
./build/fleetctl package \
--type=msi \
--fleet-url=https://$MSI_HOSTNAME:8080 \
@@ -143,4 +161,4 @@ if [ -n "$GENERATE_PKGS" ]; then
echo "Packages generated"
fi
-wait $SERVER_PID
\ No newline at end of file
+wait $SERVER_PID