Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let network processor handle multiple IPs #41918

Merged
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff]
- Remove unnecessary debug logs during idle connection teardown {issue}40824[40824]
- 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 multiple IPs {pull}41918[41918]
fearful-symmetry marked this conversation as resolved.
Show resolved Hide resolved
- 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*
Expand Down
23 changes: 23 additions & 0 deletions libbeat/conditions/conditions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
65 changes: 40 additions & 25 deletions libbeat/conditions/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,31 +94,31 @@ 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{
fields: map[string]networkMatcher{},
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:
Expand Down Expand Up @@ -157,15 +157,24 @@ 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
matches := false
for _, ip := range ipList {
if network.Contains(ip) {
matches = true
break
}
}
if !matches {
return false
}

fearful-symmetry marked this conversation as resolved.
Show resolved Hide resolved
}

return true
Expand Down Expand Up @@ -202,12 +211,18 @@ 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)
return []net.IP{net.ParseIP(v)}
case net.IP:
fearful-symmetry marked this conversation as resolved.
Show resolved Hide resolved
return v
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
}
Expand Down
52 changes: 52 additions & 0 deletions libbeat/conditions/network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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{
Expand All @@ -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) {
Expand Down
7 changes: 4 additions & 3 deletions libbeat/docs/processors-using.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,10 @@ range:
===== `network`

The `network` condition checks if the field is in a certain IP network range.
fearful-symmetry marked this conversation as resolved.
Show resolved Hide resolved
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:
Both IPv4 and IPv6 addresses are supported.
If the field contains multiple IPs, only one IP needs to match the conditional.
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`.
Expand Down
Loading