feat: implement net.cidr_expand builtin

Signed-off-by: tjons <tylerschade99@gmail.com>
This commit is contained in:
tjons
2025-12-16 06:05:32 -05:00
parent 2a75b3b0b6
commit c41f289b19
3 changed files with 59 additions and 2 deletions

View File

@@ -304,7 +304,6 @@ The following test suites don't pass fully due to missing builtins:
- `jwtverifyhs512` - `jwtverifyhs512`
- `jwtverifyrsa` - `jwtverifyrsa`
- `netcidrcontainsmatches` - `netcidrcontainsmatches`
- `netcidrexpand`
- `netcidrintersects` - `netcidrintersects`
- `netcidrmerge` - `netcidrmerge`
- `netcidroverlap` - `netcidroverlap`

View File

@@ -1,7 +1,9 @@
use core::net::IpAddr; use core::net::IpAddr;
use ipnet::IpNet; use ipnet::IpNet;
use std::format; use std::format;
use std::string::ToString;
use std::sync::Arc; use std::sync::Arc;
use std::vec::Vec;
use crate::ast::{Expr, Ref}; use crate::ast::{Expr, Ref};
use crate::builtins; use crate::builtins;
@@ -16,6 +18,7 @@ use super::utils::ensure_string;
pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) { pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("net.cidr_is_valid", (cidr_is_valid, 1)); m.insert("net.cidr_is_valid", (cidr_is_valid, 1));
m.insert("net.cidr_contains", (cidr_contains, 2)); m.insert("net.cidr_contains", (cidr_contains, 2));
m.insert("net.cidr_expand", (cidr_expand, 1));
} }
/// Checks if a CIDR string is valid or invalid. Uses the /// Checks if a CIDR string is valid or invalid. Uses the
@@ -62,6 +65,7 @@ fn is_valid_cidr(cidr: Arc<str>) -> bool {
} }
} }
/// Checks if a CIDR string contains a given CIDR or individual network address.
pub fn cidr_contains( pub fn cidr_contains(
span: &Span, span: &Span,
params: &[Ref<Expr>], params: &[Ref<Expr>],
@@ -106,10 +110,42 @@ fn _cidr_contains(cidr: Arc<str>, cidr_or_ip: Arc<str>) -> Result<bool> {
Ok(net.contains(&subnet)) Ok(net.contains(&subnet))
} }
pub fn cidr_expand(
span: &Span,
params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
ensure_args_count(span, "cidr_expand", params, args, 1)?;
let cidr = ensure_string("cidr_expand", &params[0], &args[0])?;
_cidr_expand(cidr)
}
fn _cidr_expand(cidr: Arc<str>) -> Result<Value> {
let net = cidr
.parse::<IpNet>()
.map_err(|e| anyhow!("Error parsing {cidr}: {e}"))?;
let mut hosts: Vec<Value> = net
.hosts()
.map(|h| Value::String(h.to_string().into()))
.collect();
// the IpNet library has some different behavior regarding CIDR expansion from the go implementation
// that OPA uses; it will exclude the IPv4 CIDR network address and broadcast address when the netmask < 31.
// Adjust accordingly for parity.
if matches!(net, IpNet::V4(_) if net.prefix_len() < 31) {
hosts.push(net.broadcast().to_string().into());
hosts.insert(0, net.network().to_string().into());
}
Ok(Value::Array(Arc::from(hosts)))
}
#[cfg(test)] #[cfg(test)]
mod net_tests { mod net_tests {
use super::*; use super::*;
use std::vec::Vec;
#[test] #[test]
fn test_cidr_is_valid() { fn test_cidr_is_valid() {
@@ -184,4 +220,24 @@ mod net_tests {
} }
} }
} }
#[test]
fn test_cidr_expand() {
let cases = Vec::from([
("127.0.0.1/32", Vec::from(["127.0.0.1"])),
(
"10.0.0.0/29",
Vec::from([
"10.0.0.0", "10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4", "10.0.0.5",
"10.0.0.6", "10.0.0.7",
]),
),
]);
for (cidr, exp) in cases {
let cidrs = _cidr_expand(cidr.into()).expect("CIDRs should be returned");
let expv = Value::from(exp.iter().map(|s| Value::from(*s)).collect::<Vec<Value>>());
assert_eq!(cidrs, expv);
}
}
} }

View File

@@ -47,6 +47,7 @@ v0/negation
v0/nestedreferences v0/nestedreferences
v0/netcidrisvalid v0/netcidrisvalid
v0/netcidrcontains v0/netcidrcontains
v0/netcidrexpand
v0/numbersrange v0/numbersrange
v0/numbersrangestep v0/numbersrangestep
v0/objectfilter v0/objectfilter
@@ -153,6 +154,7 @@ v1/negation
v1/nestedreferences v1/nestedreferences
v1/netcidrcontains v1/netcidrcontains
v1/netcidrisvalid v1/netcidrisvalid
v1/netcidrexpand
v1/numbersrange v1/numbersrange
v1/numbersrangestep v1/numbersrangestep
v1/objectfilter v1/objectfilter