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 PolishNationalIdChecksum,
270 PolishNipChecksum,
271 PortugueseTaxIdChecksum,
272 RodneCisloNumberChecksum,
273 RomanianPersonalNumericCode,
274 SingaporeNricChecksum,
275 SloveniaTinChecksum,
276 SlovenianPINChecksum,
277 SpanishDniChecksum,
278 SpanishNussChecksum,
279 SwedenPINChecksum,
280 TokenEfficiencyCheck,
281 UkNinoFormatCheck,
282 UkTrnChecksum,
283 UsDeaChecksum,
284 UsNpiChecksum,
285 VerhoeffChecksum,
286}
287
288#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
289#[serde(tag = "type", content = "config")]
290pub enum ClaimRequirement {
291 Present,
293 NotExpired,
295 ExactValue(String),
297 RegexMatch(String),
299}
300
301#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
302pub struct JwtClaimsValidatorConfig {
303 #[serde(default)]
304 pub required_headers: std::collections::BTreeMap<String, ClaimRequirement>,
305 #[serde(default)]
306 pub required_claims: std::collections::BTreeMap<String, ClaimRequirement>,
307}
308
309#[cfg(test)]
310mod test {
311 use crate::{AwsType, CustomHttpConfig, MatchAction, MatchValidationType, RootRuleConfig};
312 use std::collections::BTreeMap;
313 use strum::IntoEnumIterator;
314
315 use super::*;
316
317 #[test]
318 fn should_override_pattern() {
319 let rule_config = RegexRuleConfig::new("123").with_pattern("456");
320 assert_eq!(rule_config.pattern, "456");
321 }
322
323 #[test]
324 #[allow(deprecated)]
325 fn should_have_default() {
326 let rule_config = RegexRuleConfig::new("123");
327 assert_eq!(
328 rule_config,
329 RegexRuleConfig {
330 pattern: "123".to_string(),
331 proximity_keywords: None,
332 validator: None,
333 labels: Labels::empty(),
334 pattern_capture_groups: None,
335 }
336 );
337 }
338
339 #[test]
340 fn should_use_capture_group() {
341 let rule_config = RegexRuleConfig::new("hey (?<capture_group>world)")
342 .with_pattern_capture_groups(vec!["capture_group".to_string()]);
343 assert_eq!(
344 rule_config,
345 RegexRuleConfig {
346 pattern: "hey (?<capture_group>world)".to_string(),
347 proximity_keywords: None,
348 validator: None,
349 labels: Labels::empty(),
350 pattern_capture_groups: Some(vec!["capture_group".to_string()]),
351 }
352 );
353 }
354
355 #[test]
356 fn match_action_should_default_to_none_on_deserialization() {
357 let config: RootRuleConfig<RegexRuleConfig> =
358 serde_json::from_str(r#"{"pattern":"hello"}"#).unwrap();
359 assert_eq!(config.match_action, MatchAction::None);
360 }
361
362 #[test]
363 fn proximity_keywords_should_have_default() {
364 let json_config = r#"{"look_ahead_character_count": 0}"#;
365 let test: ProximityKeywordsConfig = serde_json::from_str(json_config).unwrap();
366 assert_eq!(
367 test,
368 ProximityKeywordsConfig {
369 look_ahead_character_count: 0,
370 included_keywords: vec![],
371 excluded_keywords: vec![]
372 }
373 );
374
375 let json_config = r#"{"look_ahead_character_count": 0, "excluded_keywords": null, "included_keywords": null}"#;
376 let test: ProximityKeywordsConfig = serde_json::from_str(json_config).unwrap();
377 assert_eq!(
378 test,
379 ProximityKeywordsConfig {
380 look_ahead_character_count: 0,
381 included_keywords: vec![],
382 excluded_keywords: vec![]
383 }
384 );
385 }
386
387 #[test]
388 #[allow(deprecated)]
389 fn test_third_party_active_checker() {
390 let http_config = CustomHttpConfig::default().with_endpoint("http://test.com".to_string());
392 let validation_type = MatchValidationType::CustomHttp(http_config.clone());
393 let rule_config = RootRuleConfig::new(RegexRuleConfig::new("123"))
394 .third_party_active_checker(validation_type.clone());
395
396 assert_eq!(
397 rule_config.third_party_active_checker,
398 Some(validation_type.clone())
399 );
400 assert_eq!(rule_config.match_validation_type, None);
401 assert_eq!(
402 rule_config.get_third_party_active_checker(),
403 Some(&validation_type)
404 );
405
406 let aws_type = AwsType::AwsId;
408 let validation_type2 = MatchValidationType::Aws(aws_type);
409 let rule_config = RootRuleConfig::new(RegexRuleConfig::new("123"))
410 .third_party_active_checker(validation_type2.clone());
411
412 assert_eq!(
413 rule_config.third_party_active_checker,
414 Some(validation_type2.clone())
415 );
416 assert_eq!(
417 rule_config.get_third_party_active_checker(),
418 Some(&validation_type2)
419 );
420
421 let rule_config = RootRuleConfig::new(RegexRuleConfig::new("123"))
423 .third_party_active_checker(MatchValidationType::CustomHttp(http_config.clone()));
424
425 assert_eq!(
426 rule_config.get_third_party_active_checker(),
427 Some(&MatchValidationType::CustomHttp(http_config.clone()))
428 );
429 }
430
431 #[test]
432 fn test_secondary_validator_enum_iter() {
433 let validators: Vec<SecondaryValidator> = SecondaryValidator::iter().collect();
435 assert!(validators.contains(&SecondaryValidator::GithubTokenChecksum));
437 assert!(validators.contains(&SecondaryValidator::JwtExpirationChecker));
438 }
439
440 #[test]
441 fn test_secondary_validator_are_sorted() {
442 let validator_names: Vec<String> = SecondaryValidator::iter()
443 .map(|a| a.as_ref().to_string())
444 .collect();
445 let mut sorted_validator_names = validator_names.clone();
446 sorted_validator_names.sort();
447 assert_eq!(
448 sorted_validator_names, validator_names,
449 "Secondary validators should be sorted by alphabetical order, but it's not the case, expected order:"
450 );
451 }
452
453 #[test]
455 fn test_jwt_claims_validator_config_serialization_order() {
456 let mut required_claims = BTreeMap::new();
458 required_claims.insert("zzz".to_string(), ClaimRequirement::Present);
459 required_claims.insert("exp".to_string(), ClaimRequirement::NotExpired);
460 required_claims.insert(
461 "aaa".to_string(),
462 ClaimRequirement::ExactValue("test".to_string()),
463 );
464 required_claims.insert(
465 "mmm".to_string(),
466 ClaimRequirement::RegexMatch(r"^test.*".to_string()),
467 );
468
469 let config = JwtClaimsValidatorConfig {
470 required_claims,
471 required_headers: std::collections::BTreeMap::new(),
472 };
473
474 let serialized1 = serde_json::to_string(&config).unwrap();
476 let serialized2 = serde_json::to_string(&config).unwrap();
477
478 assert_eq!(serialized1, serialized2, "Serialization should be stable");
480
481 assert!(serialized1.find("aaa").unwrap() < serialized1.find("exp").unwrap());
483 assert!(serialized1.find("exp").unwrap() < serialized1.find("mmm").unwrap());
484 assert!(serialized1.find("mmm").unwrap() < serialized1.find("zzz").unwrap());
485 }
486
487 #[test]
488 fn test_capture_groups_validation() {
489 let test_cases: Vec<(
490 &str,
491 Vec<String>,
492 Result<(), RegexPatternCaptureGroupsValidationError>,
493 )> = vec![
494 (
495 "hello (?<sds_match>world)",
496 vec!["sds_match".to_string()],
497 Ok(()),
498 ),
499 (
500 "hello (?<capture_group>world)",
501 vec!["capture_group".to_string()],
502 Err(RegexPatternCaptureGroupsValidationError::TargetedCaptureGroupMustBeSdsMatch),
503 ),
504 (
505 "hello (?<sds_match>world) and (?<another_group>world)",
506 vec!["sds_match".to_string()],
507 Ok(()),
508 ),
509 (
510 "hello (?<capture_grou>world)",
511 vec!["capture_group".to_string()],
512 Err(
513 RegexPatternCaptureGroupsValidationError::CaptureGroupNotPresent(
514 "capture_group".to_string(),
515 ),
516 ),
517 ),
518 (
519 "hello (?<sds_match>d*)",
520 vec!["sds_match".to_string()],
521 Err(RegexPatternCaptureGroupsValidationError::CaptureGroupMatchesEmptyString),
522 ),
523 (
524 "hello (?<sds_match>world)",
525 vec!["sds_match".to_string(), "sds_match2".to_string()],
526 Err(RegexPatternCaptureGroupsValidationError::TooManyCaptureGroups(2)),
527 ),
528 ];
529 for (pattern, capture_groups, expected_result) in test_cases {
530 let rule_config =
531 RegexRuleConfig::new(pattern).with_pattern_capture_groups(capture_groups);
532 assert_eq!(
533 is_pattern_capture_groups_valid(
534 &rule_config.pattern,
535 &rule_config.pattern_capture_groups,
536 &get_memoized_regex(pattern, validate_and_create_regex)
537 .unwrap()
538 .group_info()
539 ),
540 expected_result
541 );
542 }
543 }
544}