sledgedev commited on
Commit
fe40c24
·
verified ·
1 Parent(s): 5da2c61

Add/update pii_rules.py

Browse files
Files changed (1) hide show
  1. pii_rules.py +101 -0
pii_rules.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic PII layer: regex + checksum/structural validators.
2
+
3
+ Mirrors the "deterministic layer" described in Rampart's whitepaper, which is the
4
+ *system of record* for classes the neural model is weak on (cards, SSNs) and for
5
+ classes whose structure lives in punctuation (email, URL, IP). Each detector
6
+ returns character spans `(start, end, label)` over the ORIGINAL text, so they can
7
+ be unioned with the model's spans — the deterministic layer taking precedence.
8
+ """
9
+ import re
10
+ from typing import List, Tuple
11
+
12
+ Span = Tuple[int, int, str]
13
+
14
+ EMAIL_RE = re.compile(r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b")
15
+ URL_RE = re.compile(r"\b(?:https?://|www\.)[^\s<>()]+", re.IGNORECASE)
16
+ IPV4_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
17
+ IPV6_RE = re.compile(r"\b(?:[A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}\b")
18
+ MAC_RE = re.compile(r"\b(?:[A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}\b")
19
+ SSN_RE = re.compile(r"\b(\d{3})[-\s](\d{2})[-\s](\d{4})\b")
20
+ # A run of 13-19 digits, allowing single space/hyphen separators (card shape).
21
+ CARD_RE = re.compile(r"\b(?:\d[ -]?){12,18}\d\b")
22
+
23
+
24
+ def _luhn_ok(digits: str) -> bool:
25
+ if not (13 <= len(digits) <= 19):
26
+ return False
27
+ total, alt = 0, False
28
+ for ch in reversed(digits):
29
+ d = ord(ch) - 48
30
+ if alt:
31
+ d *= 2
32
+ if d > 9:
33
+ d -= 9
34
+ total += d
35
+ alt = not alt
36
+ return total % 10 == 0
37
+
38
+
39
+ def _ssn_ok(area: str, group: str, serial: str) -> bool:
40
+ a = int(area)
41
+ if a == 0 or a == 666 or a >= 900: # reserved / invalid SSA areas
42
+ return False
43
+ if int(group) == 0 or int(serial) == 0:
44
+ return False
45
+ return True
46
+
47
+
48
+ def _ipv4_ok(s: str) -> bool:
49
+ parts = s.split(".")
50
+ return len(parts) == 4 and all(p.isdigit() and 0 <= int(p) <= 255 for p in parts)
51
+
52
+
53
+ def detect_rules(text: str) -> List[Span]:
54
+ spans: List[Span] = []
55
+
56
+ for m in EMAIL_RE.finditer(text):
57
+ spans.append((m.start(), m.end(), "EMAIL"))
58
+ for m in URL_RE.finditer(text):
59
+ spans.append((m.start(), m.end(), "URL"))
60
+ for m in MAC_RE.finditer(text):
61
+ spans.append((m.start(), m.end(), "IP_ADDRESS"))
62
+ for m in IPV6_RE.finditer(text):
63
+ spans.append((m.start(), m.end(), "IP_ADDRESS"))
64
+ for m in IPV4_RE.finditer(text):
65
+ if _ipv4_ok(m.group(0)):
66
+ spans.append((m.start(), m.end(), "IP_ADDRESS"))
67
+ for m in SSN_RE.finditer(text):
68
+ if _ssn_ok(m.group(1), m.group(2), m.group(3)):
69
+ spans.append((m.start(), m.end(), "SSN"))
70
+ for m in CARD_RE.finditer(text):
71
+ digits = re.sub(r"\D", "", m.group(0))
72
+ if _luhn_ok(digits):
73
+ spans.append((m.start(), m.end(), "CREDIT_CARD"))
74
+
75
+ return spans
76
+
77
+
78
+ def _overlaps(a: Span, b: Span) -> bool:
79
+ return a[0] < b[1] and b[0] < a[1]
80
+
81
+
82
+ def union(model_spans: List[Span], rule_spans: List[Span]) -> List[Span]:
83
+ """Union the two layers; the deterministic layer wins on any overlap."""
84
+ final = list(rule_spans)
85
+ for ms in model_spans:
86
+ if not any(_overlaps(ms, rs) for rs in rule_spans):
87
+ final.append(ms)
88
+ final.sort(key=lambda s: s[0])
89
+ return final
90
+
91
+
92
+ def redact(text: str, spans: List[Span]) -> str:
93
+ out, last = [], 0
94
+ for s, e, label in sorted(spans, key=lambda x: x[0]):
95
+ if s < last:
96
+ continue
97
+ out.append(text[last:s])
98
+ out.append(f"[{label}]")
99
+ last = e
100
+ out.append(text[last:])
101
+ return "".join(out)