sentinel_modsec/operators/
network.rs

1//! Network operators (@ipMatch).
2
3use super::traits::{Operator, OperatorResult};
4use crate::error::{Error, Result};
5use ipnetwork::IpNetwork;
6use std::net::IpAddr;
7
8/// IP match operator (@ipMatch).
9pub struct IpMatchOperator {
10    networks: Vec<IpNetwork>,
11}
12
13impl IpMatchOperator {
14    /// Create from space-separated IP/CIDR list.
15    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                // Handle bare IPs (add /32 or /128)
22                if s.contains('/') {
23                    s.parse::<IpNetwork>()
24                } else {
25                    // Try as IP address first
26                    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    /// Create from a file containing IPs/CIDRs.
50    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    /// Check if an IP is in any of the networks.
88    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}