Skip to content

Commit 017eae4

Browse files
authored
Merge pull request #32 from Quad9DNS/feature/cidr-rule
Add CIDR rule
2 parents 19e9f10 + b8ec98a commit 017eae4

File tree

14 files changed

+178
-7
lines changed

14 files changed

+178
-7
lines changed

Cargo.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ criterion = { version = "0.5.1" }
2424
exitcode = { version = "1.1.2" }
2525
futures = { version = "0.3.31" }
2626
hashbrown = { version = "0.16.1" }
27+
ipnet = { version = "2.12.0" }
2728
lazy_static = { version = "1.5.0" }
2829
metrics = { version = "0.24.2" }
2930
metrics-exporter-prometheus = { version = "0.17.0" }

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Stringsimile supports different inputs, outputs and rules to use when comparing
4747
- Match Rating
4848
- Bitflip
4949
- Regex
50+
- CIDR
5051

5152
Check out the [example rules file](./distribution/rules/example.json) to see how they can be defined. You can also check out the included man pages (`man 5 stringsimile-rule-config`).
5253

bin/stringsimile-service/tests/basic_file_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const INPUT_DATA: &[u8] =
2323
"#;
2424

2525
const RULES_DATA: &[u8] = br#"
26-
{ "name": "Example string group", "rule_sets": [ { "name": "Test rule set", "string_match": "test", "preprocessors": [ { "preprocessor_type": "split_target", "ignore_tld": true } ], "match_rules": [ { "rule_type": "levenshtein", "values": { "maximum_distance": 3 } }, { "rule_type": "hamming", "values": { "maximum_distance": 3 } }, { "rule_type": "soundex", "values": { "minimum_similarity": 3 } }, { "rule_type": "metaphone", "values": { "max_code_length": 3 } }, { "rule_type": "nysiis", "values": { "strict": true } }, { "rule_type": "jaro", "values": { "match_percent_threshold": 0.85 } }, { "rule_type": "jaro_winkler", "values": { "match_percent_threshold": 0.85 } }, { "rule_type": "confusables" }, { "rule_type": "match_rating" }, { "rule_type": "damerau_levenshtein", "values": { "maximum_distance": 3 } } ] }, { "name": "Example rule set", "split_target": true, "ignore_tld": true, "string_match": "example", "match_rules": [ { "rule_type": "levenshtein", "values": { "maximum_distance": 3 } }, { "rule_type": "jaro", "values": { "match_percent_threshold": 0.85 } }, { "rule_type": "bitflip" }, { "rule_type": "regex", "values": { "pattern": "test" } } ] } ] }
26+
{ "name": "Example string group", "rule_sets": [ { "name": "Test rule set", "string_match": "test", "preprocessors": [ { "preprocessor_type": "split_target", "ignore_tld": true } ], "match_rules": [ { "rule_type": "levenshtein", "values": { "maximum_distance": 3 } }, { "rule_type": "hamming", "values": { "maximum_distance": 3 } }, { "rule_type": "soundex", "values": { "minimum_similarity": 3 } }, { "rule_type": "metaphone", "values": { "max_code_length": 3 } }, { "rule_type": "nysiis", "values": { "strict": true } }, { "rule_type": "jaro", "values": { "match_percent_threshold": 0.85 } }, { "rule_type": "jaro_winkler", "values": { "match_percent_threshold": 0.85 } }, { "rule_type": "confusables" }, { "rule_type": "match_rating" }, { "rule_type": "damerau_levenshtein", "values": { "maximum_distance": 3 } } ] }, { "name": "Example rule set", "split_target": true, "ignore_tld": true, "string_match": "example", "match_rules": [ { "rule_type": "levenshtein", "values": { "maximum_distance": 3 } }, { "rule_type": "jaro", "values": { "match_percent_threshold": 0.85 } }, { "rule_type": "bitflip" }, { "rule_type": "regex", "values": { "pattern": "test" } }, { "rule_type": "cidr", "values": { "address": "192.168.0.0/24" } } ] } ] }
2727
"#;
2828

2929
#[test]

crates/stringsimile-config/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ version.workspace = true
99
stringsimile-matcher.workspace = true
1010

1111
hashbrown.workspace = true
12+
ipnet.workspace = true
1213
regex.workspace = true
1314
serde = { workspace = true, features = ["derive"] }
1415
serde_json.workspace = true

crates/stringsimile-config/src/rules.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use stringsimile_matcher::{
77
rule::{GenericMatcherRule, IntoGenericMatcherRule},
88
rules::{
99
bitflip::BitflipRule,
10+
cidr::CidrRule,
1011
confusables::ConfusablesRule,
1112
damerau_levenshtein::DamerauLevenshteinRule,
1213
hamming::HammingRule,
@@ -49,6 +50,8 @@ pub enum RuleConfig {
4950
Bitflip(Option<BitflipConfig>),
5051
/// Configuration for Regex rule
5152
Regex(RegexConfig),
53+
/// Configuration for CIDR rule
54+
Cidr(CidrConfig),
5255
}
5356

5457
/// Errors for rule configuration
@@ -109,6 +112,13 @@ pub enum RuleConfigError {
109112
/// Regex error.
110113
source: regex::Error,
111114
},
115+
116+
/// CIDR rule configuration error
117+
#[snafu(display("Invalid address for CIDR rule: {}", source))]
118+
CidrInvalidAddress {
119+
/// Address parse error.
120+
source: ipnet::AddrParseError,
121+
},
112122
}
113123

114124
impl RuleConfig {
@@ -160,6 +170,7 @@ impl RuleConfig {
160170
RuleConfig::Regex(regex_config) => {
161171
Box::new(regex_config.build()?.into_generic_matcher())
162172
}
173+
RuleConfig::Cidr(cidr_config) => Box::new(cidr_config.build()?.into_generic_matcher()),
163174
})
164175
}
165176
}
@@ -450,6 +461,21 @@ impl RegexConfig {
450461
}
451462
}
452463

464+
/// Configuration for CIDR rule
465+
#[derive(Debug, Clone, Serialize, Deserialize)]
466+
pub struct CidrConfig {
467+
/// CIDR notation address to match against.
468+
pub address: String,
469+
}
470+
471+
impl CidrConfig {
472+
fn build(&self) -> Result<CidrRule, Error> {
473+
Ok(CidrRule::new(
474+
self.address.parse().context(CidrInvalidAddressSnafu)?,
475+
))
476+
}
477+
}
478+
453479
#[cfg(test)]
454480
mod tests {
455481
use super::*;
@@ -871,4 +897,44 @@ mod tests {
871897
let res = config.build();
872898
assert!(res.is_err());
873899
}
900+
901+
#[test]
902+
fn test_parse_cidr() {
903+
let json = r#"
904+
{
905+
"rule_type": "cidr",
906+
"values": {
907+
"address": "192.168.0.0/24"
908+
}
909+
}
910+
"#;
911+
912+
let RuleConfig::Cidr(config) = serde_json::from_str(json).unwrap() else {
913+
panic!("Expected CIDR config");
914+
};
915+
assert_eq!(config.address, "192.168.0.0/24");
916+
917+
let res = config.build();
918+
assert!(res.is_ok());
919+
}
920+
921+
#[test]
922+
fn test_parse_cidr_invalid_address() {
923+
let json = r#"
924+
{
925+
"rule_type": "cidr",
926+
"values": {
927+
"address": "test"
928+
}
929+
}
930+
"#;
931+
932+
let RuleConfig::Cidr(config) = serde_json::from_str(json).unwrap() else {
933+
panic!("Expected CIDR config");
934+
};
935+
assert_eq!(config.address, "test");
936+
937+
let res = config.build();
938+
assert!(res.is_err());
939+
}
874940
}

crates/stringsimile-matcher/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ version.workspace = true
77

88
[features]
99
default = ["all"]
10-
all = ["rules-levenshtein", "rules-hamming", "rules-jaro", "rules-jaro-winkler", "rules-confusables", "rules-damerau-levenshtein", "rules-soundex", "rules-metaphone", "rules-nysiis", "rules-match-rating", "rules-bitflip", "rules-regex"]
10+
all = ["rules-levenshtein", "rules-hamming", "rules-jaro", "rules-jaro-winkler", "rules-confusables", "rules-damerau-levenshtein", "rules-soundex", "rules-metaphone", "rules-nysiis", "rules-match-rating", "rules-bitflip", "rules-regex", "rules-cidr"]
1111
rules-bitflip = ["dep:lazy_static"]
12+
rules-cidr = ["dep:ipnet"]
1213
rules-confusables = ["dep:confusables"]
1314
rules-damerau-levenshtein = ["dep:triple_accel"]
1415
rules-hamming = ["dep:triple_accel"]
@@ -24,6 +25,7 @@ rules-soundex = ["dep:rphonetic"]
2425
[dependencies]
2526
confusables = { workspace = true, optional = true }
2627
hashbrown.workspace = true
28+
ipnet = { workspace = true, optional = true }
2729
lazy_static = { workspace = true, optional = true }
2830
metrics.workspace = true
2931
regex = { workspace = true, optional = true }

crates/stringsimile-matcher/benches/rules.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use stringsimile_matcher::{
44
rule::MatcherRule,
55
rules::{
66
bitflip::BitflipRule,
7+
cidr::CidrRule,
78
confusables::ConfusablesRule,
89
damerau_levenshtein::DamerauLevenshteinRule,
910
hamming::HammingRule,
@@ -350,6 +351,15 @@ bench_rule! {
350351
}
351352
}
352353

354+
bench_rule! {
355+
name = cidr;
356+
single_match = "192.168.0.1";
357+
single_mismatch = "192.168.1.1";
358+
builder {
359+
CidrRule::new("192.168.0.0/24".parse().unwrap())
360+
}
361+
}
362+
353363
criterion_group!(
354364
benches,
355365
confusables,
@@ -373,5 +383,6 @@ criterion_group!(
373383
bitflip_ascii_printable_case_insensitive,
374384
regex_exact_match,
375385
regex_complex_pattern,
386+
cidr,
376387
);
377388
criterion_main!(benches);

crates/stringsimile-matcher/benches/rulesets.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use stringsimile_matcher::{
55
rule::IntoGenericMatcherRule,
66
rules::{
77
bitflip::BitflipRule,
8+
cidr::CidrRule,
89
confusables::ConfusablesRule,
910
damerau_levenshtein::DamerauLevenshteinRule,
1011
hamming::HammingRule,
@@ -204,6 +205,7 @@ bench_ruleset! {
204205
Box::new(SoundexRule::new(SoundexRuleType::Refined, 5, target_str).into_generic_matcher()),
205206
Box::new(BitflipRule::new_dns(target_str, true)),
206207
Box::new(RegexRule::new(Regex::new(target_str).unwrap())),
208+
Box::new(CidrRule::new("192.168.0.0/24".parse().unwrap())),
207209
]
208210
}])
209211
}
@@ -237,6 +239,7 @@ bench_ruleset! {
237239
Box::new(SoundexRule::new(SoundexRuleType::Refined, 5, target_str).into_generic_matcher()),
238240
Box::new(BitflipRule::new_dns(target_str, true)),
239241
Box::new(RegexRule::new(Regex::new(target_str).unwrap())),
242+
Box::new(CidrRule::new("192.168.0.0/24".parse().unwrap())),
240243
]
241244
}])
242245
}
@@ -270,6 +273,7 @@ bench_ruleset! {
270273
Box::new(SoundexRule::new(SoundexRuleType::Refined, 5, target_str).into_generic_matcher()),
271274
Box::new(BitflipRule::new_dns(target_str, true)),
272275
Box::new(RegexRule::new(Regex::new(target_str).unwrap())),
276+
Box::new(CidrRule::new("192.168.0.0/24".parse().unwrap())),
273277
]
274278
}])
275279
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! CIDR rule implementation
2+
3+
use ipnet::IpNet;
4+
use std::{fmt::Debug, io::Error, net::IpAddr};
5+
6+
use serde::{Deserialize, Serialize};
7+
8+
use crate::{
9+
MatcherResult,
10+
rule::{MatcherResultRuleMetadataExt, MatcherRule, RuleMetadata},
11+
};
12+
13+
/// Rule
14+
#[derive(Debug, Clone)]
15+
pub struct CidrRule {
16+
net: IpNet,
17+
}
18+
19+
impl CidrRule {
20+
/// Creates a new instance of [`CidrRule`], with the provided address/network.
21+
pub fn new(net: IpNet) -> Self {
22+
Self { net }
23+
}
24+
}
25+
26+
/// metadata
27+
#[derive(Debug, Clone, Serialize, Deserialize)]
28+
pub struct CidrMetadata;
29+
30+
impl MatcherRule for CidrRule {
31+
type OutputMetadata = CidrMetadata;
32+
type Error = Error;
33+
34+
fn match_rule(
35+
&self,
36+
input_str: &str,
37+
_target_str: &str,
38+
) -> MatcherResult<Self::OutputMetadata, Self::Error> {
39+
let Ok(addr) = input_str.parse::<IpAddr>() else {
40+
return MatcherResult::new_no_match(CidrMetadata);
41+
};
42+
if self.net.contains(&addr) {
43+
MatcherResult::new_match(CidrMetadata)
44+
} else {
45+
MatcherResult::new_no_match(CidrMetadata)
46+
}
47+
}
48+
}
49+
50+
impl RuleMetadata for CidrMetadata {
51+
const RULE_NAME: &str = "cidr";
52+
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
use crate::rule::MatcherResultExt;
57+
58+
use super::*;
59+
60+
#[test]
61+
fn simple_example() {
62+
let rule = CidrRule::new("192.168.0.0/24".parse().unwrap());
63+
64+
let result = rule.match_rule("192.168.0.1", "whatever");
65+
assert!(result.is_match());
66+
let result = rule.match_rule("192.168.0.30", "whatever");
67+
assert!(result.is_match());
68+
let result = rule.match_rule("192.168.1.30", "whatever");
69+
assert!(!result.is_match());
70+
}
71+
}

0 commit comments

Comments
 (0)