sentinel_modsec/operators/
network.rs1use super::traits::{Operator, OperatorResult};
4use crate::error::{Error, Result};
5use ipnetwork::IpNetwork;
6use std::net::IpAddr;
7
8pub struct IpMatchOperator {
10 networks: Vec<IpNetwork>,
11}
12
13impl IpMatchOperator {
14 pub fn new(ips: &str) -> Result<Self> {
16 let networks = ips
17 .split_whitespace()
18 .map(|s| s.trim())
19 .filter(|s| !s.is_empty())
20 .map(|s| {
21 if s.contains('/') {
23 s.parse::<IpNetwork>()
24 } else {
25 if let Ok(ip) = s.parse::<IpAddr>() {
27 match ip {
28 IpAddr::V4(v4) => Ok(IpNetwork::V4(
29 ipnetwork::Ipv4Network::new(v4, 32).unwrap(),
30 )),
31 IpAddr::V6(v6) => Ok(IpNetwork::V6(
32 ipnetwork::Ipv6Network::new(v6, 128).unwrap(),
33 )),
34 }
35 } else {
36 s.parse::<IpNetwork>()
37 }
38 }
39 })
40 .collect::<std::result::Result<Vec<_>, _>>()
41 .map_err(|e| Error::InvalidIp {
42 value: ips.to_string(),
43 message: e.to_string(),
44 })?;
45
46 Ok(Self { networks })
47 }
48
49 pub fn from_file(path: &str) -> Result<Self> {
51 let content = std::fs::read_to_string(path).map_err(|e| Error::RuleFileLoad {
52 path: path.into(),
53 source: e,
54 })?;
55
56 let networks = content
57 .lines()
58 .map(|l| l.trim())
59 .filter(|l| !l.is_empty() && !l.starts_with('#'))
60 .map(|s| {
61 if s.contains('/') {
62 s.parse::<IpNetwork>()
63 } else {
64 if let Ok(ip) = s.parse::<IpAddr>() {
65 match ip {
66 IpAddr::V4(v4) => Ok(IpNetwork::V4(
67 ipnetwork::Ipv4Network::new(v4, 32).unwrap(),
68 )),
69 IpAddr::V6(v6) => Ok(IpNetwork::V6(
70 ipnetwork::Ipv6Network::new(v6, 128).unwrap(),
71 )),
72 }
73 } else {
74 s.parse::<IpNetwork>()
75 }
76 }
77 })
78 .collect::<std::result::Result<Vec<_>, _>>()
79 .map_err(|e| Error::InvalidIp {
80 value: path.to_string(),
81 message: e.to_string(),
82 })?;
83
84 Ok(Self { networks })
85 }
86
87 fn contains(&self, ip: &IpAddr) -> bool {
89 self.networks.iter().any(|net| net.contains(*ip))
90 }
91}
92
93impl Operator for IpMatchOperator {
94 fn execute(&self, value: &str) -> OperatorResult {
95 if let Ok(ip) = value.parse::<IpAddr>() {
96 if self.contains(&ip) {
97 return OperatorResult::matched(value.to_string());
98 }
99 }
100 OperatorResult::no_match()
101 }
102
103 fn name(&self) -> &'static str {
104 "ipMatch"
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_ip_match_single() {
114 let op = IpMatchOperator::new("192.168.1.1").unwrap();
115 assert!(op.execute("192.168.1.1").matched);
116 assert!(!op.execute("192.168.1.2").matched);
117 }
118
119 #[test]
120 fn test_ip_match_cidr() {
121 let op = IpMatchOperator::new("192.168.1.0/24").unwrap();
122 assert!(op.execute("192.168.1.1").matched);
123 assert!(op.execute("192.168.1.255").matched);
124 assert!(!op.execute("192.168.2.1").matched);
125 }
126
127 #[test]
128 fn test_ip_match_multiple() {
129 let op = IpMatchOperator::new("10.0.0.0/8 192.168.0.0/16").unwrap();
130 assert!(op.execute("10.1.2.3").matched);
131 assert!(op.execute("192.168.1.1").matched);
132 assert!(!op.execute("172.16.0.1").matched);
133 }
134}