diff --git a/README.md b/README.md index 2815e28..96c3698 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,6 @@ The following test suites don't pass fully due to missing builtins: - `jwtverifyhs512` - `jwtverifyrsa` - `netcidrcontainsmatches` -- `netcidrexpand` - `netcidrintersects` - `netcidrmerge` - `netcidroverlap` diff --git a/src/builtins/net.rs b/src/builtins/net.rs index 4165324..c311779 100644 --- a/src/builtins/net.rs +++ b/src/builtins/net.rs @@ -1,7 +1,9 @@ use core::net::IpAddr; use ipnet::IpNet; use std::format; +use std::string::ToString; use std::sync::Arc; +use std::vec::Vec; use crate::ast::{Expr, Ref}; use crate::builtins; @@ -16,6 +18,7 @@ use super::utils::ensure_string; 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_contains", (cidr_contains, 2)); + m.insert("net.cidr_expand", (cidr_expand, 1)); } /// Checks if a CIDR string is valid or invalid. Uses the @@ -62,6 +65,7 @@ fn is_valid_cidr(cidr: Arc) -> bool { } } +/// Checks if a CIDR string contains a given CIDR or individual network address. pub fn cidr_contains( span: &Span, params: &[Ref], @@ -106,10 +110,42 @@ fn _cidr_contains(cidr: Arc, cidr_or_ip: Arc) -> Result { Ok(net.contains(&subnet)) } +pub fn cidr_expand( + span: &Span, + params: &[Ref], + args: &[Value], + _strict: bool, +) -> Result { + ensure_args_count(span, "cidr_expand", params, args, 1)?; + let cidr = ensure_string("cidr_expand", ¶ms[0], &args[0])?; + + _cidr_expand(cidr) +} + +fn _cidr_expand(cidr: Arc) -> Result { + let net = cidr + .parse::() + .map_err(|e| anyhow!("Error parsing {cidr}: {e}"))?; + + let mut hosts: Vec = 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)] mod net_tests { use super::*; - use std::vec::Vec; #[test] 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::>()); + assert_eq!(cidrs, expv); + } + } } diff --git a/tests/opa.passing b/tests/opa.passing index 6500da2..439bef1 100644 --- a/tests/opa.passing +++ b/tests/opa.passing @@ -47,6 +47,7 @@ v0/negation v0/nestedreferences v0/netcidrisvalid v0/netcidrcontains +v0/netcidrexpand v0/numbersrange v0/numbersrangestep v0/objectfilter @@ -153,6 +154,7 @@ v1/negation v1/nestedreferences v1/netcidrcontains v1/netcidrisvalid +v1/netcidrexpand v1/numbersrange v1/numbersrangestep v1/objectfilter