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 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*
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
61 changes: 36 additions & 25 deletions libbeat/conditions/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package conditions
import (
"fmt"
"net"
"slices"
"strings"

"github.com/elastic/elastic-agent-libs/logp"
Expand Down Expand Up @@ -94,31 +95,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 +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
Expand Down Expand Up @@ -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:
fearful-symmetry marked this conversation as resolved.
Show resolved Hide resolved
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
13 changes: 9 additions & 4 deletions libbeat/docs/processors-using.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Loading