Fix fleetd with pre-packaged osquery flags (#47305)
Resolves #47285. Final behavior (matches pre-1.56): ``` ┌────────────────────┬─────────────────────────┬───────────────────────────────────────┐ │ command_line_flags │ wire config.Flags │ Orbit action │ ├────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Unset │ omitted (nil, len == 0) │ Preserve osquery.flags — return early │ ├────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ null │ null (len == 4) │ Clear (parses to empty map) │ ├────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ {} │ {} (len == 2) │ Clear (parses to empty map) │ ├────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ {"verbose": true} │ non-empty │ Write flags │ └────────────────────┴─────────────────────────┴───────────────────────────────────────┘ ``` - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [X] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [x] Verified that fleetd runs on macOS, Linux and Windows - [x] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Preserve existing pre-packaged or user-provided osquery flagfiles when the server omits flag configuration (no unintended clears or restarts). * Treat an explicit empty flag payload (including JSON empty document or JSON "null") as an instruction to clear flagfiles and queue a restart. * **Tests** * Expanded coverage to validate the preserve-vs-clear behavior for unset, empty, and "null" flag payloads. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Fixed fleetd clearing pre-packaged/user-provided osquery flagfiles (`osquery.flags`) when `command_line_flags` is unset in the agent settings. Setting `command_line_flags` to an empty document (`{}` or `null`) still explicitly clears the flagfile.
|
||||
@@ -45,9 +45,26 @@ func NewFlagReceiver(triggerOrbitRestart func(reason string), opt FlagUpdateOpti
|
||||
// It gets the flags from the Fleet server, and compares them to locally stored flagfile (if it exists)
|
||||
// If the flag comparison from disk and server are not equal, it writes the flags to disk, and returns true
|
||||
func (r *FlagRunner) Run(config *fleet.OrbitConfig) error {
|
||||
// When command_line_flags is not set in the agent settings, the server omits
|
||||
// the field and config.Flags is nil/empty. In that case Orbit leaves the
|
||||
// osquery.flags file untouched, preserving any flags that were pre-packaged
|
||||
// with fleetd or otherwise provided by the user.
|
||||
//
|
||||
// Explicitly setting command_line_flags to an empty document ({}) or "null" is
|
||||
// distinct: those arrive as a non-empty payload that parses to an empty flag
|
||||
// map, which reconciles (clears) the osquery.flags file below.
|
||||
if len(config.Flags) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
osqueryFlagMapFromFleet, err := getFlagsFromJSON(config.Flags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing flags: %w", err)
|
||||
}
|
||||
|
||||
flagFileExists := true
|
||||
|
||||
// first off try and read osquery.flags from disk
|
||||
// try and read osquery.flags from disk
|
||||
osqueryFlagMapFromFile, err := readFlagFile(r.opt.RootDir)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
@@ -57,16 +74,7 @@ func (r *FlagRunner) Run(config *fleet.OrbitConfig) error {
|
||||
flagFileExists = false
|
||||
}
|
||||
|
||||
// Nil/empty Flags is a valid state we must be able to reconcile TO
|
||||
// (admin cleared command_line_flags, or server-side debug merge turned off).
|
||||
osqueryFlagMapFromFleet := map[string]string{}
|
||||
if len(config.Flags) > 0 {
|
||||
osqueryFlagMapFromFleet, err = getFlagsFromJSON(config.Flags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing flags: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// nothing on disk and nothing to write (e.g. {} with no existing file): no-op
|
||||
if !flagFileExists && len(osqueryFlagMapFromFleet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -78,8 +78,9 @@ func touchFile(t *testing.T, name string) {
|
||||
}
|
||||
|
||||
// TestDoFlagsUpdateWithEmptyFlags tests the scenario of Fleet flag `command_line_flags`
|
||||
// being set to an empty JSON document `{}` and Orbit osquery.flags file being
|
||||
// an empty file. Such scenario should trigger no update of flags.
|
||||
// being set to an empty JSON document `{}`. Setting it to `{}` is an explicit instruction
|
||||
// to clear osquery flags, so Orbit reconciles the osquery.flags file to empty (distinct
|
||||
// from command_line_flags being unset, which is covered by TestDoFlagsUpdateWithNilFlags).
|
||||
func TestDoFlagsUpdateWithEmptyFlags(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
osqueryFlagsFile := filepath.Join(rootDir, "osquery.flags")
|
||||
@@ -102,13 +103,14 @@ func TestDoFlagsUpdateWithEmptyFlags(t *testing.T) {
|
||||
|
||||
// Non-empty fleet flags and osquery.flags has empty flags.
|
||||
testConfig = &fleet.OrbitConfig{
|
||||
Flags: json.RawMessage(`{"--verbose": true}`),
|
||||
Flags: json.RawMessage(`{"verbose": true}`),
|
||||
}
|
||||
err = fr.Run(testConfig)
|
||||
require.NoError(t, err)
|
||||
require.True(t, restartQueued)
|
||||
|
||||
// Empty Fleet flags and osquery.flags has non-empty flags.
|
||||
// Empty Fleet flags ({}) and osquery.flags has non-empty flags: the file is
|
||||
// cleared and a restart is triggered.
|
||||
restartQueued = false
|
||||
testConfig = &fleet.OrbitConfig{
|
||||
Flags: json.RawMessage("{}"),
|
||||
@@ -118,8 +120,16 @@ func TestDoFlagsUpdateWithEmptyFlags(t *testing.T) {
|
||||
err = fr.Run(testConfig)
|
||||
require.NoError(t, err)
|
||||
require.True(t, restartQueued)
|
||||
|
||||
contents, err := os.ReadFile(osqueryFlagsFile)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, string(contents))
|
||||
}
|
||||
|
||||
// TestDoFlagsUpdateWithNilFlags verifies that when the server is not managing
|
||||
// osquery command-line flags (command_line_flags unset, so config.Flags is
|
||||
// nil/empty), Orbit leaves the osquery.flags file untouched, preserving any
|
||||
// pre-packaged or user-provided flags.
|
||||
func TestDoFlagsUpdateWithNilFlags(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
osqueryFlagsFile := filepath.Join(rootDir, "osquery.flags")
|
||||
@@ -133,11 +143,44 @@ func TestDoFlagsUpdateWithNilFlags(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.False(t, restartQueued)
|
||||
|
||||
// Nil Flags + existing file: reconcile to empty + restart.
|
||||
err = os.WriteFile(osqueryFlagsFile, []byte("--verbose=true\n--tls_dump=true\n"), 0o644)
|
||||
// Nil Flags + existing user-provided file: file is preserved, no restart.
|
||||
userFlags := "--verbose=true\n--tls_dump=true\n"
|
||||
err = os.WriteFile(osqueryFlagsFile, []byte(userFlags), 0o644)
|
||||
require.NoError(t, err)
|
||||
err = fr.Run(&fleet.OrbitConfig{Flags: nil})
|
||||
require.NoError(t, err)
|
||||
require.False(t, restartQueued)
|
||||
|
||||
contents, err := os.ReadFile(osqueryFlagsFile)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userFlags, string(contents))
|
||||
|
||||
// Empty (but non-nil) Flags is treated the same as nil: file preserved.
|
||||
err = fr.Run(&fleet.OrbitConfig{Flags: json.RawMessage("")})
|
||||
require.NoError(t, err)
|
||||
require.False(t, restartQueued)
|
||||
|
||||
contents, err = os.ReadFile(osqueryFlagsFile)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userFlags, string(contents))
|
||||
}
|
||||
|
||||
// TestDoFlagsUpdateWithNullFlags verifies that the JSON literal "null" is an
|
||||
// explicit instruction to clear osquery flags, distinct from command_line_flags
|
||||
// being unset (which preserves the file).
|
||||
func TestDoFlagsUpdateWithNullFlags(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
osqueryFlagsFile := filepath.Join(rootDir, "osquery.flags")
|
||||
|
||||
var restartQueued bool
|
||||
queueOrbitRestart := func(string) { restartQueued = true }
|
||||
fr := NewFlagReceiver(queueOrbitRestart, FlagUpdateOptions{RootDir: rootDir})
|
||||
|
||||
// "null" + existing flags: the file is cleared and a restart is triggered.
|
||||
err := os.WriteFile(osqueryFlagsFile, []byte("--verbose=true\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
err = fr.Run(&fleet.OrbitConfig{Flags: json.RawMessage("null")})
|
||||
require.NoError(t, err)
|
||||
require.True(t, restartQueued)
|
||||
|
||||
contents, err := os.ReadFile(osqueryFlagsFile)
|
||||
|
||||
Reference in New Issue
Block a user