diff --git a/cmd/package/package.go b/cmd/package/package.go index b1635e1a5a..355a0e274f 100644 --- a/cmd/package/package.go +++ b/cmd/package/package.go @@ -23,6 +23,11 @@ func main() { app.Usage = "A powered-up, (near) drop-in replacement for osquery" app.Commands = []*cli.Command{} app.Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Usage: "Type of package to build", + Required: true, + }, &cli.StringFlag{ Name: "enroll-secret", Usage: "Enroll secret for authenticating to Fleet server", @@ -33,6 +38,18 @@ func main() { Usage: "URL (host:port) of Fleet server", Destination: &opt.FleetURL, }, + &cli.StringFlag{ + Name: "identifier", + Usage: "Identifier for package product", + Value: "com.fleetdm.orbit", + Destination: &opt.Identifier, + }, + &cli.StringFlag{ + Name: "version", + Usage: "Version for package product", + Value: "0.0.1", + Destination: &opt.Version, + }, &cli.BoolFlag{ Name: "insecure", Usage: "Disable TLS certificate verification", @@ -58,7 +75,14 @@ func main() { } } - return packaging.BuildDeb(opt) + switch c.String("type") { + case "pkg": + return packaging.BuildPkg(opt) + case "deb": + return packaging.BuildDeb(opt) + default: + return errors.New("type must be one of ('pkg', 'deb')") + } } if err := app.Run(os.Args); err != nil { diff --git a/pkg/packaging/deb.go b/pkg/packaging/deb.go index 88a4d25ba3..49e43466dc 100644 --- a/pkg/packaging/deb.go +++ b/pkg/packaging/deb.go @@ -27,9 +27,9 @@ func BuildDeb(opt Options) error { defer os.RemoveAll(tmpDir) log.Debug().Str("path", tmpDir).Msg("created temp dir") - filesystemRoot := filepath.Join(tmpDir, "filesystem") + filesystemRoot := filepath.Join(tmpDir, "root") if err := os.MkdirAll(filesystemRoot, constant.DefaultDirMode); err != nil { - return errors.Wrap(err, "create filesystem dir") + return errors.Wrap(err, "create root dir") } orbitRoot := filepath.Join(filesystemRoot, "var", "lib", "fleet", "orbit") if err := os.MkdirAll(orbitRoot, constant.DefaultDirMode); err != nil { @@ -113,7 +113,7 @@ func BuildDeb(opt Options) error { info := &nfpm.Info{ Name: "orbit-osquery", - Version: "0.0.1", + Version: opt.Version, Description: "Osquery launcher and autoupdater", Arch: "amd64", Maintainer: "FleetDM Engineers ", diff --git a/pkg/packaging/macos.go b/pkg/packaging/macos.go new file mode 100644 index 0000000000..294e9e3ccc --- /dev/null +++ b/pkg/packaging/macos.go @@ -0,0 +1,226 @@ +package packaging + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/fleetdm/orbit/pkg/constant" + "github.com/fleetdm/orbit/pkg/update" + "github.com/fleetdm/orbit/pkg/update/filestore" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// See helful docs in http://bomutils.dyndns.org/tutorial.html + +// BuildPkg builds a macOS .pkg. So far this is tested only on macOS but in theory it works with bomutils on +// Linux. +func BuildPkg(opt Options) error { + // Initialize directories + + tmpDir, err := ioutil.TempDir("", "orbit-package") + if err != nil { + return errors.Wrap(err, "failed to create temp dir") + } + // TODO reenable + //defer os.RemoveAll(tmpDir) + log.Debug().Str("path", tmpDir).Msg("created temp dir") + + filesystemRoot := filepath.Join(tmpDir, "root") + if err := os.MkdirAll(filesystemRoot, constant.DefaultDirMode); err != nil { + return errors.Wrap(err, "create root dir") + } + // if err := os.MkdirAll( + // filepath.Join(filesystemRoot, "Resources", "en.lproj"), + // constant.DefaultDirMode, + // ); err != nil { + // return errors.Wrap(err, "create resources dir") + // } + orbitRoot := filepath.Join(filesystemRoot, "var", "lib", "fleet", "orbit") + if err := os.MkdirAll(orbitRoot, constant.DefaultDirMode); err != nil { + return errors.Wrap(err, "create orbit dir") + } + + // Write files + + if err := writePackageInfo(opt, tmpDir); err != nil { + return errors.Wrap(err, "write PackageInfo") + } + if err := writeDistribution(opt, tmpDir); err != nil { + return errors.Wrap(err, "write Distribution") + } + + // Initialize autoupdate metadata + + localStore, err := filestore.New(filepath.Join(orbitRoot, "tuf-metadata.json")) + if err != nil { + return errors.Wrap(err, "failed to create local metadata store") + } + updateOpt := update.DefaultOptions + updateOpt.RootDirectory = orbitRoot + updateOpt.ServerURL = "https://tuf.fleetctl.com" + updateOpt.LocalStore = localStore + updateOpt.Platform = "macos" + + updater, err := update.New(updateOpt) + if err != nil { + return errors.Wrap(err, "failed to init updater") + } + if err := updater.UpdateMetadata(); err != nil { + return errors.Wrap(err, "failed to update metadata") + } + osquerydPath, err := updater.Get("osqueryd", "stable") + if err != nil { + return errors.Wrap(err, "failed to get osqueryd") + } + log.Debug().Str("path", osquerydPath).Msg("got osqueryd") + + // Build package + if err := xarBom(opt, tmpDir); err != nil { + return errors.Wrap(err, "build pkg") + } + + filename := fmt.Sprintf("orbit-osquery_%s_amd64.pkg", opt.Version) + if err := os.Rename(filepath.Join(tmpDir, "orbit.pkg"), filename); err != nil { + return errors.Wrap(err, "rename pkg") + } + log.Info().Str("path", filename).Msg("wrote pkg package") + + return nil +} + +func writePackageInfo(opt Options, rootPath string) error { + path := filepath.Join(rootPath, "flat", "base.pkg", "PackageInfo") + if err := os.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil { + return errors.Wrap(err, "mkdir") + } + + var contents bytes.Buffer + if err := macosPackageInfoTemplate.Execute(&contents, opt); err != nil { + return errors.Wrap(err, "execute template") + } + + if err := ioutil.WriteFile(path, contents.Bytes(), constant.DefaultFileMode); err != nil { + return errors.Wrap(err, "write file") + } + + return nil +} + +func writeDistribution(opt Options, rootPath string) error { + path := filepath.Join(rootPath, "flat", "Distribution") + if err := os.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil { + return errors.Wrap(err, "mkdir") + } + + var contents bytes.Buffer + if err := macosDistributionTemplate.Execute(&contents, opt); err != nil { + return errors.Wrap(err, "execute template") + } + + if err := ioutil.WriteFile(path, contents.Bytes(), constant.DefaultFileMode); err != nil { + return errors.Wrap(err, "write file") + } + + return nil +} + +// xarBom creates the actual .pkg format. It's a xar archive with a BOM (Bill of +// materials?). See http://bomutils.dyndns.org/tutorial.html. +func xarBom(opt Options, rootPath string) error { + // Adapted from BSD licensed + // https://github.com/go-flutter-desktop/hover/blob/v0.46.2/cmd/packaging/darwin-pkg.go + + // Copy payload + payload, err := os.OpenFile(filepath.Join(rootPath, "flat", "base.pkg", "Payload"), os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return errors.Wrap(err, "open payload") + } + + cmdFind := exec.Command("find", ".") + cmdFind.Dir = filepath.Join(rootPath, "root") + cmdCpio := exec.Command("cpio", "-o", "--format", "odc", "-R", "0:80") + cmdCpio.Dir = filepath.Join(rootPath, "root") + cmdGzip := exec.Command("gzip", "-c") + + // Pipes like this: find | cpio | gzip > Payload + cmdCpio.Stdin, err = cmdFind.StdoutPipe() + if err != nil { + return errors.Wrap(err, "pipe cpio") + } + cmdGzip.Stdin, err = cmdCpio.StdoutPipe() + if err != nil { + return errors.Wrap(err, "pipe gzip") + } + cmdGzip.Stdout = payload + + err = cmdGzip.Start() + if err != nil { + return errors.Wrap(err, "start gzip") + } + err = cmdCpio.Start() + if err != nil { + return errors.Wrap(err, "start cpio") + } + err = cmdFind.Run() + if err != nil { + return errors.Wrap(err, "run find") + } + err = cmdCpio.Wait() + if err != nil { + return errors.Wrap(err, "wait cpio") + } + err = cmdGzip.Wait() + if err != nil { + return errors.Wrap(err, "wait gzip") + } + err = payload.Close() + if err != nil { + return errors.Wrap(err, "close payload") + } + + // Make bom + var cmdMkbom *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmdMkbom = exec.Command("mkbom", filepath.Join(rootPath, "root"), filepath.Join("flat", "base.pkg", "Bom")) + case "linux": + cmdMkbom = exec.Command("mkbom", "-u", "0", "-g", "80", filepath.Join(rootPath, "flat", "root"), filepath.Join("flat", "base.pkg", "Bom")) + } + cmdMkbom.Dir = rootPath + cmdMkbom.Stdout = os.Stdout + cmdMkbom.Stderr = os.Stderr + if err := cmdMkbom.Run(); err != nil { + return errors.Wrap(err, "mkbom") + } + + // List files for xar + var files []string + if err := filepath.Walk(filepath.Join(rootPath, "flat"), func(path string, info os.FileInfo, err error) error { + relativePath, err := filepath.Rel(filepath.Join(rootPath, "flat"), path) + if err != nil { + return err + } + files = append(files, relativePath) + return nil + }); err != nil { + return errors.Wrap(err, "iterate files") + } + + // Make xar + cmdXar := exec.Command("xar", append([]string{"--compression", "none", "-cf", filepath.Join("..", "orbit.pkg")}, files...)...) + cmdXar.Dir = filepath.Join(rootPath, "flat") + cmdXar.Stdout = os.Stdout + cmdXar.Stderr = os.Stderr + + if err := cmdXar.Run(); err != nil { + return errors.Wrap(err, "run xar") + } + + return nil +} diff --git a/pkg/packaging/macos_templates.go b/pkg/packaging/macos_templates.go new file mode 100644 index 0000000000..415749e115 --- /dev/null +++ b/pkg/packaging/macos_templates.go @@ -0,0 +1,28 @@ +package packaging + +import "text/template" + +// Best reference I could find: +// http://s.sudre.free.fr/Stuff/Ivanhoe/FLAT.html +var macosPackageInfoTemplate = template.Must(template.New("").Option("missingkey=error").Parse( + ` + + + +`)) + +// Reference: +// https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html +var macosDistributionTemplate = template.Must(template.New("").Option("missingkey=error").Parse( + ` + + Orbit + + + + + + + #base.pkg + +`)) diff --git a/pkg/packaging/packaging.go b/pkg/packaging/packaging.go index 4086f971f2..f482085d59 100644 --- a/pkg/packaging/packaging.go +++ b/pkg/packaging/packaging.go @@ -8,6 +8,10 @@ type Options struct { // EnrollSecret is the enroll secret used to authenticate to the Fleet // server. EnrollSecret string + // Version is the version number for this package. + Version string + // Identifier is the identifier (eg. com.fleetdm.orbit) for the package product. + Identifier string // StartService is a boolean indicating whether to start a system-specific // background service. StartService bool