1use crate::proximity_keywords::compile_keywords_proximity_config;
2use crate::scanner::config::RuleConfig;
3use crate::scanner::metrics::RuleMetrics;
4use crate::scanner::regex_rule::compiled::RegexCompiledRule;
5use crate::scanner::regex_rule::regex_store::get_memoized_regex;
6use crate::validation::{
7 RegexPatternCaptureGroupsValidationError, validate_and_create_regex,
8 validate_named_capture_group_minimum_length,
9};
10use crate::{CompiledRule, CreateScannerError, Labels};
11use regex_automata::util::captures::GroupInfo;
12use serde::{Deserialize, Serialize};
13use serde_with::DefaultOnNull;
14use serde_with::serde_as;
15use std::sync::Arc;
16use strum::{AsRefStr, EnumIter};
17
18pub const DEFAULT_KEYWORD_LOOKAHEAD: usize = 30;
19
20#[serde_as]
21#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
22pub struct RegexRuleConfig {
23 pub pattern: String,
24 pub proximity_keywords: Option<ProximityKeywordsConfig>,
25 pub validator: Option<SecondaryValidator>,
26 #[serde_as(deserialize_as = "DefaultOnNull")]
27 #[serde(default)]
28 pub labels: Labels,
29 pub pattern_capture_groups: Option<Vec<String>>,
30}
31
32impl RegexRuleConfig {
33 pub fn new(pattern: &str) -> Self {
34 #[allow(deprecated)]
35 Self {
36 pattern: pattern.to_owned(),
37 proximity_keywords: None,
38 validator: None,
39 labels: Labels::default(),
40 pattern_capture_groups: None,
41 }
42 }
43
44 pub fn with_pattern(&self, pattern: &str) -> Self {
45 self.mutate_clone(|x| x.pattern = pattern.to_string())
46 }
47
48 pub fn with_proximity_keywords(&self, proximity_keywords: ProximityKeywordsConfig) -> Self {
49 self.mutate_clone(|x| x.proximity_keywords = Some(proximity_keywords))
50 }
51
52 pub fn with_labels(&self, labels: Labels) -> Self {
53 self.mutate_clone(|x| x.labels = labels)
54 }
55
56 pub fn with_pattern_capture_groups(&self, pattern_capture_groups: Vec<String>) -> Self {
57 self.mutate_clone(|x| x.pattern_capture_groups = Some(pattern_capture_groups))
58 }
59
60 pub fn with_pattern_capture_group(&self, pattern_capture_group: &str) -> Self {
61 self.mutate_clone(|x| match x.pattern_capture_groups {
62 Some(ref mut pattern_capture_groups) => {
63 pattern_capture_groups.push(pattern_capture_group.to_string());
64 }
65 None => {
66 x.pattern_capture_groups = Some(vec![pattern_capture_group.to_string()]);
67 }
68 })
69 }
70
71 pub fn build(&self) -> Arc<dyn RuleConfig> {
72 Arc::new(self.clone())
73 }
74
75 fn mutate_clone(&self, modify: impl FnOnce(&mut Self)) -> Self {
76 let mut clone = self.clone();
77 modify(&mut clone);
78 clone
79 }
80
81 pub fn with_included_keywords(
82 &self,
83 keywords: impl IntoIterator<Item = impl AsRef<str>>,
84 ) -> Self {
85 let mut this = self.clone();
86 let mut config = self.get_or_create_proximity_keywords_config();
87 config.included_keywords = keywords
88 .into_iter()
89 .map(|x| x.as_ref().to_string())
90 .collect::<Vec<_>>();
91 this.proximity_keywords = Some(config);
92 this
93 }
94
95 pub fn with_excluded_keywords(
96 &self,
97 keywords: impl IntoIterator<Item = impl AsRef<str>>,
98 ) -> Self {
99 let mut this = self.clone();
100 let mut config = self.get_or_create_proximity_keywords_config();
101 config.excluded_keywords = keywords
102 .into_iter()
103 .map(|x| x.as_ref().to_string())
104 .collect::<Vec<_>>();
105 this.proximity_keywords = Some(config);
106 this
107 }
108
109 pub fn with_validator(&self, validator: Option<SecondaryValidator>) -> Self {
110 let mut this = self.clone();
111 this.validator = validator;
112 this
113 }
114
115 fn get_or_create_proximity_keywords_config(&self) -> ProximityKeywordsConfig {
116 self.proximity_keywords
117 .clone()
118 .unwrap_or_else(|| ProximityKeywordsConfig {
119 look_ahead_character_count: DEFAULT_KEYWORD_LOOKAHEAD,
120 included_keywords: vec![],
121 excluded_keywords: vec![],
122 })
123 }
124}
125
126fn is_pattern_capture_groups_valid(
127 pattern: &str,
128 pattern_capture_groups: &Option<Vec<String>>,
129 group_info: &GroupInfo,
130) -> Result<(), RegexPatternCaptureGroupsValidationError> {
131 if pattern_capture_groups.is_none() {
132 return Ok(());
133 }
134 let pattern_capture_groups = pattern_capture_groups.as_ref().unwrap();
135 if pattern_capture_groups.len() != 1 {
136 return Err(
138 RegexPatternCaptureGroupsValidationError::TooManyCaptureGroups(
139 pattern_capture_groups.len(),
140 ),
141 );
142 }
143 let pattern_capture_group = pattern_capture_groups.first().unwrap();
144 if !group_info
145 .all_names()
146 .filter(|(_, _, name)| name.is_some())
147 .map(|(_, _, name)| name.unwrap())
148 .any(|name| name == pattern_capture_group)
149 {
150 return Err(
151 RegexPatternCaptureGroupsValidationError::CaptureGroupNotPresent(
152 pattern_capture_group.clone(),
153 ),
154 );
155 }
156 if pattern_capture_group != "sds_match" {
159 return Err(RegexPatternCaptureGroupsValidationError::TargetedCaptureGroupMustBeSdsMatch);
160 }
161 validate_named_capture_group_minimum_length(pattern, pattern_capture_group)?;
162 Ok(())
163}
164
165impl RuleConfig for RegexRuleConfig {
166 fn convert_to_compiled_rule(
167 &self,
168 rule_index: usize,
169 scanner_labels: Labels,
170 ) -> Result<Box<dyn CompiledRule>, CreateScannerError> {
171 let regex = get_memoized_regex(&self.pattern, validate_and_create_regex)?;
172
173 let rule_labels = scanner_labels.clone_with_labels(self.labels.clone());
174
175 let (included_keywords, excluded_keywords) = self
176 .proximity_keywords
177 .as_ref()
178 .map(|config| compile_keywords_proximity_config(config, &rule_labels))
179 .unwrap_or(Ok((None, None)))?;
180
181 is_pattern_capture_groups_valid(
182 &self.pattern,
183 &self.pattern_capture_groups,
184 regex.group_info(),
185 )?;
186
187 Ok(Box::new(RegexCompiledRule {
188 rule_index,
189 regex,
190 included_keywords,
191 excluded_keywords,
192 validator: self.validator.clone().map(|x| x.compile()),
193 metrics: RuleMetrics::new(&rule_labels),
194 pattern_capture_groups: self.pattern_capture_groups.clone(),
195 }))
196 }
197
198 fn as_regex_rule(&self) -> Option<&RegexRuleConfig> {
199 Some(self)
200 }
201}
202
203#[serde_as]
204#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
205pub struct ProximityKeywordsConfig {
206 pub look_ahead_character_count: usize,
207
208 #[serde_as(deserialize_as = "DefaultOnNull")]
209 #[serde(default)]
210 pub included_keywords: Vec<String>,
211
212 #[serde_as(deserialize_as = "DefaultOnNull")]
213 #[serde(default)]
214 pub excluded_keywords: Vec<String>,
215}
216
217#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, EnumIter, AsRefStr)]
218#[serde(tag = "type")]
219pub enum SecondaryValidator {
220 AbaRtnChecksum,
221 AtlassianTokenChecksum,
222 AustralianMedicareChecksum,
223 AustralianTfnChecksum,
224 AustrianSSNChecksum,
225 BelgiumNationalRegisterChecksum,
226 BrazilianCnpjChecksum,
227 BrazilianCpfChecksum,
228 BtcChecksum,
229 BulgarianEGNChecksum,
230 ChineseIdChecksum,
231 CoordinationNumberChecksum,
232 CzechPersonalIdentificationNumberChecksum,
233 CzechTaxIdentificationNumberChecksum,
234 DutchBsnChecksum,
235 DutchPassportChecksum,
236 EntropyCheck,
237 EstoniaPersonalCodeChecksum,
238 EthereumChecksum,
239 FinnishHetuChecksum,
240 FranceNifChecksum,
241 FranceSsnChecksum,
242 GermanIdsChecksum,
243 GermanSvnrChecksum,
244 GithubTokenChecksum,
245 GreeceAmkaChecksum,
246 GreekTinChecksum,
247 HungarianTinChecksum,
248 IbanChecker,
249 IrishPpsChecksum,
250 ItalianNationalIdChecksum,
251 JwtClaimsValidator { config: JwtClaimsValidatorConfig },
252 JwtExpirationChecker,
253 LatviaNationalIdChecksum,
254 LithuanianPersonalIdentificationNumberChecksum,
255 LuhnChecksum,
256 LuxembourgIndividualNINChecksum,
257 Mod11_10checksum,
258 Mod11_2checksum,
259 Mod1271_36Checksum,
260 Mod27_26checksum,
261 Mod37_2checksum,
262 Mod37_36checksum,
263 Mod661_26checksum,
264 Mod97_10checksum,
265 MoneroAddress,
266 NhsCheckDigit,
267 NirChecksum,
268 NonHexChecker,
269 NonHexPlusTokenEfficiencyChecker,
270 PolishNationalIdChecksum,
271 PolishNipChecksum,
272 PortugueseTaxIdChecksum,
273 RodneCisloNumberChecksum,
274 RomanianPersonalNumericCode,
275 SingaporeNricChecksum,
276 SloveniaTinChecksum,
277 SlovenianPINChecksum,
278 SpanishDniChecksum,
279 SpanishNussChecksum,
280 SwedenPINChecksum,
281 TokenEfficiencyCheck,
282 UkNinoFormatCheck,
283 UkTrnChecksum,
284 UsDeaChecksum,
285 UsNpiChecksum,
286 VerhoeffChecksum,
287 VinChecksum,
288}
289
290#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
291#[serde(tag = "type", content = "config")]
292pub enum ClaimRequirement {
293 Present,
295 NotExpired,
297 ExactValue(String),
299 RegexMatch(String),
301}
302
303#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
304pub struct JwtClaimsValidatorConfig {
305 #[serde(default)]
306 pub required_headers: std::collections::BTreeMap<String, ClaimRequirement>,
307 #[serde(default)]
308 pub required_claims: std::collections::BTreeMap<String, ClaimRequirement>,
309}
310
311#[cfg(test)]
312mod test {
313 use crate::{AwsType, CustomHttpConfig, MatchAction, MatchValidationType, RootRuleConfig};
314 use std::collections::BTreeMap;
315 use strum::IntoEnumIterator;
316
317 use super::*;
318
319 #[test]
320 fn should_override_pattern() {
321 let rule_config = RegexRuleConfig::new("123").with_pattern("456");
322 assert_eq!(rule_config.pattern, "456");
323 }
324
325 #[test]
326 #[allow(deprecated)]
327 fn should_have_default() {
328 let rule_config = RegexRuleConfig::new("123");
329 assert_eq!(
330 rule_config,
331 RegexRuleConfig {
332 pattern: "123".to_string(),
333 proximity_keywords: None,
334 validator: None,
335 labels: Labels::empty(),
336 pattern_capture_groups: None,
337 }
338 );
339 }
340
341 #[test]
342 fn should_use_capture_group() {
343 let rule_config = RegexRuleConfig::new("hey (?<capture_group>world)")
344 .with_pattern_capture_groups(vec!["capture_group".to_string()]);
345 assert_eq!(
346 rule_config,
347 RegexRuleConfig {
348 pattern: "hey (?<capture_group>world)".to_string(),
349 proximity_keywords: None,
350 validator: None,
351 labels: Labels::empty(),
352 pattern_capture_groups: Some(vec!["capture_group".to_string()]),
353 }
354 );
355 }
356
357 #[test]
358 fn match_action_should_default_to_none_on_deserialization() {
359 let config: RootRuleConfig<RegexRuleConfig> =
360 serde_json::from_str(r#"{"pattern":"hello"}"#).unwrap();
361 assert_eq!(config.match_action, MatchAction::None);
362 }
363
364 #[test]
365 fn proximity_keywords_should_have_default() {
366 let json_config = r#"{"look_ahead_character_count": 0}"#;
367 let test: ProximityKeywordsConfig = serde_json::from_str(json_config).unwrap();
368 assert_eq!(
369 test,
370 ProximityKeywordsConfig {
371 look_ahead_character_count: 0,
372 included_keywords: vec![],
373 excluded_keywords: vec![]
374 }
375 );
376
377 let json_config = r#"{"look_ahead_character_count": 0, "excluded_keywords": null, "included_keywords": null}"#;
378 let test: ProximityKeywordsConfig = serde_json::from_str(json_config).unwrap();
379 assert_eq!(
380 test,
381 ProximityKeywordsConfig {
382 look_ahead_character_count: 0,
383 included_keywords: vec![],
384 excluded_keywords: vec![]
385 }
386 );
387 }
388
389 #[test]
390 #[allow(deprecated)]
391 fn test_third_party_active_checker() {
392 let http_config = CustomHttpConfig::default().with_endpoint("http://test.com".to_string());
394 let validation_type = MatchValidationType::CustomHttp(http_config.clone());
395 let rule_config = RootRuleConfig::new(RegexRuleConfig::new("123"))
396 .third_party_active_checker(validation_type.clone());
397
398 assert_eq!(
399 rule_config.third_party_active_checker,
400 Some(validation_type.clone())
401 );
402 assert_eq!(rule_config.match_validation_type, None);
403 assert_eq!(
404 rule_config.get_third_party_active_checker(),
405 Some(&validation_type)
406 );
407
408 let aws_type = AwsType::AwsId;
410 let validation_type2 = MatchValidationType::Aws(aws_type);
411 let rule_config = RootRuleConfig::new(RegexRuleConfig::new("123"))
412 .third_party_active_checker(validation_type2.clone());
413
414 assert_eq!(
415 rule_config.third_party_active_checker,
416 Some(validation_type2.clone())
417 );
418 assert_eq!(
419 rule_config.get_third_party_active_checker(),
420 Some(&validation_type2)
421 );
422
423 let rule_config = RootRuleConfig::new(RegexRuleConfig::new("123"))
425 .third_party_active_checker(MatchValidationType::CustomHttp(http_config.clone()));
426
427 assert_eq!(
428 rule_config.get_third_party_active_checker(),
429 Some(&MatchValidationType::CustomHttp(http_config.clone()))
430 );
431 }
432
433 #[test]
434 fn test_secondary_validator_enum_iter() {
435 let validators: Vec<SecondaryValidator> = SecondaryValidator::iter().collect();
437 assert!(validators.contains(&SecondaryValidator::GithubTokenChecksum));
439 assert!(validators.contains(&SecondaryValidator::JwtExpirationChecker));
440 }
441
442 #[test]
443 fn test_secondary_validator_are_sorted() {
444 let validator_names: Vec<String> = SecondaryValidator::iter()
445 .map(|a| a.as_ref().to_string())
446 .collect();
447 let mut sorted_validator_names = validator_names.clone();
448 sorted_validator_names.sort();
449 assert_eq!(
450 sorted_validator_names, validator_names,
451 "Secondary validators should be sorted by alphabetical order, but it's not the case, expected order:"
452 );
453 }
454
455 #[test]
457 fn test_jwt_claims_validator_config_serialization_order() {
458 let mut required_claims = BTreeMap::new();
460 required_claims.insert("zzz".to_string(), ClaimRequirement::Present);
461 required_claims.insert("exp".to_string(), ClaimRequirement::NotExpired);
462 required_claims.insert(
463 "aaa".to_string(),
464 ClaimRequirement::ExactValue("test".to_string()),
465 );
466 required_claims.insert(
467 "mmm".to_string(),
468 ClaimRequirement::RegexMatch(r"^test.*".to_string()),
469 );
470
471 let config = JwtClaimsValidatorConfig {
472 required_claims,
473 required_headers: std::collections::BTreeMap::new(),
474 };
475
476 let serialized1 = serde_json::to_string(&config).unwrap();
478 let serialized2 = serde_json::to_string(&config).unwrap();
479
480 assert_eq!(serialized1, serialized2, "Serialization should be stable");
482
483 assert!(serialized1.find("aaa").unwrap() < serialized1.find("exp").unwrap());
485 assert!(serialized1.find("exp").unwrap() < serialized1.find("mmm").unwrap());
486 assert!(serialized1.find("mmm").unwrap() < serialized1.find("zzz").unwrap());
487 }
488
489 #[test]
490 fn test_capture_groups_validation() {
491 let test_cases: Vec<(
492 &str,
493 Vec<String>,
494 Result<(), RegexPatternCaptureGroupsValidationError>,
495 )> = vec![
496 (
497 "hello (?<sds_match>world)",
498 vec!["sds_match".to_string()],
499 Ok(()),
500 ),
501 (
502 "hello (?<capture_group>world)",
503 vec!["capture_group".to_string()],
504 Err(RegexPatternCaptureGroupsValidationError::TargetedCaptureGroupMustBeSdsMatch),
505 ),
506 (
507 "hello (?<sds_match>world) and (?<another_group>world)",
508 vec!["sds_match".to_string()],
509 Ok(()),
510 ),
511 (
512 "hello (?<capture_grou>world)",
513 vec!["capture_group".to_string()],
514 Err(
515 RegexPatternCaptureGroupsValidationError::CaptureGroupNotPresent(
516 "capture_group".to_string(),
517 ),
518 ),
519 ),
520 (
521 "hello (?<sds_match>d*)",
522 vec!["sds_match".to_string()],
523 Err(RegexPatternCaptureGroupsValidationError::CaptureGroupMatchesEmptyString),
524 ),
525 (
526 "hello (?<sds_match>world)",
527 vec!["sds_match".to_string(), "sds_match2".to_string()],
528 Err(RegexPatternCaptureGroupsValidationError::TooManyCaptureGroups(2)),
529 ),
530 ];
531 for (pattern, capture_groups, expected_result) in test_cases {
532 let rule_config =
533 RegexRuleConfig::new(pattern).with_pattern_capture_groups(capture_groups);
534 assert_eq!(
535 is_pattern_capture_groups_valid(
536 &rule_config.pattern,
537 &rule_config.pattern_capture_groups,
538 &get_memoized_regex(pattern, validate_and_create_regex)
539 .unwrap()
540 .group_info()
541 ),
542 expected_result
543 );
544 }
545 }
546}