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