diff --git a/Cargo.lock b/Cargo.lock index 1f44e35..f8894d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -657,6 +657,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1177,6 +1183,7 @@ dependencies = [ "dashmap", "data-encoding", "globset", + "ipnet", "jsonschema", "lazy_static", "mimalloc", diff --git a/Cargo.toml b/Cargo.toml index bdaef3d..54c3d8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ glob = ["dep:globset"] graph = [] jsonschema = ["dep:jsonschema"] mimalloc = ["dep:mimalloc"] -net = [] +net = ["dep:ipnet"] no_std = ["lazy_static/spin_no_std"] opa-runtime = [] regex = ["dep:regex"] @@ -108,6 +108,7 @@ uuid = { version = "1.15.1", default-features = false, features = ["v4", "fast-r jsonschema = { version = "0.30.0", default-features = false, optional = true } chrono = { version = "0.4.40", optional = true } chrono-tz = { version = "0.10.1", optional = true } +ipnet = { version = "2.11.0", optional = true, default-features = false } serde_yaml = {version = "0.9.16", default-features = false, optional = true } # Specify thread_rng for in order to use random_range diff --git a/README.md b/README.md index 6aae5f2..2815e28 100644 --- a/README.md +++ b/README.md @@ -303,11 +303,9 @@ The following test suites don't pass fully due to missing builtins: - `jwtverifyhs384` - `jwtverifyhs512` - `jwtverifyrsa` -- `netcidrcontains` - `netcidrcontainsmatches` - `netcidrexpand` - `netcidrintersects` -- `netcidrisvalid` - `netcidrmerge` - `netcidroverlap` - `netlookupipaddr` diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock index d224cee..ad0642b 100644 --- a/bindings/ffi/Cargo.lock +++ b/bindings/ffi/Cargo.lock @@ -546,6 +546,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -960,6 +966,7 @@ dependencies = [ "dashmap", "data-encoding", "globset", + "ipnet", "jsonschema", "lazy_static", "mimalloc", diff --git a/bindings/java/Cargo.lock b/bindings/java/Cargo.lock index 2b9e579..ba41709 100644 --- a/bindings/java/Cargo.lock +++ b/bindings/java/Cargo.lock @@ -418,6 +418,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "itoa" version = "1.0.15" @@ -835,6 +841,7 @@ dependencies = [ "chrono-tz", "data-encoding", "globset", + "ipnet", "jsonschema", "lazy_static", "mimalloc", diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock index 5723206..51d186f 100644 --- a/bindings/python/Cargo.lock +++ b/bindings/python/Cargo.lock @@ -408,6 +408,12 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "itoa" version = "1.0.15" @@ -891,6 +897,7 @@ dependencies = [ "chrono-tz", "data-encoding", "globset", + "ipnet", "jsonschema", "lazy_static", "mimalloc", diff --git a/bindings/ruby/Cargo.lock b/bindings/ruby/Cargo.lock index 652ea4f..e814903 100644 --- a/bindings/ruby/Cargo.lock +++ b/bindings/ruby/Cargo.lock @@ -448,6 +448,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "itertools" version = "0.12.1" @@ -937,6 +943,7 @@ dependencies = [ "chrono-tz", "data-encoding", "globset", + "ipnet", "jsonschema", "lazy_static", "mimalloc", diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock index 3be7b8a..e4e450d 100644 --- a/bindings/wasm/Cargo.lock +++ b/bindings/wasm/Cargo.lock @@ -411,6 +411,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "itoa" version = "1.0.15" @@ -816,6 +822,7 @@ dependencies = [ "chrono-tz", "data-encoding", "globset", + "ipnet", "jsonschema", "lazy_static", "mimalloc", diff --git a/src/builtins/net.rs b/src/builtins/net.rs index 874e281..4165324 100644 --- a/src/builtins/net.rs +++ b/src/builtins/net.rs @@ -1,4 +1,6 @@ use core::net::IpAddr; +use ipnet::IpNet; +use std::format; use std::sync::Arc; use crate::ast::{Expr, Ref}; @@ -7,12 +9,13 @@ use crate::builtins::utils::ensure_args_count; use crate::lexer::Span; use crate::value::Value; -use anyhow::Result; +use anyhow::{anyhow, bail, Result}; 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)); } /// Checks if a CIDR string is valid or invalid. Uses the @@ -59,6 +62,50 @@ fn is_valid_cidr(cidr: Arc) -> bool { } } +pub fn cidr_contains( + span: &Span, + params: &[Ref], + args: &[Value], + strict: bool, +) -> Result { + ensure_args_count(span, "cidr_contains", params, args, 2)?; + let cidr = ensure_string("cidr_contains", ¶ms[0], &args[0])?; + let cidr_or_ip = ensure_string("cidr_contains", ¶ms[1], &args[1])?; + let contains = _cidr_contains(cidr, cidr_or_ip); + + match contains { + Ok(r) => Ok(Value::from(r)), + // The rego implementation will retur an error in strict mode, see + // https://github.com/open-policy-agent/opa/blob/main/v1/test/cases/testdata/v1/netcidrcontains/test-netcidrcontains-0100.yaml + // as an example, so we will propagate the error if the builtin is + // run in strict mode. + Err(e) if strict => bail!(span.error(&format!("{e}"))), + // If not in strict mode, an error will result in Undefined. + _ => Ok(Value::Undefined), + } +} + +fn _cidr_contains(cidr: Arc, cidr_or_ip: Arc) -> Result { + let net = cidr + .parse::() + .map_err(|e| anyhow!("Error parsing {cidr}: {e}"))?; + + if cidr_or_ip.contains("/") { + let subnet = cidr_or_ip + .parse::() + .map_err(|e| anyhow!("Error parsing {cidr_or_ip} as CIDR: {e}"))?; + + return Ok(net.contains(&subnet)); + } + + // if the caller did not provide a CIDR string, try to parse + // the input as an IP address. + let subnet = cidr_or_ip + .parse::() + .map_err(|e| anyhow!("Error parsing {cidr_or_ip} as IP address: {e}"))?; + Ok(net.contains(&subnet)) +} + #[cfg(test)] mod net_tests { use super::*; @@ -83,4 +130,58 @@ mod net_tests { ); } } + + #[test] + fn test_cidr_contains() { + let test_cases: std::vec::IntoIter<(Arc, Arc, bool, bool)> = Vec::from([ + // Each case is a tuple of (cidr, cidr_or_ip, expected Ok(result), and expected error) + ( + Arc::from("127.0.0.1/32"), + Arc::from("127.0.0.1"), + true, + false, + ), + ( + Arc::from("10.0.0.0/8"), + Arc::from("10.10.10.10"), + true, + false, + ), + ( + Arc::from("10.0.0.0/8"), + Arc::from("10.10.10.0/24"), + true, + false, + ), + (Arc::from("fd00::/16"), Arc::from("fd00::/17"), true, false), + ( + Arc::from("127.0.0.1/32"), + Arc::from("127.0.0.2"), + false, + false, + ), + (Arc::from("10.0.0.0/8"), Arc::from("11.0.0.1"), false, false), + (Arc::from("fd00::/16"), Arc::from("fd00::/15"), false, false), + ( + Arc::from("127.0.0.0/8"), + Arc::from("not a cidr"), + false, + true, + ), + ]) + .into_iter(); + + for (cidr, sub, result, should_err) in test_cases { + let got = _cidr_contains(cidr.clone(), sub.clone()); + match got { + Err(_) if should_err => continue, + Ok(res) if res == result => continue, + _ => { + panic!( + "Expected `cidr_contains` for cidr {cidr} and subnet {sub} to be {result}" + ) + } + } + } + } } diff --git a/tests/opa.passing b/tests/opa.passing index b81fb71..6500da2 100644 --- a/tests/opa.passing +++ b/tests/opa.passing @@ -46,6 +46,7 @@ v0/jsonschema v0/negation v0/nestedreferences v0/netcidrisvalid +v0/netcidrcontains v0/numbersrange v0/numbersrangestep v0/objectfilter @@ -150,6 +151,7 @@ v1/jsonremoveidempotent v1/jsonschema v1/negation v1/nestedreferences +v1/netcidrcontains v1/netcidrisvalid v1/numbersrange v1/numbersrangestep @@ -206,4 +208,4 @@ v1/uuid v1/varreferences v1/virtualdocs v1/walkbuiltin -v1/withkeyword \ No newline at end of file +v1/withkeyword