diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md
index c3398539fd..a56d3d8dbe 100644
--- a/docs/content/policy-reference.md
+++ b/docs/content/policy-reference.md
@@ -1015,7 +1015,7 @@ The table below shows examples of calling `http.send`:
| ``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. | ``SDK-dependent`` |
| ``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"}` | ``SDK-dependent`` |
-| ``net.cidr_merge(cidrs_or_ips)`` | `output` is the smallest possible set of CIDRs obtained after merging the provided list of IP addresses and subnets in `cidrs_or_ips` (e.g., `net.cidr_merge(["192.0.128.0/24", "192.0.129.0/24"])` generates `{"192.0.128.0/23"}`. This function merges adjacent subnets where possible, those contained within others and also removes any duplicates. Supports both IPv4 and IPv6 notations. | ``SDK-dependent`` |
+| ``net.cidr_merge(cidrs_or_ips)`` | `output` is the smallest possible set of CIDRs obtained after merging the provided list of IP addresses and subnets in `cidrs_or_ips` (e.g., `net.cidr_merge(["192.0.128.0/24", "192.0.129.0/24"])` generates `{"192.0.128.0/23"}`. This function merges adjacent subnets where possible, those contained within others and also removes any duplicates. Supports both IPv4 and IPv6 notations. IPv6 inputs need a prefix length (e.g. "/128"). | ``SDK-dependent`` |
#### Notes on Name Resolution (`net.lookup_ip_addr`)
diff --git a/test/cases/testdata/netcidrmerge/test-ipv6-with-and-without-prefix.yaml b/test/cases/testdata/netcidrmerge/test-ipv6-with-and-without-prefix.yaml
new file mode 100644
index 0000000000..8642fc6e0a
--- /dev/null
+++ b/test/cases/testdata/netcidrmerge/test-ipv6-with-and-without-prefix.yaml
@@ -0,0 +1,59 @@
+cases:
+- note: netcidrmerge/cidr ipv6 with prefix
+ modules:
+ - |
+ package test
+
+ p = x {
+ net.cidr_merge(["2601:600:8a80:207e:a57d:7567:e2c9:e7b3/128"], x)
+ }
+ query: data.test.p = x
+ want_result:
+ - x:
+ - "2601:600:8a80:207e:a57d:7567:e2c9:e7b3/128"
+- note: netcidrmerge/cidr ipv6 with prefix, same twice
+ modules:
+ - |
+ package test
+
+ p = x {
+ net.cidr_merge(["2601:600:8a80:207e:a57d:7567:e2c9:e7b3/128", "2601:600:8a80:207e:a57d:7567:e2c9:e7b3/128"], x)
+ }
+ query: data.test.p = x
+ want_result:
+ - x:
+ - "2601:600:8a80:207e:a57d:7567:e2c9:e7b3/128"
+- note: netcidrmerge/cidr ipv6 with prefix, two different prefixes
+ modules:
+ - |
+ package test
+
+ p = x {
+ net.cidr_merge(["2601:600:8a80:207e:a57d:7567:e2c9:e7b3/64", "2601:600:8a80:207e:a57d:7567:e2c9:e7b3/128"], x)
+ }
+ query: data.test.p = x
+ want_result:
+ - x:
+ - "2601:600:8a80:207e::/64"
+- note: netcidrmerge/cidr ipv6 without prefix
+ modules:
+ - |
+ package test
+
+ p = x {
+ net.cidr_merge(["2601:600:8a80:207e:a57d:7567:e2c9:e7b3"], x)
+ }
+ query: data.test.p = x
+ strict_error: true
+ want_error: "eval_builtin_error: net.cidr_merge: IPv6 invalid: needs prefix length"
+- note: netcidrmerge/cidr ipv6 without prefix, same twice
+ modules:
+ - |
+ package test
+
+ p = x {
+ net.cidr_merge(["2601:600:8a80:207e:a57d:7567:e2c9:e7b3", "2601:600:8a80:207e:a57d:7567:e2c9:e7b3"], x)
+ }
+ query: data.test.p = x
+ strict_error: true
+ want_error: "eval_builtin_error: net.cidr_merge: IPv6 invalid: needs prefix length"
\ No newline at end of file
diff --git a/topdown/cidr.go b/topdown/cidr.go
index 2123698c7c..e2dbfed479 100644
--- a/topdown/cidr.go
+++ b/topdown/cidr.go
@@ -73,7 +73,7 @@ func builtinNetCIDRIntersects(a, b ast.Value) (ast.Value, error) {
}
// If either net contains the others starting IP they are overlapping
- cidrsOverlap := (cidrnetA.Contains(cidrnetB.IP) || cidrnetB.Contains(cidrnetA.IP))
+ cidrsOverlap := cidrnetA.Contains(cidrnetB.IP) || cidrnetB.Contains(cidrnetA.IP)
return ast.Boolean(cidrsOverlap), nil
}
@@ -332,25 +332,25 @@ func evalNetCIDRMerge(networks []*net.IPNet) []*net.IPNet {
}
func generateIPNet(term *ast.Term) (*net.IPNet, error) {
- switch e := term.Value.(type) {
- case ast.String:
- network := &net.IPNet{}
- // try to parse element as an IP first, fall back to CIDR
- ip := net.ParseIP(string(e))
- if ip != nil {
- network.IP = ip
- network.Mask = ip.DefaultMask()
- } else {
- var err error
- _, network, err = net.ParseCIDR(string(e))
- if err != nil {
- return nil, err
- }
- }
- return network, nil
- default:
+ e, ok := term.Value.(ast.String)
+ if !ok {
return nil, errors.New("element must be string")
}
+
+ // try to parse element as an IP first, fall back to CIDR
+ ip := net.ParseIP(string(e))
+ if ip == nil {
+ _, network, err := net.ParseCIDR(string(e))
+ return network, err
+ }
+
+ if ip.To4() != nil {
+ return &net.IPNet{
+ IP: ip,
+ Mask: ip.DefaultMask(),
+ }, nil
+ }
+ return nil, errors.New("IPv6 invalid: needs prefix length")
}
func mergeCIDRs(ranges cidrBlockRanges) cidrBlockRanges {