diff --git a/changes/43598-fix-gitops-glob-path-validation b/changes/43598-fix-gitops-glob-path-validation new file mode 100644 index 0000000000..bd46be2bd3 --- /dev/null +++ b/changes/43598-fix-gitops-glob-path-validation @@ -0,0 +1 @@ +- Fixed an issue where `fleetctl gitops` rejected `path:` values whose actual filenames contained glob metacharacters even when the file existed at that literal path. diff --git a/pkg/spec/gitops.go b/pkg/spec/gitops.go index da8a72046b..bc0b595244 100644 --- a/pkg/spec/gitops.go +++ b/pkg/spec/gitops.go @@ -1297,11 +1297,21 @@ func expandBaseItems[T any, PT interface { result = append(result, entity) // Single path -- resolve to absolute path and add to result. case hasPath: - if containsGlobMeta(*baseItem.Path) { - errs = append(errs, fmt.Errorf(`%s "path" %q contains glob characters; use "paths" for glob patterns`, entityType, *baseItem.Path)) - continue - } resolved := resolveApplyRelativePath(baseDir, *baseItem.Path) + // Reject glob metacharacters in "path:" only when the literal path + // does not resolve to an existing file. This allows filenames that + // contain literal glob metacharacters (e.g. Windows CSP names like + // "[AllowSpotlightCollection].xml") to be referenced via "path:". + if containsGlobMeta(*baseItem.Path) { + if _, err := os.Stat(resolved); err != nil { + if os.IsNotExist(err) { + errs = append(errs, fmt.Errorf(`%s "path" %q contains glob characters; use "paths" for glob patterns`, entityType, *baseItem.Path)) + } else { + errs = append(errs, fmt.Errorf("failed to stat %s path %q: %w", entityType, resolved, err)) + } + continue + } + } // Check for duplicate filenames if requested. if opts.RequireUniqueBasenames { base := filepath.Base(resolved) diff --git a/pkg/spec/gitops_test.go b/pkg/spec/gitops_test.go index 54ae94c806..930b04dce3 100644 --- a/pkg/spec/gitops_test.go +++ b/pkg/spec/gitops_test.go @@ -2217,6 +2217,41 @@ func TestExpandBaseItems(t *testing.T) { requireErrorContains(t, errs, `contains glob characters`) }) + // Filenames containing glob metacharacters should be accepted by "path:" + // when the literal file exists on disk. Common with Windows MDM CSP + // profile names like "[AllowSpotlightCollection].xml". Regression test for + // fleetdm/fleet#43598. + t.Run("path_with_literal_glob_meta_chars_existing_file", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + filenames := []string{ + "AllowRebootless -[Updates].xml", + "profile{a,b}.xml", + } + for _, name := range filenames { + require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(""), 0o644)) + } + + items := make([]fleet.BaseItem, 0, len(filenames)) + for _, name := range filenames { + items = append(items, fleet.BaseItem{Path: ptr.String(name)}) //nolint:modernize + } + result, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{}) + require.Empty(t, errs) + require.Len(t, result, len(filenames)) + for i, name := range filenames { + assert.Equal(t, filepath.Join(dir, name), *result[i].Path) + } + }) + + t.Run("path_with_glob_meta_chars_missing_file_error", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + items := []fleet.BaseItem{{Path: ptr.String("does-not-[exist].xml")}} //nolint:modernize + _, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{}) + requireErrorContains(t, errs, `contains glob characters`) + }) + t.Run("both_path_and_paths_error", func(t *testing.T) { t.Parallel() items := []fleet.BaseItem{{Path: ptr.String("foo.yml"), Paths: ptr.String("*.yml")}} //nolint:modernize