diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 536c5627985..e82fc6f4a18 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -98,6 +98,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Updated Websocket input title to align with existing inputs {pull}39006[39006] - Fix publication of group data from the Okta entity analytics provider. {pull}40681[40681] - Ensure netflow custom field configuration is applied. {issue}40735[40735] {pull}40730[40730] +- Fix replace processor handling of zero string replacement validation. {pull}40751[40751] *Heartbeat* diff --git a/libbeat/processors/actions/replace.go b/libbeat/processors/actions/replace.go index e168e644181..48feaf193e5 100644 --- a/libbeat/processors/actions/replace.go +++ b/libbeat/processors/actions/replace.go @@ -45,7 +45,14 @@ type replaceStringConfig struct { type replaceConfig struct { Field string `config:"field" validate:"required"` Pattern *regexp.Regexp `config:"pattern" validate:"required"` - Replacement string `config:"replacement" validate:"required"` + Replacement *string `config:"replacement"` +} + +func (c replaceConfig) Validate() error { + if c.Replacement == nil { + return errors.New("missing replacement") + } + return nil } func init() { @@ -82,7 +89,7 @@ func (f *replaceString) Run(event *beat.Event) (*beat.Event, error) { } for _, field := range f.config.Fields { - err := f.replaceField(field.Field, field.Pattern, field.Replacement, event) + err := f.replaceField(field.Field, field.Pattern, *field.Replacement, event) if err != nil { errMsg := fmt.Errorf("Failed to replace fields in processor: %w", err) f.log.Debugw(errMsg.Error(), logp.TypeKey, logp.EventType) diff --git a/libbeat/processors/actions/replace_test.go b/libbeat/processors/actions/replace_test.go index 7fd6c8def13..3bf9e559890 100644 --- a/libbeat/processors/actions/replace_test.go +++ b/libbeat/processors/actions/replace_test.go @@ -45,7 +45,7 @@ func TestBadConfig(t *testing.T) { }, { name: "no-regex", - cfg: replaceStringConfig{Fields: []replaceConfig{{Field: "message", Replacement: "new_message"}}}, + cfg: replaceStringConfig{Fields: []replaceConfig{{Field: "message", Replacement: ptr("new_message")}}}, shouldError: true, }, { @@ -56,7 +56,7 @@ func TestBadConfig(t *testing.T) { { name: "valid-then-invalid", cfg: replaceStringConfig{Fields: []replaceConfig{ - {Field: "message", Pattern: regexp.MustCompile(`message`), Replacement: "new_message"}, + {Field: "message", Pattern: regexp.MustCompile(`message`), Replacement: ptr("new_message")}, {Field: "message", Pattern: regexp.MustCompile(`message`)}, }, }, @@ -64,7 +64,12 @@ func TestBadConfig(t *testing.T) { }, { name: "no-error", - cfg: replaceStringConfig{Fields: []replaceConfig{{Field: "message", Replacement: "new_message", Pattern: regexp.MustCompile(`message`)}}}, + cfg: replaceStringConfig{Fields: []replaceConfig{{Field: "message", Replacement: ptr("new_message"), Pattern: regexp.MustCompile(`message`)}}}, + shouldError: false, + }, + { + name: "no-error zero string", + cfg: replaceStringConfig{Fields: []replaceConfig{{Field: "message", Replacement: ptr(""), Pattern: regexp.MustCompile(`message`)}}}, shouldError: false, }, } @@ -102,7 +107,7 @@ func TestReplaceRun(t *testing.T) { { Field: "f", Pattern: regexp.MustCompile(`a`), - Replacement: "b", + Replacement: ptr("b"), }, }, Input: mapstr.M{ @@ -115,13 +120,32 @@ func TestReplaceRun(t *testing.T) { IgnoreMissing: false, FailOnError: true, }, + { + description: "replace with zero", + Fields: []replaceConfig{ + { + Field: "f", + Pattern: regexp.MustCompile(`a`), + Replacement: ptr(""), + }, + }, + Input: mapstr.M{ + "f": "abc", + }, + Output: mapstr.M{ + "f": "bc", + }, + error: false, + IgnoreMissing: false, + FailOnError: true, + }, { description: "Add one more hierarchy to event", Fields: []replaceConfig{ { Field: "f.b", Pattern: regexp.MustCompile(`a`), - Replacement: "b", + Replacement: ptr("b"), }, }, Input: mapstr.M{ @@ -144,12 +168,12 @@ func TestReplaceRun(t *testing.T) { { Field: "f", Pattern: regexp.MustCompile(`a.*c`), - Replacement: "cab", + Replacement: ptr("cab"), }, { Field: "g", Pattern: regexp.MustCompile(`ef`), - Replacement: "oor", + Replacement: ptr("oor"), }, }, Input: mapstr.M{ @@ -170,12 +194,12 @@ func TestReplaceRun(t *testing.T) { { Field: "f", Pattern: regexp.MustCompile(`abc`), - Replacement: "xyz", + Replacement: ptr("xyz"), }, { Field: "g", Pattern: regexp.MustCompile(`def`), - Replacement: "", + Replacement: nil, }, }, Input: mapstr.M{ @@ -216,11 +240,13 @@ func TestReplaceRun(t *testing.T) { assert.Error(t, err) } - assert.True(t, reflect.DeepEqual(newEvent.Fields, test.Output)) + assert.Equal(t, newEvent.Fields, test.Output) }) } } +func ptr[T any](v T) *T { return &v } + func TestReplaceField(t *testing.T) { var tests = []struct { Field string @@ -322,7 +348,7 @@ func TestReplaceField(t *testing.T) { { Field: "@metadata.f", Pattern: regexp.MustCompile(`a`), - Replacement: "b", + Replacement: ptr("b"), }, }, },