diff --git a/ast/builtins.go b/ast/builtins.go
index 1ab7dc0a93..1531183ae1 100644
--- a/ast/builtins.go
+++ b/ast/builtins.go
@@ -200,6 +200,7 @@ var DefaultBuiltins = [...]*Builtin{
NetCIDROverlap,
NetCIDRIntersects,
NetCIDRContains,
+ NetCIDRContainsMatches,
NetCIDRExpand,
// Glob
@@ -1745,6 +1746,34 @@ var NetCIDRContains = &Builtin{
),
}
+// NetCIDRContainsMatches checks if collections of cidrs or ips are contained within another collection of cidrs and returns matches.
+var NetCIDRContainsMatches = &Builtin{
+ Name: "net.cidr_contains_matches",
+ Decl: types.NewFunction(
+ types.Args(netCidrContainsMatchesOperandType, netCidrContainsMatchesOperandType),
+ types.NewSet(types.NewArray([]types.Type{types.A, types.A}, nil)),
+ ),
+}
+
+var netCidrContainsMatchesOperandType = types.NewAny(
+ types.S,
+ types.NewArray(nil, types.NewAny(
+ types.S,
+ types.NewArray(nil, types.A),
+ )),
+ types.NewSet(types.NewAny(
+ types.S,
+ types.NewArray(nil, types.A),
+ )),
+ types.NewObject(nil, types.NewDynamicProperty(
+ types.S,
+ types.NewAny(
+ types.S,
+ types.NewArray(nil, types.A),
+ ),
+ )),
+)
+
/**
* Deprecated built-ins.
*/
diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md
index f99de40681..1997c13205 100644
--- a/docs/content/policy-reference.md
+++ b/docs/content/policy-reference.md
@@ -74,9 +74,9 @@ complex types.
| `output := object.union(objectA, objectB)` | `output` is a new object which is the result of an asymmetric recursive union of two objects where conflicts are resolved by choosing the key from the right-hand object (`objectB`). For example: `object.union({"a": 1, "b": 2, "c": {"d": 3}}, {"a": 7, "c": {"d": 4, "e": 5}})` will result in `{"a": 7, "b": 2, "c": {"d": 4, "e": 5}}` |
| `filtered := object.filter(object, keys)` | `filtered` is a new object with the remaining data from `object` with only keys specified in `keys` which is an array, object, or set of keys. For example: `object.filter({"a": {"b": "x", "c": "y"}, "d": "z"}, ["a"])` will result in `{"a": {"b": "x", "c": "y"}}`). |
| `filtered := json.filter(object, paths)` | `filtered` is the remaining data from `object` with only keys specified in `paths` which is an array or set of JSON string paths. For example: `json.filter({"a": {"b": "x", "c": "y"}}, ["a/b"])` will result in `{"a": {"b": "x"}}`). Paths are not filtered in-order and are deduplicated before being evaluated. |
-| `output := json.remove(object, paths)` | `output` is a new object which is the result of removing all keys specified in `paths` which is an array or set of JSON string paths. For example: `json.remove{"a": {"b": "x", "c": "y"}, ["a/b"]}` will result in `{"a": {"c": "y"}}`. Paths are not removed in-order and are deduplicated before being evaluated. |
+| `output := json.remove(object, paths)` | `output` is a new object which is the result of removing all keys specified in `paths` which is an array or set of JSON string paths. For example: `json.remove{"a": {"b": "x", "c": "y"}, ["a/b"]}` will result in `{"a": {"c": "y"}}`. Paths are not removed in-order and are deduplicated before being evaluated. |
-* When `keys` are provided as an object only the top level keys on the object will be used, values are ignored.
+* When `keys` are provided as an object only the top level keys on the object will be used, values are ignored.
For example: `object.remove({"a": {"b": {"c": 2}}, "x": 123}, {"a": 1}) == {"x": 123}` regardless of the value
for key `a` in the keys object, the following `keys` object gives the same result
`object.remove({"a": {"b": {"c": 2}}, "x": 123}, {"a": {"b": {"foo": "bar"}}}) == {"x": 123}`.
@@ -604,9 +604,62 @@ The table below shows examples of calling `http.send`:
| Built-in | Description |
| ------- |-------------|
| ``net.cidr_contains(cidr, cidr_or_ip)`` | `output` is `true` if `cidr_or_ip` (e.g. `127.0.0.64/26` or `127.0.0.1`) is contained within `cidr` (e.g. `127.0.0.1/24`) and false otherwise. Supports both IPv4 and IPv6 notations.|
+| ``output := net.cidr_contains_matches(cidrs, cidrs_or_ips)`` | `output` is a `set` of tuples identifying matches where `cidrs_or_ips` are contained within `cidrs`. This function is similar to `net.cidr_contains` except it allows callers to pass collections of CIDRs or IPs as arguments and returns the matches (as opposed to a boolean result indicating a match between two CIDRs/IPs.) See below for examples. |
| ``net.cidr_intersects(cidr1, cidr2)`` | `output` is `true` if `cidr1` (e.g. `192.168.0.0/16`) overlaps with `cidr2` (e.g. `192.168.1.0/24`) and false otherwise. Supports both IPv4 and IPv6 notations.|
| ``net.cidr_expand(cidr)`` | `output` is the set of hosts in `cidr` (e.g., `net.cidr_expand("192.168.0.0/30")` generates 4 hosts: `{"192.168.0.0", "192.168.0.1", "192.168.0.2", "192.168.0.3"}` |
+**`net.cidr_contains_matches` examples**
+
+The `output := net.cidr_contains_matches(a, b)` function allows callers to supply
+strings, arrays, sets, or objects for either `a` or `b`. The `output` value in
+all cases is a set of tuples (2-element arrays) that identify matches, i.e.,
+elements of `b` contained by elements of `a`. The first tuple element refers to
+the match in `a` and the second tuple element refers to the match in `b`.
+
+| Input Type | Output Type |
+| --- | --- |
+| `string` | `string` |
+| `array` | `array` index |
+| `set` | `set` element |
+| `object` | `object` key |
+
+
+```live:netcidrcontainsmatches:module:hidden
+package netcidrcontainsmatches
+```
+
+If both operands are string values the function is similar to `net.cidr_contains`.
+
+```live:netcidrcontainsmatches/strings:query:merge_down
+net.cidr_contains_matches("1.1.1.0/24", "1.1.1.128")
+```
+```live:netcidrcontainsmatches/strings:output
+```
+
+Either (or both) operand(s) may be an array, set, or object.
+
+```live:netcidrcontainsmatches/array:query:merge_down
+net.cidr_contains_matches(["1.1.1.0/24", "1.1.2.0/24"], "1.1.1.128")
+```
+```live:netcidrcontainsmatches/array:output
+```
+
+The array/set/object elements may be arrays. In that case, the first element must be a valid CIDR/IP.
+
+```live:netcidrcontainsmatches/tuples:query:merge_down
+net.cidr_contains_matches([["1.1.0.0/16", "foo"], "1.1.2.0/24"], ["1.1.1.128", ["1.1.254.254", "bar"]])
+```
+```live:netcidrcontainsmatches/tuples:output
+```
+
+If the operand is a set, the outputs are matching elements. If the operand is an object, the outputs are matching keys.
+
+```live:netcidrcontainsmatches/sets_and_objects:query:merge_down
+net.cidr_contains_matches({["1.1.0.0/16", "foo"], "1.1.2.0/24"}, {"x": "1.1.1.128", "y": ["1.1.254.254", "bar"]})
+```
+```live:netcidrcontainsmatches/sets_and_objects:output:merge_down
+```
+
### Rego
| Built-in | Description |
| ------- |-------------|
diff --git a/topdown/cidr.go b/topdown/cidr.go
index b65628daef..3ad74ea3b6 100644
--- a/topdown/cidr.go
+++ b/topdown/cidr.go
@@ -1,6 +1,7 @@
package topdown
import (
+ "errors"
"fmt"
"math/big"
"net"
@@ -111,6 +112,75 @@ func builtinNetCIDRContains(a, b ast.Value) (ast.Value, error) {
return ast.Boolean(cidrContained), nil
}
+var errNetCIDRContainsMatchElementType = errors.New("element must be string or non-empty array")
+
+func getCIDRMatchTerm(a *ast.Term) (*ast.Term, error) {
+ switch v := a.Value.(type) {
+ case ast.String:
+ return a, nil
+ case ast.Array:
+ if len(v) == 0 {
+ return nil, errNetCIDRContainsMatchElementType
+ }
+ return v[0], nil
+ default:
+ return nil, errNetCIDRContainsMatchElementType
+ }
+}
+
+func evalNetCIDRContainsMatchesOperand(operand int, a *ast.Term, iter func(cidr, index *ast.Term) error) error {
+ switch v := a.Value.(type) {
+ case ast.String:
+ return iter(a, a)
+ case ast.Array:
+ for i := range v {
+ cidr, err := getCIDRMatchTerm(v[i])
+ if err != nil {
+ return fmt.Errorf("operand %v: %v", operand, err)
+ }
+ if err := iter(cidr, ast.IntNumberTerm(i)); err != nil {
+ return err
+ }
+ }
+ return nil
+ case ast.Set:
+ return v.Iter(func(x *ast.Term) error {
+ cidr, err := getCIDRMatchTerm(x)
+ if err != nil {
+ return fmt.Errorf("operand %v: %v", operand, err)
+ }
+ return iter(cidr, x)
+ })
+ case ast.Object:
+ return v.Iter(func(k, v *ast.Term) error {
+ cidr, err := getCIDRMatchTerm(v)
+ if err != nil {
+ return fmt.Errorf("operand %v: %v", operand, err)
+ }
+ return iter(cidr, k)
+ })
+ }
+ return nil
+}
+
+func builtinNetCIDRContainsMatches(bctx BuiltinContext, args []*ast.Term, iter func(*ast.Term) error) error {
+ result := ast.NewSet()
+ err := evalNetCIDRContainsMatchesOperand(1, args[0], func(cidr1 *ast.Term, index1 *ast.Term) error {
+ return evalNetCIDRContainsMatchesOperand(2, args[1], func(cidr2 *ast.Term, index2 *ast.Term) error {
+ if v, err := builtinNetCIDRContains(cidr1.Value, cidr2.Value); err != nil {
+ return err
+ } else if vb, ok := v.(ast.Boolean); ok && bool(vb) {
+ result.Add(ast.ArrayTerm(index1, index2))
+ }
+ return nil
+ })
+ })
+ if err == nil {
+ return iter(ast.NewTerm(result))
+ }
+ return err
+}
+
func builtinNetCIDRExpand(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
s, err := builtins.StringOperand(operands[0].Value, 1)
@@ -153,5 +223,6 @@ func init() {
RegisterFunctionalBuiltin2(ast.NetCIDROverlap.Name, builtinNetCIDRContains)
RegisterFunctionalBuiltin2(ast.NetCIDRIntersects.Name, builtinNetCIDRIntersects)
RegisterFunctionalBuiltin2(ast.NetCIDRContains.Name, builtinNetCIDRContains)
+ RegisterBuiltinFunc(ast.NetCIDRContainsMatches.Name, builtinNetCIDRContainsMatches)
RegisterBuiltinFunc(ast.NetCIDRExpand.Name, builtinNetCIDRExpand)
}
diff --git a/topdown/cidr_test.go b/topdown/cidr_test.go
index 5df63935f4..e8b1877ac0 100644
--- a/topdown/cidr_test.go
+++ b/topdown/cidr_test.go
@@ -75,6 +75,66 @@ func TestNetCIDRContains(t *testing.T) {
}
}
+func TestNetCIDRContainsMatches(t *testing.T) {
+ tests := []struct {
+ note string
+ rules []string
+ expected interface{}
+ }{
+ {
+ note: "strings",
+ rules: []string{`p = x { x := net.cidr_contains_matches("1.1.1.0/24", "1.1.1.1") }`},
+ expected: `[["1.1.1.0/24", "1.1.1.1"]]`,
+ },
+ {
+ note: "arrays",
+ rules: []string{`p = x { x := net.cidr_contains_matches(["1.1.2.0/24", "1.1.1.0/24"], ["1.1.1.1", "1.1.2.1"]) }`},
+ expected: `[[0,1], [1,0]]`,
+ },
+ {
+ note: "arrays of tuples",
+ rules: []string{`p = x { x := net.cidr_contains_matches([["1.1.2.0/24", 1], "1.1.1.0/24"], ["1.1.1.1", "1.1.2.1"]) }`},
+ expected: `[[0,1], [1,0]]`,
+ },
+ {
+ note: "bad array",
+ rules: []string{`p = x { x := net.cidr_contains_matches(["1.1.2.0/24", "1.1.1.0/24"], ["1.1.1.1", data.a[0]]) }`},
+ expected: &Error{Code: BuiltinErr, Message: "net.cidr_contains_matches: operand 2: element must be string or non-empty array"},
+ },
+ {
+ note: "sets of strings",
+ rules: []string{`p = x { x := net.cidr_contains_matches({"1.1.2.0/24", "1.1.1.0/24"}, {"1.1.1.1", "1.1.2.1"}) }`},
+ expected: `[["1.1.1.0/24", "1.1.1.1"], ["1.1.2.0/24", "1.1.2.1"]]`,
+ },
+ {
+ note: "sets of tuples",
+ rules: []string{`p = x { x := net.cidr_contains_matches({["1.1.2.0/24", "foo"], ["1.1.1.0/24", "bar"]}, {["1.1.1.1", "baz"], ["1.1.2.1", "qux"]}) }`},
+ expected: `[[["1.1.1.0/24", "bar"], ["1.1.1.1", "baz"]], [["1.1.2.0/24", "foo"], ["1.1.2.1", "qux"]]]`,
+ },
+ {
+ note: "bad set",
+ rules: []string{`p = x { x := net.cidr_contains_matches({["1.1.2.0/24", "foo"], ["1.1.1.0/24", "bar"]}, {data.a[0], ["1.1.2.1", "qux"]}) }`},
+ expected: &Error{Code: BuiltinErr, Message: `net.cidr_contains_matches: operand 2: element must be string or non-empty array`},
+ },
+ {
+ note: "bad set tuple element",
+ rules: []string{`p = x { x := net.cidr_contains_matches({["1.1.2.0/24", "foo"], ["1.1.1.0/24", "bar"]}, {[], ["1.1.2.1", "qux"]}) }`},
+ expected: &Error{Code: BuiltinErr, Message: `net.cidr_contains_matches: operand 2: element must be string or non-empty array`},
+ },
+ {
+ note: "objects",
+ rules: []string{`p = x { x := net.cidr_contains_matches({"k1": "1.1.1.1/24", "k2": ["1.1.1.2/24", 1]}, "1.1.1.128") }`},
+ expected: `[["k1", "1.1.1.128"], ["k2", "1.1.1.128"]]`,
+ },
+ }
+
+ data := loadSmallTestData()
+
+ for _, tc := range tests {
+ runTopDownTestCase(t, data, tc.note, tc.rules, tc.expected)
+ }
+}
+
func TestNetCIDRExpand(t *testing.T) {
tests := []struct {
note string