diff --git a/changes/16014-add-osquery-db-flag-to-fleetd b/changes/16014-add-osquery-db-flag-to-fleetd new file mode 100644 index 0000000000..0a38db7b00 --- /dev/null +++ b/changes/16014-add-osquery-db-flag-to-fleetd @@ -0,0 +1 @@ +* Add `--osquery-db` flag to `fleetctl package` command to configure a custom directory for osquery's database (`fleetctl package --osquery-db=/path/to/osquery.db`). diff --git a/cmd/fleetctl/package.go b/cmd/fleetctl/package.go index d0a49c138f..7abb5029f0 100644 --- a/cmd/fleetctl/package.go +++ b/cmd/fleetctl/package.go @@ -8,10 +8,12 @@ import ( "os" "path/filepath" "runtime" + "strings" "time" eefleetctl "github.com/fleetdm/fleet/v4/ee/fleetctl" "github.com/fleetdm/fleet/v4/orbit/pkg/packaging" + "github.com/fleetdm/fleet/v4/pkg/filepath_windows" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/rs/zerolog" zlog "github.com/rs/zerolog/log" @@ -240,6 +242,12 @@ func packageCommand() *cli.Command { EnvVars: []string{"FLEETCTL_DISABLE_KEYSTORE"}, Destination: &opt.DisableKeystore, }, + &cli.StringFlag{ + Name: "osquery-db", + Usage: "Sets a custom osquery database directory, it must be an absolute path (requires orbit >= v1.22.0)", + EnvVars: []string{"FLEETCTL_OSQUERY_DB"}, + Destination: &opt.OsqueryDB, + }, }, Action: func(c *cli.Context) error { if opt.FleetURL != "" || opt.EnrollSecret != "" { @@ -280,6 +288,10 @@ func packageCommand() *cli.Command { } } + if opt.OsqueryDB != "" && !isAbsolutePath(opt.OsqueryDB, c.String("type")) { + return fmt.Errorf("--osquery-db must be an absolute path: %q", opt.OsqueryDB) + } + if runtime.GOOS == "windows" && c.String("type") != "msi" { return errors.New("Windows can only build MSI packages.") } @@ -372,3 +384,13 @@ func checkPEMCertificate(path string) error { } return nil } + +// isAbsolutePath returns whether a path is absolute. +// It does not make use of filepath.IsAbs to support +// checking Windows paths from Go code running in unix. +func isAbsolutePath(path, pkgType string) bool { + if pkgType == "msi" { + return filepath_windows.IsAbs(path) + } + return strings.HasPrefix(path, "/") // this is the unix implementation of filepath.IsAbs +} diff --git a/orbit/changes/16014-add-osquery-db-flag-to-fleetd b/orbit/changes/16014-add-osquery-db-flag-to-fleetd new file mode 100644 index 0000000000..110f42de6c --- /dev/null +++ b/orbit/changes/16014-add-osquery-db-flag-to-fleetd @@ -0,0 +1 @@ +* Allow configuring a custom osquery database directory (`ORBIT_OSQUERY_DB` environment variable or `--osquery-db` flag). diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 29b6e017b2..e0c1ab61c2 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -197,6 +197,11 @@ func main() { Usage: "Disables the use of the keychain on macOS and Credentials Manager on Windows", EnvVars: []string{"ORBIT_DISABLE_KEYSTORE"}, }, + &cli.StringFlag{ + Name: "osquery-db", + Usage: "Sets a custom osquery database directory, it must be an absolute path", + EnvVars: []string{"ORBIT_OSQUERY_DB"}, + }, } app.Before = func(c *cli.Context) error { // handle old installations, which had default root dir set to /var/lib/orbit @@ -270,6 +275,10 @@ func main() { return errors.New("insecure and update-tls-certificate may not be specified together") } + if odb := c.String("osquery-db"); odb != "" && !filepath.IsAbs(odb) { + return fmt.Errorf("the osquery database must be an absolute path: %q", odb) + } + enrollSecretPath := c.String("enroll-secret-path") if enrollSecretPath != "" { if c.String("enroll-secret") != "" { @@ -588,7 +597,12 @@ func main() { log.Debug().Str("processes", fmt.Sprintf("%+v", killedProcesses)).Msg("existing osqueryd processes killed") } - osqueryHostInfo, err := getHostInfo(osquerydPath, filepath.Join(c.String("root-dir"), "osquery.db")) + osqueryDB := filepath.Join(c.String("root-dir"), "osquery.db") + if odb := c.String("osquery-db"); odb != "" { + osqueryDB = odb + } + + osqueryHostInfo, err := getHostInfo(osquerydPath, osqueryDB) if err != nil { return fmt.Errorf("get UUID: %w", err) } @@ -614,11 +628,16 @@ func main() { } var ( - options []osquery.Option + options []osquery.Option + // optionsAfterFlagfile is populated with options that will be set after the '--flagfile' argument + // to not allow users to change their values on their flagfiles. optionsAfterFlagfile []osquery.Option ) - options = append(options, osquery.WithDataPath(c.String("root-dir"))) + options = append(options, osquery.WithDataPath(c.String("root-dir"), "")) options = append(options, osquery.WithLogPath(filepath.Join(c.String("root-dir"), "osquery_log"))) + optionsAfterFlagfile = append(optionsAfterFlagfile, osquery.WithFlags( + []string{"--database_path", osqueryDB}, + )) if logFile != nil { // If set, redirect osqueryd's stderr to the logFile. @@ -1430,6 +1449,10 @@ type osqueryHostInfo struct { // getHostInfo retrieves system information about the host by shelling out to `osqueryd -S` and performing a `SELECT` query. func getHostInfo(osqueryPath string, osqueryDBPath string) (*osqueryHostInfo, error) { + // Make sure parent directory exists (`osqueryd -S` doesn't create the parent directories). + if err := os.MkdirAll(filepath.Dir(osqueryDBPath), constant.DefaultDirMode); err != nil { + return nil, err + } const systemQuery = "SELECT si.uuid, si.hardware_serial, si.hostname, os.platform, oi.instance_id FROM system_info si, os_version os, osquery_info oi" args := []string{ "-S", diff --git a/orbit/cmd/orbit/shell.go b/orbit/cmd/orbit/shell.go index 227e0af5cd..d81ae338f1 100644 --- a/orbit/cmd/orbit/shell.go +++ b/orbit/cmd/orbit/shell.go @@ -78,14 +78,24 @@ var shellCommand = &cli.Command{ var g run.Group + // We use a suffix on the extension path on Windows + // because there was an issue when the osqueryd instance ran through + // `orbit shell` attempted to register the same named pipe name used by + // the osqueryd instance launched by orbit service. extensionPathPostfix := "" if runtime.GOOS == "windows" { extensionPathPostfix = "-" + uuid.New().String() } + // We use a different path from the orbit daemon + // to avoid issues with concurrent accesses to files/databases + // (RocksDB lock and/or Windows file locking). + dataPath := filepath.Join(c.String("root-dir"), "shell") + osqueryDB := filepath.Join(dataPath, "osquery.db") opts := []osquery.Option{ osquery.WithShell(), - osquery.WithDataPathAndExtensionPathPostfix(filepath.Join(c.String("root-dir"), "shell"), extensionPathPostfix), + osquery.WithDataPath(dataPath, extensionPathPostfix), + osquery.WithFlags([]string{"--database_path", osqueryDB}), } // Detect if the additional arguments have a positional argument. diff --git a/orbit/pkg/osquery/osquery.go b/orbit/pkg/osquery/osquery.go index 3e63514ad2..75340d4393 100644 --- a/orbit/pkg/osquery/osquery.go +++ b/orbit/pkg/osquery/osquery.go @@ -102,34 +102,19 @@ func WithShell() func(*Runner) error { } } -func WithDataPath(path string) Option { +// WithDataPath configures the dataPath in the *Runner and +// sets the --pidfile and --extensions_socket paths +// to the osqueryd invocation. +func WithDataPath(dataPath, extensionPathPostfix string) Option { return func(r *Runner) error { - r.dataPath = path + r.dataPath = dataPath - if err := secure.MkdirAll(path, constant.DefaultDirMode); err != nil { + if err := secure.MkdirAll(dataPath, constant.DefaultDirMode); err != nil { return fmt.Errorf("initialize osquery data path: %w", err) } r.cmd.Args = append(r.cmd.Args, - "--pidfile="+filepath.Join(path, constant.OsqueryPidfile), - "--database_path="+filepath.Join(path, "osquery.db"), - "--extensions_socket="+r.ExtensionSocketPath(), - ) - return nil - } -} - -func WithDataPathAndExtensionPathPostfix(path string, extensionPathPostfix string) Option { - return func(r *Runner) error { - r.dataPath = path - - if err := secure.MkdirAll(path, constant.DefaultDirMode); err != nil { - return fmt.Errorf("initialize osquery data path: %w", err) - } - - r.cmd.Args = append(r.cmd.Args, - "--pidfile="+filepath.Join(path, constant.OsqueryPidfile), - "--database_path="+filepath.Join(path, "osquery.db"), + "--pidfile="+filepath.Join(dataPath, constant.OsqueryPidfile), "--extensions_socket="+r.ExtensionSocketPath()+extensionPathPostfix, ) return nil diff --git a/orbit/pkg/packaging/linux_shared.go b/orbit/pkg/packaging/linux_shared.go index b153fd7b3d..8e0d67aff9 100644 --- a/orbit/pkg/packaging/linux_shared.go +++ b/orbit/pkg/packaging/linux_shared.go @@ -294,6 +294,7 @@ ORBIT_FLEET_DESKTOP_ALTERNATIVE_BROWSER_HOST={{ .FleetDesktopAlternativeBrowserH {{ if .Debug }}ORBIT_DEBUG=true{{ end }} {{ if .EnableScripts }}ORBIT_ENABLE_SCRIPTS=true{{ end }} {{ if and (ne .HostIdentifier "") (ne .HostIdentifier "uuid") }}ORBIT_HOST_IDENTIFIER={{.HostIdentifier}}{{ end }} +{{ if .OsqueryDB }}ORBIT_OSQUERY_DB={{.OsqueryDB}}{{ end }} `)) func writeEnvFile(opt Options, rootPath string) error { diff --git a/orbit/pkg/packaging/macos_templates.go b/orbit/pkg/packaging/macos_templates.go index 91e7af2537..98200821f9 100644 --- a/orbit/pkg/packaging/macos_templates.go +++ b/orbit/pkg/packaging/macos_templates.go @@ -161,6 +161,10 @@ var macosLaunchdTemplate = template.Must(template.New("").Option("missingkey=err ORBIT_DISABLE_KEYSTORE true {{- end }} + {{- if .OsqueryDB }} + ORBIT_OSQUERY_DB + {{ .OsqueryDB }} + {{- end }} KeepAlive diff --git a/orbit/pkg/packaging/packaging.go b/orbit/pkg/packaging/packaging.go index 97b84389ba..05d0995805 100644 --- a/orbit/pkg/packaging/packaging.go +++ b/orbit/pkg/packaging/packaging.go @@ -122,6 +122,9 @@ type Options struct { EndUserEmail string // DisableKeystore disables the use of the keychain on macOS and Credentials Manager on Windows DisableKeystore bool + // OsqueryDB is the directory to use for the osquery database. + // If not set, then the default is `$ORBIT_ROOT_DIR/osquery.db`. + OsqueryDB string } func initializeTempDir() (string, error) { diff --git a/orbit/pkg/packaging/windows_templates.go b/orbit/pkg/packaging/windows_templates.go index d234c3a14e..029ad6775a 100644 --- a/orbit/pkg/packaging/windows_templates.go +++ b/orbit/pkg/packaging/windows_templates.go @@ -99,7 +99,7 @@ var windowsWixTemplate = template.Must(template.New("").Option("missingkey=error Start="auto" Type="ownProcess" Description="This service runs Fleet's osquery runtime and autoupdater (Orbit)." - Arguments='--root-dir "[ORBITROOT]." --log-file "[System64Folder]config\systemprofile\AppData\Local\FleetDM\Orbit\Logs\orbit-osquery.log" --fleet-url "[FLEET_URL]"{{ if .FleetCertificate }} --fleet-certificate "[ORBITROOT]fleet.pem"{{ end }}{{ if .EnrollSecret }} --enroll-secret-path "[ORBITROOT]secret.txt"{{ end }}{{if .Insecure }} --insecure{{ end }}{{ if .Debug }} --debug{{ end }}{{ if .UpdateURL }} --update-url "{{ .UpdateURL }}"{{ end }}{{ if .UpdateTLSServerCertificate }} --update-tls-certificate "[ORBITROOT]update.pem"{{ end }}{{ if .DisableUpdates }} --disable-updates{{ end }}{{ if .Desktop }} --fleet-desktop --desktop-channel {{ .DesktopChannel }}{{ if .FleetDesktopAlternativeBrowserHost }} --fleet-desktop-alternative-browser-host {{ .FleetDesktopAlternativeBrowserHost }}{{ end }}{{ end }} --orbit-channel "{{ .OrbitChannel }}" --osqueryd-channel "{{ .OsquerydChannel }}" {{ if .EnableScripts }} --enable-scripts{{ end }}{{ if and (ne .HostIdentifier "") (ne .HostIdentifier "uuid") }}--host-identifier={{ .HostIdentifier }}{{ end }}{{ if .EndUserEmail }} --end-user-email "{{ .EndUserEmail }}"{{ end }}' + Arguments='--root-dir "[ORBITROOT]." --log-file "[System64Folder]config\systemprofile\AppData\Local\FleetDM\Orbit\Logs\orbit-osquery.log" --fleet-url "[FLEET_URL]"{{ if .FleetCertificate }} --fleet-certificate "[ORBITROOT]fleet.pem"{{ end }}{{ if .EnrollSecret }} --enroll-secret-path "[ORBITROOT]secret.txt"{{ end }}{{if .Insecure }} --insecure{{ end }}{{ if .Debug }} --debug{{ end }}{{ if .UpdateURL }} --update-url "{{ .UpdateURL }}"{{ end }}{{ if .UpdateTLSServerCertificate }} --update-tls-certificate "[ORBITROOT]update.pem"{{ end }}{{ if .DisableUpdates }} --disable-updates{{ end }}{{ if .Desktop }} --fleet-desktop --desktop-channel {{ .DesktopChannel }}{{ if .FleetDesktopAlternativeBrowserHost }} --fleet-desktop-alternative-browser-host {{ .FleetDesktopAlternativeBrowserHost }}{{ end }}{{ end }} --orbit-channel "{{ .OrbitChannel }}" --osqueryd-channel "{{ .OsquerydChannel }}" {{ if .EnableScripts }} --enable-scripts{{ end }}{{ if and (ne .HostIdentifier "") (ne .HostIdentifier "uuid") }}--host-identifier={{ .HostIdentifier }}{{ end }}{{ if .EndUserEmail }} --end-user-email "{{ .EndUserEmail }}"{{ end }}{{ if .OsqueryDB }} --osquery-db="{{ .OsqueryDB }}"{{ end }}' > len(prefix) && !isSlash(s[len(prefix)]) { + return false + } + return true +} + +// volumeNameLen returns length of the leading volume name on Windows. +// It returns 0 elsewhere. +// +// See: +// https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats +// https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html +func volumeNameLen(path string) int { + switch { + case len(path) >= 2 && path[1] == ':': + // Path starts with a drive letter. + // + // Not all Windows functions necessarily enforce the requirement that + // drive letters be in the set A-Z, and we don't try to here. + // + // We don't handle the case of a path starting with a non-ASCII character, + // in which case the "drive letter" might be multiple bytes long. + return 2 + + case len(path) == 0 || !isSlash(path[0]): + // Path does not have a volume component. + return 0 + + case pathHasPrefixFold(path, `\\.\UNC`): + // We're going to treat the UNC host and share as part of the volume + // prefix for historical reasons, but this isn't really principled; + // Windows's own GetFullPathName will happily remove the first + // component of the path in this space, converting + // \\.\unc\a\b\..\c into \\.\unc\a\c. + return uncLen(path, len(`\\.\UNC\`)) + + case pathHasPrefixFold(path, `\\.`) || + pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`): + // Path starts with \\.\, and is a Local Device path; or + // path starts with \\?\ or \??\ and is a Root Local Device path. + // + // We treat the next component after the \\.\ prefix as + // part of the volume name, which means Clean(`\\?\c:\`) + // won't remove the trailing \. (See #64028.) + if len(path) == 3 { + return 3 // exactly \\. + } + _, rest, ok := cutPath(path[4:]) + if !ok { + return len(path) + } + return len(path) - len(rest) - 1 + + case len(path) >= 2 && isSlash(path[1]): + // Path starts with \\, and is a UNC path. + return uncLen(path, 2) + } + return 0 +} + +// uncLen returns the length of the volume prefix of a UNC path. +// prefixLen is the prefix prior to the start of the UNC host; +// for example, for "//host/share", the prefixLen is len("//")==2. +func uncLen(path string, prefixLen int) int { + count := 0 + for i := prefixLen; i < len(path); i++ { + if isSlash(path[i]) { + count++ + if count == 2 { + return i + } + } + } + return len(path) +} + +// cutPath slices path around the first path separator. +func cutPath(path string) (before, after string, found bool) { + for i := range path { + if isSlash(path[i]) { + return path[:i], path[i+1:], true + } + } + return path, "", false +} diff --git a/server/fleet/agent_options.go b/server/fleet/agent_options.go index 7d72858488..ef38d7d23c 100644 --- a/server/fleet/agent_options.go +++ b/server/fleet/agent_options.go @@ -61,6 +61,9 @@ func ValidateJSONAgentOptions(ctx context.Context, ds Datastore, rawJSON json.Ra if flags.ExtensionsAutoload != "" { return fmt.Errorf(flagNotSupportedErr, "--extensions_autoload") } + if flags.DatabasePath != "" { + return fmt.Errorf(flagNotSupportedErr, "--database_path") + } } if len(opts.UpdateChannels) > 0 { diff --git a/tools/tuf/test/create_repository.sh b/tools/tuf/test/create_repository.sh index 8b1db300f2..ba27e17e05 100755 --- a/tools/tuf/test/create_repository.sh +++ b/tools/tuf/test/create_repository.sh @@ -29,7 +29,7 @@ SWIFT_DIALOG_MACOS_APP_VERSION=2.2.1 SWIFT_DIALOG_MACOS_APP_BUILD_VERSION=4591 if [[ -z "$OSQUERY_VERSION" ]]; then - OSQUERY_VERSION=5.10.2 + OSQUERY_VERSION=5.11.0 fi mkdir -p $TUF_PATH/tmp