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
+}