admina-core 0.13.2__tar.gz → 0.14.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {admina_core-0.13.2 → admina_core-0.14.1}/Cargo.lock +1 -1
- {admina_core-0.13.2 → admina_core-0.14.1}/Cargo.toml +1 -1
- {admina_core-0.13.2 → admina_core-0.14.1}/PKG-INFO +2 -2
- {admina_core-0.13.2 → admina_core-0.14.1}/README.md +1 -1
- {admina_core-0.13.2 → admina_core-0.14.1}/pyproject.toml +1 -1
- {admina_core-0.13.2 → admina_core-0.14.1}/src/pii.rs +139 -11
- {admina_core-0.13.2 → admina_core-0.14.1}/uv.lock +1 -1
- {admina_core-0.13.2 → admina_core-0.14.1}/.cargo/config.toml +0 -0
- {admina_core-0.13.2 → admina_core-0.14.1}/src/firewall.rs +0 -0
- {admina_core-0.13.2 → admina_core-0.14.1}/src/forensic.rs +0 -0
- {admina_core-0.13.2 → admina_core-0.14.1}/src/lib.rs +0 -0
- {admina_core-0.13.2 → admina_core-0.14.1}/src/loop_breaker.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: admina-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.1
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -46,7 +46,7 @@ pip install admina-core
|
|
|
46
46
|
|
|
47
47
|
The Python framework auto-detects the Rust engine at runtime and falls
|
|
48
48
|
back to a pure-Python implementation if `admina-core` is not installed,
|
|
49
|
-
so `admina
|
|
49
|
+
so `admina` always works on its own.
|
|
50
50
|
|
|
51
51
|
## License
|
|
52
52
|
|
|
@@ -17,7 +17,7 @@ pip install admina-core
|
|
|
17
17
|
|
|
18
18
|
The Python framework auto-detects the Rust engine at runtime and falls
|
|
19
19
|
back to a pure-Python implementation if `admina-core` is not installed,
|
|
20
|
-
so `admina
|
|
20
|
+
so `admina` always works on its own.
|
|
21
21
|
|
|
22
22
|
## License
|
|
23
23
|
|
|
@@ -19,7 +19,7 @@ build-backend = "maturin"
|
|
|
19
19
|
|
|
20
20
|
[project]
|
|
21
21
|
name = "admina-core"
|
|
22
|
-
version = "0.
|
|
22
|
+
version = "0.14.1"
|
|
23
23
|
description = "Admina governance engines — Rust core with Python bindings (firewall, PII, loop breaker, hash chain)"
|
|
24
24
|
readme = "README.md"
|
|
25
25
|
requires-python = ">=3.11"
|
|
@@ -13,13 +13,84 @@
|
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
15
|
use pyo3::prelude::*;
|
|
16
|
-
use regex::Regex;
|
|
16
|
+
use regex::{Captures, Regex};
|
|
17
17
|
use std::sync::OnceLock;
|
|
18
18
|
|
|
19
19
|
struct PiiPattern {
|
|
20
20
|
name: &'static str,
|
|
21
21
|
regex: Regex,
|
|
22
22
|
mask: &'static str,
|
|
23
|
+
/// Whether a match is reported, given the scanned text: the checks the
|
|
24
|
+
/// regex cannot express (a checksum, the characters around the match).
|
|
25
|
+
accept: fn(&str, &Captures) -> bool,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn any_match(_text: &str, _caps: &Captures) -> bool {
|
|
29
|
+
true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// A card number is reported only when its digits pass the Luhn checksum,
|
|
33
|
+
/// as in the Python PII redactor.
|
|
34
|
+
fn luhn_valid(_text: &str, caps: &Captures) -> bool {
|
|
35
|
+
let digits: Vec<u32> = caps[0].chars().filter_map(|c| c.to_digit(10)).collect();
|
|
36
|
+
let sum: u32 = digits
|
|
37
|
+
.iter()
|
|
38
|
+
.rev()
|
|
39
|
+
.enumerate()
|
|
40
|
+
.map(|(i, &d)| {
|
|
41
|
+
if !i.is_multiple_of(2) {
|
|
42
|
+
let x = d * 2;
|
|
43
|
+
if x > 9 {
|
|
44
|
+
x - 9
|
|
45
|
+
} else {
|
|
46
|
+
x
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
d
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.sum();
|
|
53
|
+
!digits.is_empty() && sum.is_multiple_of(10)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn is_word(c: char) -> bool {
|
|
57
|
+
c.is_alphanumeric() || c == '_'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// The boundaries of the phone patterns of the Python PII redactor, which
|
|
61
|
+
/// the regex crate cannot express as lookarounds: a North American number
|
|
62
|
+
/// is not preceded or followed by a digit; an Italian number is not
|
|
63
|
+
/// preceded by a word character or "+", nor followed by a word character.
|
|
64
|
+
fn phone_boundaries(text: &str, caps: &Captures) -> bool {
|
|
65
|
+
let whole = caps.get(0).unwrap();
|
|
66
|
+
let before = text[..whole.start()].chars().next_back();
|
|
67
|
+
let after = text[whole.end()..].chars().next();
|
|
68
|
+
if caps.name("us").is_some() {
|
|
69
|
+
!before.is_some_and(|c| c.is_ascii_digit()) && !after.is_some_and(|c| c.is_ascii_digit())
|
|
70
|
+
} else {
|
|
71
|
+
!before.is_some_and(|c| is_word(c) || c == '+') && !after.is_some_and(is_word)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Phone numbers, as in the Python PII redactor: North American format
|
|
76
|
+
// (3-3-4 digits, optional country code), and Italian numbers, with +39 or
|
|
77
|
+
// 0039 or without: mobile numbers (3 and 8 or 9 more digits, compact or in
|
|
78
|
+
// groups) and landline numbers (an area code 02, 06 or 0 followed by 2 or 3
|
|
79
|
+
// digits, then 5 to 8 digits, or 2 or 3 groups of 2 to 4 digits separated
|
|
80
|
+
// by spaces; without +39 a space, "/" or "-" follows the area code).
|
|
81
|
+
const US_PHONE: &str = r"(?:\+\d{1,3}[\s.\-]?)?\(?\d{3}\)?[\s.\-]\d{3}[\s.\-]\d{4}";
|
|
82
|
+
const IT_MOBILE: &str = r"3[1-9]\d(?:[ \-]?\d{6,7}|[ \-]\d{3}[ \-]\d{3,4})";
|
|
83
|
+
const IT_AREA: &str = r"0(?:[26]|[1-9]\d{1,2})";
|
|
84
|
+
const IT_SUBSCRIBER: &str = r"(?:\d{5,8}|\d{2,4} \d{2,4}(?: \d{2,4})?)";
|
|
85
|
+
|
|
86
|
+
fn phone_regex() -> Regex {
|
|
87
|
+
let italian = format!(
|
|
88
|
+
"(?:(?:\\+|00)39[ .\\-]?(?:{m}|{a}[ ./\\-]?{s})|{m}|{a}[ /\\-]{s})",
|
|
89
|
+
m = IT_MOBILE,
|
|
90
|
+
a = IT_AREA,
|
|
91
|
+
s = IT_SUBSCRIBER
|
|
92
|
+
);
|
|
93
|
+
Regex::new(&format!("(?P<us>{US_PHONE})|(?P<it>{italian})")).unwrap()
|
|
23
94
|
}
|
|
24
95
|
|
|
25
96
|
static PII_PATTERNS: OnceLock<Vec<PiiPattern>> = OnceLock::new();
|
|
@@ -31,29 +102,31 @@ fn get_pii_patterns() -> &'static Vec<PiiPattern> {
|
|
|
31
102
|
name: "email",
|
|
32
103
|
regex: Regex::new(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}").unwrap(),
|
|
33
104
|
mask: "[EMAIL_REDACTED]",
|
|
105
|
+
accept: any_match,
|
|
34
106
|
},
|
|
35
107
|
PiiPattern {
|
|
36
108
|
name: "credit_card",
|
|
37
109
|
regex: Regex::new(r"\b(?:\d{4}[\s\-]?){3}\d{4}\b").unwrap(),
|
|
38
110
|
mask: "[CC_REDACTED]",
|
|
111
|
+
accept: luhn_valid,
|
|
39
112
|
},
|
|
40
113
|
PiiPattern {
|
|
41
114
|
name: "ssn",
|
|
42
115
|
regex: Regex::new(r"\b\d{3}-\d{2}-\d{4}\b").unwrap(),
|
|
43
116
|
mask: "[SSN_REDACTED]",
|
|
117
|
+
accept: any_match,
|
|
44
118
|
},
|
|
45
119
|
PiiPattern {
|
|
46
120
|
name: "phone",
|
|
47
|
-
regex:
|
|
48
|
-
r"(?:\+?\d{1,3}[\s\-.]?)?\(?\d{2,4}\)?[\s\-.]?\d{3,4}[\s\-.]?\d{3,4}\b",
|
|
49
|
-
)
|
|
50
|
-
.unwrap(),
|
|
121
|
+
regex: phone_regex(),
|
|
51
122
|
mask: "[PHONE_REDACTED]",
|
|
123
|
+
accept: phone_boundaries,
|
|
52
124
|
},
|
|
53
125
|
PiiPattern {
|
|
54
126
|
name: "iban",
|
|
55
127
|
regex: Regex::new(r"\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}\b").unwrap(),
|
|
56
128
|
mask: "[IBAN_REDACTED]",
|
|
129
|
+
accept: any_match,
|
|
57
130
|
},
|
|
58
131
|
PiiPattern {
|
|
59
132
|
name: "ip_address",
|
|
@@ -62,6 +135,7 @@ fn get_pii_patterns() -> &'static Vec<PiiPattern> {
|
|
|
62
135
|
)
|
|
63
136
|
.unwrap(),
|
|
64
137
|
mask: "[IP_REDACTED]",
|
|
138
|
+
accept: any_match,
|
|
65
139
|
},
|
|
66
140
|
]
|
|
67
141
|
})
|
|
@@ -123,10 +197,18 @@ impl RustPiiScanner {
|
|
|
123
197
|
let mut details: Vec<(String, usize)> = Vec::new();
|
|
124
198
|
|
|
125
199
|
for pattern in patterns.iter() {
|
|
126
|
-
let
|
|
127
|
-
let match_count =
|
|
200
|
+
let current = result.clone();
|
|
201
|
+
let mut match_count: usize = 0;
|
|
202
|
+
let replaced = pattern.regex.replace_all(¤t, |caps: &Captures| {
|
|
203
|
+
if (pattern.accept)(¤t, caps) {
|
|
204
|
+
match_count += 1;
|
|
205
|
+
pattern.mask.to_string()
|
|
206
|
+
} else {
|
|
207
|
+
caps[0].to_string()
|
|
208
|
+
}
|
|
209
|
+
});
|
|
128
210
|
if match_count > 0 {
|
|
129
|
-
result =
|
|
211
|
+
result = replaced.into_owned();
|
|
130
212
|
count += match_count;
|
|
131
213
|
categories.push(pattern.name.to_string());
|
|
132
214
|
details.push((pattern.name.to_string(), match_count));
|
|
@@ -151,7 +233,11 @@ impl RustPiiScanner {
|
|
|
151
233
|
let mut details: Vec<(String, usize)> = Vec::new();
|
|
152
234
|
|
|
153
235
|
for pattern in patterns.iter() {
|
|
154
|
-
let match_count = pattern
|
|
236
|
+
let match_count = pattern
|
|
237
|
+
.regex
|
|
238
|
+
.captures_iter(text)
|
|
239
|
+
.filter(|caps| (pattern.accept)(text, caps))
|
|
240
|
+
.count();
|
|
155
241
|
if match_count > 0 {
|
|
156
242
|
count += match_count;
|
|
157
243
|
categories.push(pattern.name.to_string());
|
|
@@ -203,8 +289,50 @@ mod tests {
|
|
|
203
289
|
#[test]
|
|
204
290
|
fn test_credit_card_redaction() {
|
|
205
291
|
let mut s = RustPiiScanner::new();
|
|
206
|
-
let r = s.redact("Card: 4111-
|
|
207
|
-
assert!(!r.redacted_text.contains("4111-
|
|
292
|
+
let r = s.redact("Card: 4111-1111-1111-1111");
|
|
293
|
+
assert!(!r.redacted_text.contains("4111-1111-1111-1111"));
|
|
294
|
+
assert!(r.categories.contains(&"credit_card".to_string()));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#[test]
|
|
298
|
+
fn test_card_number_failing_luhn_is_not_a_card() {
|
|
299
|
+
let mut s = RustPiiScanner::new();
|
|
300
|
+
let r = s.redact("order 4111 1111 1111 1112 was cancelled");
|
|
301
|
+
assert!(r.redacted_text.contains("4111 1111 1111 1112"));
|
|
302
|
+
assert_eq!(r.count, 0);
|
|
303
|
+
assert_eq!(s.scan("order 4111 1111 1111 1112").count, 0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn test_phone_numbers_are_redacted() {
|
|
308
|
+
let mut s = RustPiiScanner::new();
|
|
309
|
+
for text in [
|
|
310
|
+
"call 555-123-4567 today",
|
|
311
|
+
"call +1 555-123-4567 today",
|
|
312
|
+
"chiama il 333 1234567",
|
|
313
|
+
"chiama +39 06 1234 5678",
|
|
314
|
+
"ufficio 02/12345678",
|
|
315
|
+
] {
|
|
316
|
+
let r = s.redact(text);
|
|
317
|
+
assert!(
|
|
318
|
+
r.redacted_text.contains("[PHONE_REDACTED]"),
|
|
319
|
+
"{text}: {}",
|
|
320
|
+
r.redacted_text
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[test]
|
|
326
|
+
fn test_codes_that_are_not_phone_numbers() {
|
|
327
|
+
let s = RustPiiScanner::new();
|
|
328
|
+
for text in [
|
|
329
|
+
"ISBN 978-3-16-148410-0 is out of print",
|
|
330
|
+
"build 20261008.1 passed all 4554 checks",
|
|
331
|
+
"upgrade to version 2.14.3",
|
|
332
|
+
"id1234567890",
|
|
333
|
+
] {
|
|
334
|
+
assert_eq!(s.scan(text).count, 0, "{text}");
|
|
335
|
+
}
|
|
208
336
|
}
|
|
209
337
|
|
|
210
338
|
#[test]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|