Skip to content

Add network condition for matching IP network ranges #10743

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d
- Add `overwrite` and `check_exists` settings to ILM support. {pull}10347[10347]
- Generate Kibana index pattern on demand instead of using a local file. {pull}10478[10478]
- Calls to Elasticsearch X-Pack APIs made by Beats won't cause deprecation logs in Elasticsearch logs. {9656}9656[9656]
- Add `network` condition to processors for matching IP addresses against CIDRs. {pull}10743[10743]
- Add if/then/else support to processors. {pull}10744[10744]

*Auditbeat*
Expand Down
19 changes: 11 additions & 8 deletions libbeat/conditions/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ const logName = "conditions"

// Config represents a configuration for a condition, as you would find it in the config files.
type Config struct {
Equals *Fields `config:"equals"`
Contains *Fields `config:"contains"`
Regexp *Fields `config:"regexp"`
Range *Fields `config:"range"`
HasFields []string `config:"has_fields"`
OR []Config `config:"or"`
AND []Config `config:"and"`
NOT *Config `config:"not"`
Equals *Fields `config:"equals"`
Contains *Fields `config:"contains"`
Regexp *Fields `config:"regexp"`
Range *Fields `config:"range"`
HasFields []string `config:"has_fields"`
Network map[string]interface{} `config:"network"`
OR []Config `config:"or"`
AND []Config `config:"and"`
NOT *Config `config:"not"`
}

// Condition is the interface for all defined conditions
Expand Down Expand Up @@ -71,6 +72,8 @@ func NewCondition(config *Config) (Condition, error) {
condition, err = NewRangeCondition(config.Range.fields)
case config.HasFields != nil:
condition = NewHasFieldsCondition(config.HasFields)
case config.Network != nil:
condition, err = NewNetworkCondition(config.Network)
case len(config.OR) > 0:
var conditionsList []Condition
conditionsList, err = NewConditionList(config.OR)
Expand Down
6 changes: 4 additions & 2 deletions libbeat/conditions/conditions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@ var httpResponseTestEvent = &beat.Event{
}

func testConfig(t *testing.T, expected bool, event *beat.Event, config *Config) {
t.Helper()
logp.TestingSetup()
cond, err := NewCondition(config)
assert.Nil(t, err)
assert.Equal(t, expected, cond.Check(event))
if assert.NoError(t, err) {
assert.Equal(t, expected, cond.Check(event))
}
}

func TestCombinedCondition(t *testing.T) {
Expand Down
263 changes: 263 additions & 0 deletions libbeat/conditions/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package conditions

import (
"fmt"
"net"
"strings"

"github.com/pkg/errors"

"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/libbeat/logp"
)

var (
// RFC 1918
privateIPv4 = []net.IPNet{
{IP: net.IPv4(10, 0, 0, 0), Mask: net.IPv4Mask(255, 0, 0, 0)},
{IP: net.IPv4(172, 16, 0, 0), Mask: net.IPv4Mask(255, 240, 0, 0)},
{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 0, 0)},
}

// RFC 4193
privateIPv6 = net.IPNet{
IP: net.IP{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Mask: net.IPMask{0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
}

namedNetworks = map[string]netContainsFunc{
"loopback": func(ip net.IP) bool { return ip.IsLoopback() },
"global_unicast": func(ip net.IP) bool { return ip.IsGlobalUnicast() },
"unicast": func(ip net.IP) bool { return ip.IsGlobalUnicast() },
"link_local_unicast": func(ip net.IP) bool { return ip.IsLinkLocalUnicast() },
"interface_local_multicast": func(ip net.IP) bool { return ip.IsInterfaceLocalMulticast() },
"link_local_multicast": func(ip net.IP) bool { return ip.IsLinkLocalMulticast() },
"multicast": func(ip net.IP) bool { return ip.IsMulticast() },
"unspecified": func(ip net.IP) bool { return ip.IsUnspecified() },
"private": isPrivateNetwork,
"public": func(ip net.IP) bool { return !isLocalOrPrivate(ip) },
}
)

// Network is a condition that tests if an IP address is in a network range.
type Network struct {
fields map[string]networkMatcher
log *logp.Logger
}

type networkMatcher interface {
fmt.Stringer
Contains(net.IP) bool
}

type netContainsFunc func(net.IP) bool

type singleNetworkMatcher struct {
name string
netContainsFunc
}

func (m singleNetworkMatcher) Contains(ip net.IP) bool { return m.netContainsFunc(ip) }
func (m singleNetworkMatcher) String() string { return m.name }

type multiNetworkMatcher []networkMatcher

func (m multiNetworkMatcher) Contains(ip net.IP) bool {
for _, network := range m {
if network.Contains(ip) {
return true
}
}
return false
}

func (m multiNetworkMatcher) String() string {
var names []string
for _, network := range m {
names = append(names, network.String())
}
return strings.Join(names, " OR ")
}

// 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 common.MapStr(fields).Flatten() {
switch v := value.(type) {
case string:
m, err := makeMatcher(v)
if err != nil {
return nil, err
}
cond.fields[field] = m
case []interface{}:
var matchers multiNetworkMatcher
for _, networkIfc := range v {
network, ok := networkIfc.(string)
if !ok {
return nil, invalidTypeError(field, networkIfc)
}
m, err := makeMatcher(network)
if err != nil {
return nil, err
}
matchers = append(matchers, m)
}
cond.fields[field] = matchers
default:
return nil, invalidTypeError(field, value)
}
}

return cond, nil
}

// Check determines whether the given event matches this condition.
func (c *Network) Check(event ValuesMap) bool {
for field, network := range c.fields {
value, err := event.GetValue(field)
if err != nil {
return false
}

ip := extractIP(value)
if ip == nil {
c.log.Debugf("Invalid IP address in field=%v for network condition", field)
return false
}

if !network.Contains(ip) {
return false
}
}

return true
}

// String returns a string representation of the Network condition.
func (c *Network) String() string {
var sb strings.Builder
sb.WriteString("network:(")
var i int
for field, network := range c.fields {
sb.WriteString(field)
sb.WriteString(":")
sb.WriteString(network.String())
if i < len(c.fields)-1 {
sb.WriteString(" AND ")
}
i++
}
sb.WriteString(")")
return sb.String()
}

// parseCIDR parses a network CIDR.
func parseCIDR(value string) (*net.IPNet, error) {
_, mask, err := net.ParseCIDR(value)
return mask, errors.Wrap(err, "failed to parse CIDR, values must be "+
"an IP address and prefix length, like '192.0.2.0/24' or "+
"'2001:db8::/32', as defined in RFC 4632 and RFC 4291.")
}

// 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 {
switch v := unk.(type) {
case string:
return net.ParseIP(v)
case net.IP:
return v
default:
return nil
}
}

func isPrivateNetwork(ip net.IP) bool {
for _, net := range privateIPv4 {
if net.Contains(ip) {
return true
}
}

return privateIPv6.Contains(ip)
}

func isLocalOrPrivate(ip net.IP) bool {
return isPrivateNetwork(ip) ||
ip.IsLoopback() ||
ip.IsUnspecified() ||
ip.Equal(net.IPv4bcast) ||
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() ||
ip.IsInterfaceLocalMulticast()
}

// NetworkContains returns true if the given IP is contained by any of the
// networks. networks can be a CIDR or any of these named networks:
// - loopback
// - global_unicast
// - unicast
// - link_local_unicast
// - interface_local_multicast
// - link_local_multicast
// - multicast
// - unspecified
// - private
// - public
func NetworkContains(ip net.IP, networks ...string) (bool, error) {
for _, net := range networks {
contains, found := namedNetworks[net]
if !found {
subnet, err := parseCIDR(net)
if err != nil {
return false, err
}
contains = subnet.Contains
}

if contains(ip) {
return true, nil
}
}
return false, nil
}
Loading