From 0827467636d1b22b4118968c15acdeff2937456f Mon Sep 17 00:00:00 2001 From: "Alex K." <8418476+fearful-symmetry@users.noreply.github.com> Date: Mon, 9 Dec 2024 07:31:17 -0800 Subject: [PATCH] Let network processor handle multiple IPs (#41918) * let network processor handle multiple IPs * add changelog * linter... * fix linter, logs, changelog * linter... * linter... * linter... * update docs * whoops * simplify logic * docs, cleanup --- CHANGELOG.next.asciidoc | 1 + libbeat/conditions/conditions_test.go | 23 ++++++++++ libbeat/conditions/network.go | 61 +++++++++++++++----------- libbeat/conditions/network_test.go | 52 ++++++++++++++++++++++ libbeat/docs/processors-using.asciidoc | 13 ++++-- 5 files changed, 121 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 76bc04ede5dd..bb6c4df0f627 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -118,6 +118,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Remove unnecessary reload for Elastic Agent managed beats when apm tracing config changes from nil to nil {pull}41794[41794] - Fix incorrect cloud provider identification in add_cloud_metadata processor using provider priority mechanism {pull}41636[41636] - Prevent panic if libbeat processors are loaded more than once. {issue}41475[41475] {pull}41857[51857] +- Allow network condition to handle field values that are arrays of IP addresses. {pull}41918[41918] - Fix a bug where log files are rotated on startup when interval is configured and rotateonstartup is disabled {issue}41894[41894] {pull}41895[41895] *Auditbeat* diff --git a/libbeat/conditions/conditions_test.go b/libbeat/conditions/conditions_test.go index be74f0a7caa9..8abbb0694e73 100644 --- a/libbeat/conditions/conditions_test.go +++ b/libbeat/conditions/conditions_test.go @@ -105,6 +105,29 @@ var httpResponseTestEvent = &beat.Event{ }, } +var httpResponseEventIPList = &beat.Event{ + Timestamp: time.Now(), + Fields: mapstr.M{ + "@timestamp": "2024-12-05T09:51:23.642Z", + "ecs": mapstr.M{ + "version": "8.11.0", + }, + "host": mapstr.M{ + "hostname": "testhost", + "os": mapstr.M{ + "type": "linux", + "family": "debian", + "version": "11 (bullseye)", + "platform": "debian", + }, + "ip": []string{ + "10.1.0.55", + "fe80::4001:aff:fe9a:55", + }, + }, + }, +} + func testConfig(t *testing.T, expected bool, event *beat.Event, config *Config) { t.Helper() logp.TestingSetup() diff --git a/libbeat/conditions/network.go b/libbeat/conditions/network.go index 76ab2d08423c..25c5fb86e701 100644 --- a/libbeat/conditions/network.go +++ b/libbeat/conditions/network.go @@ -20,6 +20,7 @@ package conditions import ( "fmt" "net" + "slices" "strings" "github.com/elastic/elastic-agent-libs/logp" @@ -94,6 +95,24 @@ func (m multiNetworkMatcher) String() string { return strings.Join(names, " OR ") } +func makeMatcher(network string) (networkMatcher, error) { + m := singleNetworkMatcher{name: network, netContainsFunc: namedNetworks[network]} + if m.netContainsFunc == nil { + subnet, err := parseCIDR(network) + if err != nil { + return nil, err + } + m.netContainsFunc = subnet.Contains + } + return m, nil +} + +func invalidTypeError(field string, value interface{}) error { + return fmt.Errorf("network condition attempted to set "+ + "'%v' -> '%v' and encountered unexpected type '%T', only "+ + "strings or []strings are allowed", field, value, value) +} + // NewNetworkCondition builds a new Network using the given configuration. func NewNetworkCondition(fields map[string]interface{}) (*Network, error) { cond := &Network{ @@ -101,24 +120,6 @@ func NewNetworkCondition(fields map[string]interface{}) (*Network, error) { log: logp.NewLogger(logName), } - makeMatcher := func(network string) (networkMatcher, error) { - m := singleNetworkMatcher{name: network, netContainsFunc: namedNetworks[network]} - if m.netContainsFunc == nil { - subnet, err := parseCIDR(network) - if err != nil { - return nil, err - } - m.netContainsFunc = subnet.Contains - } - return m, nil - } - - invalidTypeError := func(field string, value interface{}) error { - return fmt.Errorf("network condition attempted to set "+ - "'%v' -> '%v' and encountered unexpected type '%T', only "+ - "strings or []strings are allowed", field, value, value) - } - for field, value := range mapstr.M(fields).Flatten() { switch v := value.(type) { case string: @@ -157,15 +158,17 @@ func (c *Network) Check(event ValuesMap) bool { return false } - ip := extractIP(value) - if ip == nil { + ipList := extractIP(value) + if len(ipList) == 0 { c.log.Debugf("Invalid IP address in field=%v for network condition", field) return false } - - if !network.Contains(ip) { + // match on an "any" basis when we find multiple IPs in the event; + // if the network matcher returns true for any seen IP, consider it a match + if !slices.ContainsFunc(ipList, network.Contains) { return false } + } return true @@ -202,12 +205,20 @@ func parseCIDR(value string) (*net.IPNet, error) { // extractIP return an IP address if unk is an IP address string or a net.IP. // Otherwise it returns nil. -func extractIP(unk interface{}) net.IP { +func extractIP(unk interface{}) []net.IP { switch v := unk.(type) { case string: - return net.ParseIP(v) - case net.IP: + return []net.IP{net.ParseIP(v)} + case []net.IP: return v + case net.IP: + return []net.IP{v} + case []string: + parsed := make([]net.IP, len(v)) + for i, rawIP := range v { + parsed[i] = net.ParseIP(rawIP) + } + return parsed default: return nil } diff --git a/libbeat/conditions/network_test.go b/libbeat/conditions/network_test.go index b79568098e42..71ab0918e675 100644 --- a/libbeat/conditions/network_test.go +++ b/libbeat/conditions/network_test.go @@ -79,6 +79,26 @@ network: testYAMLConfig(t, true, evt, yaml) }) + + t.Run("IP list", func(t *testing.T) { + const yaml = ` +network: + ip: + client: [loopback] + server: [loopback] + host: 10.10.0.0/8 +` + + evt := &beat.Event{Fields: mapstr.M{ + "ip": mapstr.M{ + "client": "127.0.0.1", + "server": "127.0.0.1", + "host": []string{"10.10.0.83", "fe80::4001:aff:fe9a:53"}, + }, + }} + + testYAMLConfig(t, true, evt, yaml) + }) } func TestNetworkCreate(t *testing.T) { @@ -166,6 +186,22 @@ func TestNetworkCheck(t *testing.T) { }) }) + t.Run("multiple IPs field single match", func(t *testing.T) { + testConfig(t, true, httpResponseEventIPList, &Config{ + Network: map[string]interface{}{ + "host.ip": "10.1.0.0/24", + }, + }) + }) + + t.Run("multiple IPs field negative match", func(t *testing.T) { + testConfig(t, false, httpResponseEventIPList, &Config{ + Network: map[string]interface{}{ + "host.ip": "127.0.0.0/24", + }, + }) + }) + // Multiple conditions are treated as an implicit AND. t.Run("multiple fields negative match", func(t *testing.T) { testConfig(t, false, httpResponseTestEvent, &Config{ @@ -191,6 +227,22 @@ func TestNetworkCheck(t *testing.T) { }, }) }) + + t.Run("multiple values multiple IPs match", func(t *testing.T) { + testConfig(t, true, httpResponseEventIPList, &Config{ + Network: map[string]interface{}{ + "host.ip": []interface{}{"10.1.0.0/24", "127.0.0.0/24"}, + }, + }) + }) + + t.Run("multiple values multiple IPs no match", func(t *testing.T) { + testConfig(t, false, httpResponseEventIPList, &Config{ + Network: map[string]interface{}{ + "host.ip": []interface{}{"12.1.0.0/24", "127.0.0.0/24"}, + }, + }) + }) } func TestNetworkPrivate(t *testing.T) { diff --git a/libbeat/docs/processors-using.asciidoc b/libbeat/docs/processors-using.asciidoc index dd91ea8d5db6..a029f5f2ea8b 100644 --- a/libbeat/docs/processors-using.asciidoc +++ b/libbeat/docs/processors-using.asciidoc @@ -311,10 +311,15 @@ range: [[condition-network]] ===== `network` -The `network` condition checks if the field is in a certain IP network range. -Both IPv4 and IPv6 addresses are supported. The network range may be specified -using CIDR notation, like "192.0.2.0/24" or "2001:db8::/32", or by using one of -these named ranges: +The `network` condition checks whether a field's value falls within a specified +IP network range. If multiple fields are provided, each field value must match +its corresponding network range. You can specify multiple network ranges for a +single field, and a match occurs if any one of the ranges matches. If the field +value is an array of IPs, it will match if any of the IPs fall within any of the +given ranges. Both IPv4 and IPv6 addresses are supported. + +The network range may be specified using CIDR notation, like "192.0.2.0/24" or +"2001:db8::/32", or by using one of these named ranges: - `loopback` - Matches loopback addresses in the range of `127.0.0.0/8` or `::1/128`.