quynong commited on
Commit
98cd069
·
verified ·
1 Parent(s): a0cec9d

Update rule_detector.py

Browse files
Files changed (1) hide show
  1. rule_detector.py +57 -41
rule_detector.py CHANGED
@@ -1,8 +1,23 @@
1
- """Rule-based PII detector — HYBRID single-file edition (v2, precision-tuned).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  Same organisation as ``old rule/rule_detector.py`` (PIIMatch / RegexRule /
4
- validators / RulebasePIIDetector with bare-regex rules, keyword-context specs, a
5
- single dedup pass), but it carries the **HYBRID v2** rule set:
6
 
7
  OLD6 = {PHONE, IBAN, CARD_NUMBER, SWIFT, PIN, COORDINATE}
8
  → the precision-first OLD rules, upgraded so they still pass the
@@ -109,7 +124,9 @@ ACTIVE_RULEBASE_BLOCKING_OVERLAPS = {
109
  "PIN": frozenset({"PHONE", "CVV"}),
110
  }
111
 
112
- CONTEXT_NORMALIZE_AS_TEXT_ENTITY_TYPES = frozenset({"PASSWORD", "TIN"})
 
 
113
 
114
  ISO_COUNTRY_CODES = {
115
  "AD","AE","AF","AG","AI","AL","AM","AO","AR","AS","AT","AU","AW","AX","AZ",
@@ -597,6 +614,15 @@ _EMAIL_BARE_DOUBLE_AT = re.compile(
597
  r"(?<![\w.+*\-])[A-Z0-9._%+-]+@@[A-Z0-9.-]+\.[A-Z]{2,63}(?![\w-])",
598
  re.IGNORECASE,
599
  )
 
 
 
 
 
 
 
 
 
600
 
601
  # ---- PHONE (OLD6 hybrid) --------------------------------------------------
602
  _PHONE_MASK_GUARD = r"(?! ?\*{3,})(?! ?\*+\d)(?!\s*-{2,})(?!-\*)"
@@ -672,6 +698,14 @@ _IBAN_NEG_CONTEXT = re.compile(
672
  r"|(?:sensor|equipment|device)(?:\s+unit)?\b[^\n\d]{0,16}"
673
  r"|sensor\s+unit\b[^\n\d]{0,16})$"
674
  )
 
 
 
 
 
 
 
 
675
 
676
  # ---- CARD_NUMBER (OLD6) ---------------------------------------------------
677
  _CARD_BARE = re.compile(r"(?<![\d.])\d{4}[ -]\d{4}[ -]\d{4}[ -]\d{1,7}(?![\d])")
@@ -694,6 +728,15 @@ _IP_BARE = re.compile(
694
  r")",
695
  re.IGNORECASE,
696
  )
 
 
 
 
 
 
 
 
 
697
 
698
  # ---- URL (TUNED) ----------------------------------------------------------
699
  _URL_BARE = re.compile(
@@ -868,49 +911,22 @@ class RulebasePIIDetector:
868
  return entity_type in self.allowed_entity_types
869
 
870
  def _build_regex_rules(self):
871
- rules = [
872
- # EMAIL (TUNED)
873
- RegexRule(entity_type="EMAIL", pattern=_EMAIL_BARE, validator=is_valid_email),
874
- RegexRule(entity_type="EMAIL", pattern=_EMAIL_BARE_DOUBLE_AT,
875
- validator=is_valid_email_double_at, normalizer=normalize_email_double_at),
876
- # PHONE (OLD6)
877
- RegexRule(entity_type="PHONE", pattern=_PHONE_BARE_INTL,
878
- validator=is_phone_value, normalizer=normalize_phone),
879
- RegexRule(entity_type="PHONE", pattern=_PHONE_BARE_SEP,
880
- validator=is_phone_value, normalizer=normalize_phone,
881
- neg_context=_PHONE_NEG_LOCAL),
882
- # NATIONAL_ID (TUNED)
883
- RegexRule(entity_type="NATIONAL_ID", pattern=_NATID_BARE_SSN,
884
- validator=is_valid_ssn, neg_context=_NATID_NEG_CONTEXT),
885
- # IBAN (OLD6)
886
- RegexRule(entity_type="IBAN", pattern=_IBAN_BARE, validator=is_valid_iban,
887
- normalizer=lambda v: re.sub(r"[\s-]", "", v).upper(),
888
- neg_context=_IBAN_NEG_CONTEXT),
889
- RegexRule(entity_type="IBAN", pattern=_IBAN_BARE_SPACED, validator=is_valid_iban,
890
- normalizer=lambda v: re.sub(r"[\s-]", "", v).upper(),
891
- neg_context=_IBAN_NEG_CONTEXT),
892
- # CARD_NUMBER (OLD6)
893
- RegexRule(entity_type="CARD_NUMBER", pattern=_CARD_BARE,
894
- validator=is_valid_card_number, normalizer=strip_number_separators,
895
- neg_context=_CARD_NEG_CONTEXT),
896
- # IP (TUNED)
897
- RegexRule(entity_type="IP", pattern=_IP_BARE, validator=is_valid_ip),
898
- # URL (TUNED)
899
- RegexRule(entity_type="URL", pattern=_URL_BARE, validator=is_valid_url),
900
- RegexRule(entity_type="URL", pattern=_URL_BARE_PATH, validator=is_valid_url_barepath),
901
- # COORDINATE (OLD6)
902
- RegexRule(entity_type="COORDINATE", pattern=_COORD_BARE_DECIMAL_PAIR,
903
- validator=is_valid_coord_pair_bare),
904
- RegexRule(entity_type="COORDINATE", pattern=_COORD_BARE_DMS_PAIR,
905
- validator=is_valid_coord_pair),
906
- ]
907
- return [r for r in rules if self._is_enabled(r.entity_type)]
908
 
909
  def _build_context_rules(self):
910
  # (entity_type, pattern, validator[, neg_context]) — value captured in
911
  # group(1); if neg_context is set, the rule is suppressed when the ~28 chars
912
  # of left context match it (e.g. an explicit API-key cue before a PASSWORD).
913
  specs = [
 
 
 
 
914
  ("PHONE", _PHONE_CONTEXT, is_phone_value_ctx),
915
  ("BANK_ACCOUNT", _BANK_CONTEXT, is_valid_account_number),
916
  ("CARD_NUMBER", _CARD_CONTEXT, is_valid_card_number),
 
1
+ """Rule-based PII detector — HYBRID single-file edition (v3, CONTEXT-ONLY).
2
+
3
+ v3 derives from v2 (the precision-tuned 2026-06-12 set) with ONE structural
4
+ change requested on 2026-06-23: **every class is now detected through a positive
5
+ keyword context only.** All bare-regex rules are removed (``_build_regex_rules``
6
+ returns an empty list), and the three classes that used to be bare-only — EMAIL,
7
+ IBAN, IP — gain dedicated context rules (``_EMAIL_CONTEXT`` / ``_IBAN_CONTEXT`` /
8
+ ``_IP_CONTEXT``). A span now fires only when a cue word ("email:", "IBAN", "IP
9
+ address", "phone", "SWIFT", …) precedes the value.
10
+
11
+ Trade-off (intended): precision is held at the v2 level (context-gating tends to
12
+ raise it), while recall, F1 and the number of predictions DROP — values that
13
+ appear with no cue (most bare EMAIL/IP/COORDINATE, the bare SSN shape, bare
14
+ PHONE/CARD/IBAN runs) are no longer matched. This is accepted by design.
15
+
16
+ The detection engine, validators, dedup and blocking map are unchanged from v2.
17
 
18
  Same organisation as ``old rule/rule_detector.py`` (PIIMatch / RegexRule /
19
+ validators / RulebasePIIDetector with keyword-context specs, a single dedup
20
+ pass), but it carries the **HYBRID v2** rule set:
21
 
22
  OLD6 = {PHONE, IBAN, CARD_NUMBER, SWIFT, PIN, COORDINATE}
23
  → the precision-first OLD rules, upgraded so they still pass the
 
124
  "PIN": frozenset({"PHONE", "CVV"}),
125
  }
126
 
127
+ # EMAIL/URL/IP added in v3: their context-captured value must not have hyphens
128
+ # or dots stripped (those are part of the address), so keep it as collapsed text.
129
+ CONTEXT_NORMALIZE_AS_TEXT_ENTITY_TYPES = frozenset({"PASSWORD", "TIN", "EMAIL", "URL", "IP"})
130
 
131
  ISO_COUNTRY_CODES = {
132
  "AD","AE","AF","AG","AI","AL","AM","AO","AR","AS","AT","AU","AW","AX","AZ",
 
614
  r"(?<![\w.+*\-])[A-Z0-9._%+-]+@@[A-Z0-9.-]+\.[A-Z]{2,63}(?![\w-])",
615
  re.IGNORECASE,
616
  )
617
+ # v3 CONTEXT-ONLY: EMAIL must now be introduced by a cue ("email:", "thư điện tử là …").
618
+ # The value keeps the same shape as the bare rule; the '*' lookbehind still blocks a
619
+ # masked-tail leak. Validator is_valid_email keeps precision at the v2 level.
620
+ _EMAIL_CONTEXT = re.compile(
621
+ r"(?i)\b(?:e[-\s]?mail(?:\s*address)?|thư\s*điện\s*tử|địa\s*chỉ\s*(?:email|e-mail|thư)"
622
+ r"|hộp\s*thư(?:\s*điện\s*tử)?|gửi\s*(?:tới|đến|về)|liên\s*hệ\s*(?:qua\s*)?(?:email|mail))\b"
623
+ r"\s*(?:[:#\-]\s*|[^:\n@]{0,20}(?:là|is|at|tại)\s+|\s+)"
624
+ r"(?<![\w.+*\-])([A-Z0-9._%+\-À-ɏḀ-ỿ]+@[A-Z0-9.-]+\.[A-Z]{2,63})(?![\w-])"
625
+ )
626
 
627
  # ---- PHONE (OLD6 hybrid) --------------------------------------------------
628
  _PHONE_MASK_GUARD = r"(?! ?\*{3,})(?! ?\*+\d)(?!\s*-{2,})(?!-\*)"
 
698
  r"|(?:sensor|equipment|device)(?:\s+unit)?\b[^\n\d]{0,16}"
699
  r"|sensor\s+unit\b[^\n\d]{0,16})$"
700
  )
701
+ # v3 CONTEXT-ONLY: IBAN must now be introduced by an "IBAN" cue. The value is
702
+ # captured uppercase-only (?-i:) so a trailing lowercase word can't be absorbed;
703
+ # both compact and single-space-grouped forms are allowed. Validator runs mod-97.
704
+ _IBAN_CONTEXT = re.compile(
705
+ r"(?i)\b(?:iban(?:\s*(?:number|no\.?|code))?|số\s*iban|mã\s*iban)\b"
706
+ r"\s*(?:[:#\-]\s*|[^:\n0-9]{0,16}(?:là|is)\s+|\s+)"
707
+ r"((?-i:[A-Z]{2}\d{2}(?:\s?[A-Z0-9]){11,34}))"
708
+ )
709
 
710
  # ---- CARD_NUMBER (OLD6) ---------------------------------------------------
711
  _CARD_BARE = re.compile(r"(?<![\d.])\d{4}[ -]\d{4}[ -]\d{4}[ -]\d{1,7}(?![\d])")
 
728
  r")",
729
  re.IGNORECASE,
730
  )
731
+ # v3 CONTEXT-ONLY: IP must now be introduced by an "IP / IP address / IPv4 / IPv6"
732
+ # cue. The value reuses the bare IPv4/IPv6 alternation; validator is_valid_ip uses
733
+ # the stdlib ipaddress parser, so a malformed run is rejected (precision held).
734
+ _IP_CONTEXT = re.compile(
735
+ r"(?i)\b(?:ip(?:\s*address|\s*addr)?|ipv4|ipv6|địa\s*chỉ\s*ip|server\s*ip|host\s*ip)\b"
736
+ r"\s*(?:[:#\-=]\s*|[^:\n0-9a-f]{0,16}(?:là|is|at)\s+|\s+)"
737
+ r"((?:\d{1,3}\.){3}\d{1,3}(?!\.\d)|[A-F0-9]{0,4}(?::[A-F0-9]{0,4}){2,7})",
738
+ re.IGNORECASE,
739
+ )
740
 
741
  # ---- URL (TUNED) ----------------------------------------------------------
742
  _URL_BARE = re.compile(
 
911
  return entity_type in self.allowed_entity_types
912
 
913
  def _build_regex_rules(self):
914
+ # v3 CONTEXT-ONLY: all bare-regex rules are intentionally removed. Every
915
+ # class is detected through a positive keyword context (see
916
+ # _build_context_rules). The bare patterns are still defined above (some
917
+ # are reused as value sub-patterns inside the context rules) but no rule
918
+ # scans the text without a cue anymore.
919
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
 
921
  def _build_context_rules(self):
922
  # (entity_type, pattern, validator[, neg_context]) — value captured in
923
  # group(1); if neg_context is set, the rule is suppressed when the ~28 chars
924
  # of left context match it (e.g. an explicit API-key cue before a PASSWORD).
925
  specs = [
926
+ # v3: EMAIL / IBAN / IP context rules (these classes were bare-only in v2).
927
+ ("EMAIL", _EMAIL_CONTEXT, is_valid_email),
928
+ ("IBAN", _IBAN_CONTEXT, is_valid_iban),
929
+ ("IP", _IP_CONTEXT, is_valid_ip),
930
  ("PHONE", _PHONE_CONTEXT, is_phone_value_ctx),
931
  ("BANK_ACCOUNT", _BANK_CONTEXT, is_valid_account_number),
932
  ("CARD_NUMBER", _CARD_CONTEXT, is_valid_card_number),