From 7767da94e87c7559b03eeb41a04218fd46f34468 Mon Sep 17 00:00:00 2001 From: Zach Wasserman Date: Fri, 16 Apr 2021 17:08:02 -0700 Subject: [PATCH] Initial Windows support (#10) This commit adds support for Orbit's update and osquery execution capabilities on Windows. Still to come is the packaging tooling for building MSIs on Windows. --- cmd/orbit/orbit.go | 18 ++++- cmd/package/package.go | 4 +- pkg/database/database.go | 22 +++++- pkg/packaging/macos.go | 14 ---- pkg/packaging/macos_templates.go | 2 +- pkg/packaging/packaging.go | 16 ++++ pkg/packaging/windows.go | 104 ++++++++++++++++++++++++++ pkg/packaging/windows_templates.go | 78 ++++++++++++++++++++ pkg/packaging/wix/transform.go | 113 +++++++++++++++++++++++++++++ pkg/packaging/wix/wix.go | 88 ++++++++++++++++++++++ 10 files changed, 438 insertions(+), 21 deletions(-) create mode 100644 pkg/packaging/windows.go create mode 100644 pkg/packaging/windows_templates.go create mode 100644 pkg/packaging/wix/transform.go create mode 100644 pkg/packaging/wix/wix.go diff --git a/cmd/orbit/orbit.go b/cmd/orbit/orbit.go index 649cde203a..0106eecf73 100644 --- a/cmd/orbit/orbit.go +++ b/cmd/orbit/orbit.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/dgraph-io/badger/v2" "github.com/fleetdm/orbit/pkg/certificate" "github.com/fleetdm/orbit/pkg/constant" "github.com/fleetdm/orbit/pkg/database" @@ -26,7 +27,6 @@ import ( ) const ( - certPath = "/tmp/fleet.pem" defaultRootDir = "/var/lib/orbit" ) @@ -143,9 +143,18 @@ func main() { return errors.Wrap(err, "initialize root dir") } - db, err := database.Open(filepath.Join(c.String("root-dir"), "orbit.db")) + dbPath := filepath.Join(c.String("root-dir"), "orbit.db") + db, err := database.Open(dbPath) if err != nil { - return err + if errors.Is(err, badger.ErrTruncateNeeded) { + db, err = database.OpenTruncate(dbPath) + if err != nil { + return err + } + log.Warn().Msg("Open badger required truncate. Possible data loss.") + } else { + return err + } } defer func() { if err := db.Close(); err != nil { @@ -229,7 +238,8 @@ func main() { ) // Write cert that proxy uses - err = ioutil.WriteFile(certPath, []byte(insecure.ServerCert), os.ModePerm) + certPath := filepath.Join(opt.RootDirectory, "insecure-cert.pem") + err = ioutil.WriteFile(filepath.Join(opt.RootDirectory, "insecure-cert.pem"), []byte(insecure.ServerCert), os.ModePerm) if err != nil { return errors.Wrap(err, "write server cert") } diff --git a/cmd/package/package.go b/cmd/package/package.go index b2521f2d78..8060c42a29 100644 --- a/cmd/package/package.go +++ b/cmd/package/package.go @@ -129,8 +129,10 @@ func main() { return packaging.BuildDeb(opt) case "rpm": return packaging.BuildRPM(opt) + case "msi": + return packaging.BuildMSI(opt) default: - return errors.New("type must be one of ('pkg', 'deb', 'rpm')") + return errors.New("type must be one of ('pkg', 'deb', 'rpm', 'msi')") } } diff --git a/pkg/database/database.go b/pkg/database/database.go index 100377348d..ac3481b54b 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -22,7 +22,7 @@ type BadgerDB struct { closeChan chan struct{} } -// Open opens (initializing if necessary) a new Badger database at the specified +// Open opens (initializing if necessary) a Badger database at the specified // path. Users must close the DB with Close(). func Open(path string) (*BadgerDB, error) { // DefaultOptions sets synchronous writes to true (maximum data integrity). @@ -38,6 +38,26 @@ func Open(path string) (*BadgerDB, error) { return b, nil } +// OpenTruncate opens (initializing and/or truncating if necessary) a Badger +// database at the specified path. Users must close the DB with Close(). +// +// Prefer Open in the general case, but after a bad shutdown it may be necessary +// to call OpenTruncate. This may cause data loss. Detect this situation by +// looking for badger.ErrTruncateNeeded. +func OpenTruncate(path string) (*BadgerDB, error) { + // DefaultOptions sets synchronous writes to true (maximum data integrity). + // TODO implement logging? + db, err := badger.Open(badger.DefaultOptions(path).WithLogger(nil).WithTruncate(true)) + if err != nil { + return nil, errors.Wrapf(err, "open badger with truncate %s", path) + } + + b := &BadgerDB{DB: db} + b.startBackgroundCompaction() + + return b, nil +} + // startBackgroundCompaction starts a background loop that will call the // compaction method on the database. Badger does not do this automatically, so // we need to be sure to do so here (or elsewhere). diff --git a/pkg/packaging/macos.go b/pkg/packaging/macos.go index 96eb12a5b7..ea8871a7b9 100644 --- a/pkg/packaging/macos.go +++ b/pkg/packaging/macos.go @@ -156,20 +156,6 @@ func writeScripts(opt Options, rootPath string) error { return nil } -func writeSecret(opt Options, orbitRoot string) error { - // Enroll secret - path := filepath.Join(orbitRoot, "secret") - if err := os.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil { - return errors.Wrap(err, "mkdir") - } - - if err := ioutil.WriteFile(path, []byte(opt.EnrollSecret), 0600); err != nil { - return errors.Wrap(err, "write file") - } - - return nil -} - func writeLaunchd(opt Options, rootPath string) error { // launchd is the service mechanism on macOS path := filepath.Join(rootPath, "Library", "LaunchDaemons", "com.fleetdm.orbit.plist") diff --git a/pkg/packaging/macos_templates.go b/pkg/packaging/macos_templates.go index a0420cb171..e315f297c8 100644 --- a/pkg/packaging/macos_templates.go +++ b/pkg/packaging/macos_templates.go @@ -71,7 +71,7 @@ var macosLaunchdTemplate = template.Must(template.New("").Option("missingkey=err {{ if .Insecure }}ORBIT_INSECUREtrue{{ end }} {{ if .FleetURL }}ORBIT_FLEET_URL{{ .FleetURL }}{{ end }} {{ if .FleetCertificate }}ORBIT_FLEET_CERTIFICATE/var/lib/orbit/fleet.pem{{ end }} - {{ if .EnrollSecret }}ORBIT_ENROLL_SECRET_PATH/var/lib/orbit/secret{{ end }} + {{ if .EnrollSecret }}ORBIT_ENROLL_SECRET_PATH/var/lib/orbit/secret.txt{{ end }} {{ if .Debug }}ORBIT_DEBUGtrue{{ end }} KeepAlive diff --git a/pkg/packaging/packaging.go b/pkg/packaging/packaging.go index a12ef7954e..f3f2c518ce 100644 --- a/pkg/packaging/packaging.go +++ b/pkg/packaging/packaging.go @@ -3,9 +3,11 @@ package packaging import ( "io" + "io/ioutil" "os" "path/filepath" + "github.com/fleetdm/orbit/pkg/constant" "github.com/fleetdm/orbit/pkg/update" "github.com/fleetdm/orbit/pkg/update/filestore" "github.com/pkg/errors" @@ -98,3 +100,17 @@ func initializeUpdates(updateOpt update.Options) error { return nil } + +// writeSecret writes the enroll secret to a text file +func writeSecret(opt Options, orbitRoot string) error { + path := filepath.Join(orbitRoot, "secret.txt") + if err := os.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil { + return errors.Wrap(err, "mkdir") + } + + if err := ioutil.WriteFile(path, []byte(opt.EnrollSecret), 0600); err != nil { + return errors.Wrap(err, "write file") + } + + return nil +} diff --git a/pkg/packaging/windows.go b/pkg/packaging/windows.go new file mode 100644 index 0000000000..cad1f53ee8 --- /dev/null +++ b/pkg/packaging/windows.go @@ -0,0 +1,104 @@ +package packaging + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/fleetdm/orbit/pkg/constant" + "github.com/fleetdm/orbit/pkg/packaging/wix" + "github.com/fleetdm/orbit/pkg/update" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// BuildMSI builds a Windows .msi. +func BuildMSI(opt Options) error { + // Initialize directories + tmpDir, err := ioutil.TempDir("", "orbit-package") + if err != nil { + return errors.Wrap(err, "failed to create temp dir") + } + 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") + } + orbitRoot := filesystemRoot + if err := os.MkdirAll(orbitRoot, constant.DefaultDirMode); err != nil { + return errors.Wrap(err, "create orbit dir") + } + + // Initialize autoupdate metadata + + updateOpt := update.DefaultOptions + updateOpt.Platform = "windows" + updateOpt.RootDirectory = orbitRoot + updateOpt.OrbitChannel = opt.OrbitChannel + updateOpt.OsquerydChannel = opt.OsquerydChannel + updateOpt.ServerURL = opt.UpdateURL + if opt.UpdateRoots != "" { + updateOpt.RootKeys = opt.UpdateRoots + } + + if err := initializeUpdates(updateOpt); err != nil { + return errors.Wrap(err, "initialize updates") + } + + // Write files + + if err := writeSecret(opt, orbitRoot); err != nil { + return errors.Wrap(err, "write enroll secret") + } + + if err := writeWixFile(opt, tmpDir); err != nil { + return errors.Wrap(err, "write wix file") + } + + if err := wix.Heat(tmpDir); err != nil { + return errors.Wrap(err, "package root files") + } + + if err := wix.TransformHeat(filepath.Join(tmpDir, "heat.wxs")); err != nil { + return errors.Wrap(err, "transform heat") + } + + if err := wix.Candle(tmpDir); err != nil { + return errors.Wrap(err, "build package") + } + + if err := wix.Light(tmpDir); err != nil { + return errors.Wrap(err, "build package") + } + + filename := fmt.Sprintf("orbit-osquery_%s.msi", opt.Version) + if err := os.Rename(filepath.Join(tmpDir, "orbit.msi"), filename); err != nil { + return errors.Wrap(err, "rename msi") + } + log.Info().Str("path", filename).Msg("wrote msi package") + + return nil +} + +func writeWixFile(opt Options, rootPath string) error { + // PackageInfo is metadata for the pkg + path := filepath.Join(rootPath, "main.wxs") + if err := os.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil { + return errors.Wrap(err, "mkdir") + } + + var contents bytes.Buffer + if err := windowsWixTemplate.Execute(&contents, opt); err != nil { + return errors.Wrap(err, "execute template") + } + + if err := ioutil.WriteFile(path, contents.Bytes(), 0o666); err != nil { + return errors.Wrap(err, "write file") + } + + return nil +} diff --git a/pkg/packaging/windows_templates.go b/pkg/packaging/windows_templates.go new file mode 100644 index 0000000000..6558200144 --- /dev/null +++ b/pkg/packaging/windows_templates.go @@ -0,0 +1,78 @@ +package packaging + +import "text/template" + +// Partially adapted from Launcher's wix XML in +// https://github.com/kolide/launcher/blob/master/pkg/packagekit/internal/assets/main.wxs. +var windowsWixTemplate = template.Must(template.New("").Option("missingkey=error").Parse( + ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`)) diff --git a/pkg/packaging/wix/transform.go b/pkg/packaging/wix/transform.go new file mode 100644 index 0000000000..e95a935efd --- /dev/null +++ b/pkg/packaging/wix/transform.go @@ -0,0 +1,113 @@ +package wix + +import ( + "bytes" + "encoding/xml" + "io/ioutil" + + "github.com/pkg/errors" +) + +type node struct { + XMLName xml.Name + Attrs attrs `xml:",any,attr"` + Content string `xml:",chardata"` + Children []*node `xml:",any"` +} + +type attrs []*xml.Attr + +// Get the value of the attr with the provided name, otherwise returning an +// empty string. +func (a attrs) Get(name string) string { + for _, attr := range a { + if attr.Name.Local == name { + return attr.Value + } + } + + return "" +} + +func xmlAttr(name, value string) *xml.Attr { + return &xml.Attr{Name: xml.Name{Local: name}, Value: value} +} + +func xmlNode(name string, attrs ...*xml.Attr) *node { + return &node{ + XMLName: xml.Name{Local: name}, + Attrs: attrs, + } +} + +func TransformHeat(path string) error { + contents, err := ioutil.ReadFile(path) + if err != nil { + return errors.Wrap(err, "read file") + } + + // Eliminate line feeds (they cause extra junk in the result) + contents = bytes.ReplaceAll(contents, []byte("\r"), []byte("")) + + var n node + if err := xml.Unmarshal(contents, &n); err != nil { + return errors.Wrap(err, "unmarshal xml") + } + + stack := []*node{} + if err := transform(&n, &stack); err != nil { + return errors.Wrap(err, "in transform") + } + + contents, err = xml.MarshalIndent(n, "", " ") + if err != nil { + return errors.Wrap(err, "marshal xml") + } + + if err := ioutil.WriteFile(path, contents, 0o600); err != nil { + return errors.Wrap(err, "write file") + } + + return nil +} + +func transform(cur *node, stack *[]*node) error { + // Clear namespace on all elements (generates unnecessarily noisy output if + // this is not done). + cur.XMLName.Space = "" + + // Change permissions for all files + if cur.XMLName.Local == "File" { + // This SDDL copied directly from osqueryd.exe after a regular + // osquery MSI install. We assume that osquery is getting the + // permissions correct and use exactly the same for our files. + // Using this cryptic string seems to be the only way to disable + // permission inheritance in a WiX package, so we may not have + // any option for something more readable. + sddl := "O:SYG:SYD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;0x1200a9;;;BU)" + if cur.Attrs.Get("Name") == "secret.txt" { + // This SDDL copied from properly configured file on a Windows + // 10 machine. Permissions are same as below but with read + // access removed for regular users. + sddl = "O:SYG:SYD:PAI(A;;FA;;;SY)(A;;FA;;;BA)" + + } + cur.Children = append(cur.Children, xmlNode( + "PermissionEx", + xmlAttr("Sddl", sddl), + )) + } + + // push current node onto stack + *stack = append(*stack, cur) + // Recursively walk the children + for _, child := range cur.Children { + if err := transform(child, stack); err != nil { + return err + } + } + // pop current node from stack + *stack = (*stack)[:len(*stack)-1] + + return nil +} diff --git a/pkg/packaging/wix/wix.go b/pkg/packaging/wix/wix.go new file mode 100644 index 0000000000..c068d9cbd1 --- /dev/null +++ b/pkg/packaging/wix/wix.go @@ -0,0 +1,88 @@ +// Package wix runs the WiX packaging tools via Docker. +// +// WiX's documentation is available at https://wixtoolset.org/. +package wix + +import ( + "os" + "os/exec" + + "github.com/pkg/errors" +) + +const ( + directoryReference = "ORBITROOT" +) + +// Heat runs the WiX Heat command on the provided directory. +// +// The Heat command creates XML fragments allowing WiX to include the entire +// directory. See +// https://wixtoolset.org/documentation/manual/v3/overview/heat.html. +func Heat(path string) error { + cmd := exec.Command( + "docker", "run", "--rm", "--platform", "linux/386", + "--volume", path+":/wix", // mount volume + "dactiv/wix:latest", // image name + "heat", "dir", "root", // command in image + "-out", "heat.wxs", + "-gg", "-g1", // generate UUIDs (required by wix) + "-cg", "OrbitFiles", // set ComponentGroup name + "-scom", "-sfrag", "-srd", "-sreg", // suppress unneccesary generated items + "-dr", directoryReference, // set reference name + "-ke", // keep empty directories + ) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "heat failed") + } + + return nil +} + +// Candle runs the WiX Candle command on the provided directory. +// +// See +// https://wixtoolset.org/documentation/manual/v3/overview/candle.html. +func Candle(path string) error { + cmd := exec.Command( + "docker", "run", "--rm", "--platform", "linux/386", + "--volume", path+":/wix", // mount volume + "dactiv/wix:latest", // image name + "candle", "heat.wxs", "main.wxs", // command in image + "-ext", "WixUtilExtension", + "-arch", "x64", + ) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "candle failed") + } + + return nil +} + +// Light runs the WiX Light command on the provided directory. +// +// See +// https://wixtoolset.org/documentation/manual/v3/overview/light.html. +func Light(path string) error { + cmd := exec.Command( + "docker", "run", "--rm", "--platform", "linux/386", + "--volume", path+":/wix", // mount volume + "dactiv/wix:latest", // image name + "light", "heat.wixobj", "main.wixobj", // command in image + "-ext", "WixUtilExtension", + "-b", "root", // Set directory for finding heat files + "-out", "orbit.msi", + "-sval", // skip validation (otherwise Wine crashes) + ) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "light failed") + } + + return nil +}