datafog-core 0.3.1__tar.gz → 0.4.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.
- {datafog_core-0.3.1 → datafog_core-0.4.1}/Cargo.lock +4 -4
- {datafog_core-0.3.1 → datafog_core-0.4.1}/PKG-INFO +42 -3
- {datafog_core-0.3.1/crates/core → datafog_core-0.4.1}/README.md +41 -2
- {datafog_core-0.3.1 → datafog_core-0.4.1}/bindings/python/Cargo.toml +1 -1
- {datafog_core-0.3.1 → datafog_core-0.4.1}/bindings/python/src/lib.rs +9 -0
- datafog_core-0.4.1/bindings/python/tests/api_key_conformance.py +122 -0
- datafog_core-0.4.1/bindings/python/tests/bearer_token_conformance.py +134 -0
- datafog_core-0.4.1/bindings/python/tests/capabilities_conformance.py +86 -0
- datafog_core-0.4.1/bindings/python/tests/credential_overlap_conformance.py +103 -0
- datafog_core-0.4.1/bindings/python/tests/credential_uri_conformance.py +127 -0
- datafog_core-0.4.1/bindings/python/tests/german_conformance.py +180 -0
- datafog_core-0.4.1/bindings/python/tests/jwt_conformance.py +125 -0
- datafog_core-0.4.1/bindings/python/tests/npi_conformance.py +124 -0
- datafog_core-0.4.1/bindings/python/tests/private_key_conformance.py +122 -0
- datafog_core-0.4.1/bindings/python/tests/synthetic_fixtures.py +24 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/bindings/python/tests/test_installed.py +33 -0
- datafog_core-0.4.1/bindings/python/tests/test_python49_integration.py +204 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/bindings/python/tests/test_typing.py +10 -4
- datafog_core-0.4.1/bindings/python/tests/us_routing_number_conformance.py +124 -0
- datafog_core-0.4.1/bindings/python/tests/uuid_conformance.py +129 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/crates/core/Cargo.toml +1 -1
- {datafog_core-0.3.1 → datafog_core-0.4.1/crates/core}/README.md +41 -2
- datafog_core-0.4.1/crates/core/examples/german_benchmark.rs +47 -0
- datafog_core-0.4.1/crates/core/src/api_key.rs +66 -0
- datafog_core-0.4.1/crates/core/src/bearer_token.rs +87 -0
- datafog_core-0.4.1/crates/core/src/capabilities.rs +216 -0
- datafog_core-0.4.1/crates/core/src/credential_uri.rs +200 -0
- datafog_core-0.4.1/crates/core/src/german.rs +90 -0
- datafog_core-0.4.1/crates/core/src/jwt.rs +66 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/crates/core/src/lib.rs +403 -11
- datafog_core-0.4.1/crates/core/src/npi.rs +40 -0
- datafog_core-0.4.1/crates/core/src/private_key.rs +62 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/crates/core/src/structured.rs +28 -5
- datafog_core-0.4.1/crates/core/src/us_routing_number.rs +40 -0
- datafog_core-0.4.1/crates/core/src/uuid.rs +42 -0
- datafog_core-0.4.1/crates/core/tests/api_key.rs +120 -0
- datafog_core-0.4.1/crates/core/tests/bearer_token.rs +113 -0
- datafog_core-0.4.1/crates/core/tests/capabilities.rs +216 -0
- datafog_core-0.4.1/crates/core/tests/credential_overlaps.rs +113 -0
- datafog_core-0.4.1/crates/core/tests/credential_uri.rs +96 -0
- datafog_core-0.4.1/crates/core/tests/german.rs +182 -0
- datafog_core-0.4.1/crates/core/tests/jwt.rs +82 -0
- datafog_core-0.4.1/crates/core/tests/npi.rs +93 -0
- datafog_core-0.4.1/crates/core/tests/private_key.rs +109 -0
- datafog_core-0.4.1/crates/core/tests/support/synthetic_fixtures.rs +32 -0
- datafog_core-0.4.1/crates/core/tests/us_routing_number.rs +93 -0
- datafog_core-0.4.1/crates/core/tests/uuid.rs +117 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/pyproject.toml +1 -1
- {datafog_core-0.3.1 → datafog_core-0.4.1}/python/datafog_core/__init__.py +2 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/python/datafog_core/__init__.pyi +23 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/Cargo.toml +0 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/bindings/python/tests/requirements-typing.txt +0 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/crates/core/examples/scan_benchmark.rs +0 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/crates/core/src/offsets.rs +0 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/crates/core/src/selection_tests.rs +0 -0
- {datafog_core-0.3.1 → datafog_core-0.4.1}/python/datafog_core/py.typed +0 -0
|
@@ -80,7 +80,7 @@ checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d"
|
|
|
80
80
|
|
|
81
81
|
[[package]]
|
|
82
82
|
name = "datafog-core"
|
|
83
|
-
version = "0.
|
|
83
|
+
version = "0.4.1"
|
|
84
84
|
dependencies = [
|
|
85
85
|
"base64",
|
|
86
86
|
"futures",
|
|
@@ -94,7 +94,7 @@ dependencies = [
|
|
|
94
94
|
|
|
95
95
|
[[package]]
|
|
96
96
|
name = "datafog-core-python"
|
|
97
|
-
version = "0.
|
|
97
|
+
version = "0.4.1"
|
|
98
98
|
dependencies = [
|
|
99
99
|
"datafog-core",
|
|
100
100
|
"pyo3",
|
|
@@ -104,7 +104,7 @@ dependencies = [
|
|
|
104
104
|
|
|
105
105
|
[[package]]
|
|
106
106
|
name = "datafog-node"
|
|
107
|
-
version = "0.
|
|
107
|
+
version = "0.4.1"
|
|
108
108
|
dependencies = [
|
|
109
109
|
"datafog-core",
|
|
110
110
|
"napi",
|
|
@@ -115,7 +115,7 @@ dependencies = [
|
|
|
115
115
|
|
|
116
116
|
[[package]]
|
|
117
117
|
name = "datafog-wasm"
|
|
118
|
-
version = "0.
|
|
118
|
+
version = "0.4.1"
|
|
119
119
|
dependencies = [
|
|
120
120
|
"datafog-core",
|
|
121
121
|
"serde",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datafog-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: License :: OSI Approved :: MIT License
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -25,7 +25,7 @@ Project-URL: Repository, https://github.com/DataFog/datafog-core
|
|
|
25
25
|
|
|
26
26
|
Fast structured PII detection, implemented in Rust and exposed for Rust, Python, Node.js, and browsers.
|
|
27
27
|
|
|
28
|
-
It detects `EMAIL`, `PHONE`, `SSN`, `CREDIT_CARD`, `IP_ADDRESS`, `DATE`, and `ZIP_CODE`. Every binding returns the same finding information:
|
|
28
|
+
It detects `EMAIL`, `PHONE`, `SSN`, `CREDIT_CARD`, `IP_ADDRESS`, `DATE`, and `ZIP_CODE`. Version 0.4.0 also detects `JWT` tokens by default; see the [JWT reference](docs/reference/jwt.mdx). With an explicit German locale, it also detects `DE_IBAN`, `DE_VAT_ID`, `DE_TAX_ID`, `DE_SOCIAL_SECURITY_NUMBER`, `DE_POSTAL_CODE`, `DE_PASSPORT_NUMBER`, and `DE_RESIDENCE_PERMIT_NUMBER`. Complete PEM private-key blocks are also detected as `PRIVATE_KEY` by default; see the [private-key reference](docs/reference/private-keys.mdx). Context-labeled `US_ROUTING_NUMBER` detection is also available; see [the detector rules](docs/reference/us-routing-number.mdx). Context-labeled `NPI` detection is also available; see [the detector rules](docs/reference/npi.mdx). Every binding returns the same finding information:
|
|
29
29
|
|
|
30
30
|
```text
|
|
31
31
|
entity type, matched text, byte range, code-point range,
|
|
@@ -79,7 +79,7 @@ full-match regex values:
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
`scan_and_transform` uses `{ scan?: { locale?: string }, transform: ... }` so
|
|
82
|
+
`scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean }, transform: ... }` so
|
|
83
83
|
detection settings remain separate from transformation policy.
|
|
84
84
|
|
|
85
85
|
## Packages
|
|
@@ -297,3 +297,42 @@ fixtures/ Shared conformance fixtures
|
|
|
297
297
|
|
|
298
298
|
[MIT](LICENSE)
|
|
299
299
|
|
|
300
|
+
## German structured identifiers
|
|
301
|
+
|
|
302
|
+
Pass `{"locale":"de"}` to text or structured scans. Trimmed, ASCII
|
|
303
|
+
case-insensitive `de`, `de-DE`, and `de_DE` activate all seven German detectors;
|
|
304
|
+
omitted locale and recognized `en-US`/`fr` aliases keep base detection only.
|
|
305
|
+
Version 0.4.0 rejects unsupported explicit locales.
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from datafog_core import scan_and_transform
|
|
309
|
+
|
|
310
|
+
result = scan_and_transform("IBAN DE44 5001 0517 5407 3249 31", {
|
|
311
|
+
"scan": {"locale": "de"},
|
|
312
|
+
"transform": {"default": {"strategy": "redact"}, "entities": ["DE_IBAN"]},
|
|
313
|
+
})
|
|
314
|
+
assert result.text == "IBAN [DE_IBAN]"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
These are format/context detectors, not official identifier validators. IBAN
|
|
318
|
+
checksums and account existence are not checked. Digits are ASCII; permitted
|
|
319
|
+
internal separators are space, tab, NBSP and narrow NBSP at specified group
|
|
320
|
+
boundaries, never newlines. Returned text and offsets preserve the source.
|
|
321
|
+
Passport and residence-permit patterns are legacy heuristics with limited
|
|
322
|
+
coverage. See the [German entity reference](docs/reference/german-entities.mdx)
|
|
323
|
+
and [migration differences](docs/guides/migrating-from-datafog-python.mdx).
|
|
324
|
+
Core 0.4.0 is published. The higher-level Python adapter and its dependency
|
|
325
|
+
update remain separate and unreleased in [draft PR #179](https://github.com/DataFog/datafog-python/pull/179).
|
|
326
|
+
|
|
327
|
+
## UUID identifiers
|
|
328
|
+
|
|
329
|
+
Canonical UUID detection is opt-in: pass `{"detect_uuid":true}` to text or
|
|
330
|
+
structured scans, independently of locale. It emits `UUID` findings for versions
|
|
331
|
+
1–8 with the IETF variant and original casing/ranges. UUID syntax does not imply
|
|
332
|
+
sensitivity. See the [UUID reference](docs/reference/uuid.mdx) for boundaries,
|
|
333
|
+
excluded sentinel forms and transformation examples.
|
|
334
|
+
|
|
335
|
+
**0.4.0 is published** for Rust, Python, Node.js, and WASM. The higher-level Python adapter remains a separate unreleased integration in [draft PR #179](https://github.com/DataFog/datafog-python/pull/179). See the [release notes](docs/releases/0-4-0.mdx), [runtime capabilities](docs/reference/capabilities.mdx), and [0.4.x compatibility policy](docs/reference/compatibility.mdx).
|
|
336
|
+
|
|
337
|
+
The unreleased **0.4.1 candidate** adds three default detectors: provider-prefixed [`API_KEY`](docs/reference/api-keys.mdx), context-required [`BEARER_TOKEN`](docs/reference/bearer-token.mdx), and whole PostgreSQL [`CREDENTIAL_URI`](docs/reference/credential-uri.mdx). The registry reports 23 supported entities and 14 defaults with capability contract 1. See the [candidate release notes](docs/releases/0-4-1.mdx) for exact scopes, preserved legacy Python overlap behavior, and the required exact-wheel Python 4.9 integration gate. Python's default backend is unchanged.
|
|
338
|
+
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Fast structured PII detection, implemented in Rust and exposed for Rust, Python, Node.js, and browsers.
|
|
4
4
|
|
|
5
|
-
It detects `EMAIL`, `PHONE`, `SSN`, `CREDIT_CARD`, `IP_ADDRESS`, `DATE`, and `ZIP_CODE`. Every binding returns the same finding information:
|
|
5
|
+
It detects `EMAIL`, `PHONE`, `SSN`, `CREDIT_CARD`, `IP_ADDRESS`, `DATE`, and `ZIP_CODE`. Version 0.4.0 also detects `JWT` tokens by default; see the [JWT reference](docs/reference/jwt.mdx). With an explicit German locale, it also detects `DE_IBAN`, `DE_VAT_ID`, `DE_TAX_ID`, `DE_SOCIAL_SECURITY_NUMBER`, `DE_POSTAL_CODE`, `DE_PASSPORT_NUMBER`, and `DE_RESIDENCE_PERMIT_NUMBER`. Complete PEM private-key blocks are also detected as `PRIVATE_KEY` by default; see the [private-key reference](docs/reference/private-keys.mdx). Context-labeled `US_ROUTING_NUMBER` detection is also available; see [the detector rules](docs/reference/us-routing-number.mdx). Context-labeled `NPI` detection is also available; see [the detector rules](docs/reference/npi.mdx). Every binding returns the same finding information:
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
8
|
entity type, matched text, byte range, code-point range,
|
|
@@ -56,7 +56,7 @@ full-match regex values:
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
`scan_and_transform` uses `{ scan?: { locale?: string }, transform: ... }` so
|
|
59
|
+
`scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean }, transform: ... }` so
|
|
60
60
|
detection settings remain separate from transformation policy.
|
|
61
61
|
|
|
62
62
|
## Packages
|
|
@@ -273,3 +273,42 @@ fixtures/ Shared conformance fixtures
|
|
|
273
273
|
## License
|
|
274
274
|
|
|
275
275
|
[MIT](LICENSE)
|
|
276
|
+
|
|
277
|
+
## German structured identifiers
|
|
278
|
+
|
|
279
|
+
Pass `{"locale":"de"}` to text or structured scans. Trimmed, ASCII
|
|
280
|
+
case-insensitive `de`, `de-DE`, and `de_DE` activate all seven German detectors;
|
|
281
|
+
omitted locale and recognized `en-US`/`fr` aliases keep base detection only.
|
|
282
|
+
Version 0.4.0 rejects unsupported explicit locales.
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from datafog_core import scan_and_transform
|
|
286
|
+
|
|
287
|
+
result = scan_and_transform("IBAN DE44 5001 0517 5407 3249 31", {
|
|
288
|
+
"scan": {"locale": "de"},
|
|
289
|
+
"transform": {"default": {"strategy": "redact"}, "entities": ["DE_IBAN"]},
|
|
290
|
+
})
|
|
291
|
+
assert result.text == "IBAN [DE_IBAN]"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
These are format/context detectors, not official identifier validators. IBAN
|
|
295
|
+
checksums and account existence are not checked. Digits are ASCII; permitted
|
|
296
|
+
internal separators are space, tab, NBSP and narrow NBSP at specified group
|
|
297
|
+
boundaries, never newlines. Returned text and offsets preserve the source.
|
|
298
|
+
Passport and residence-permit patterns are legacy heuristics with limited
|
|
299
|
+
coverage. See the [German entity reference](docs/reference/german-entities.mdx)
|
|
300
|
+
and [migration differences](docs/guides/migrating-from-datafog-python.mdx).
|
|
301
|
+
Core 0.4.0 is published. The higher-level Python adapter and its dependency
|
|
302
|
+
update remain separate and unreleased in [draft PR #179](https://github.com/DataFog/datafog-python/pull/179).
|
|
303
|
+
|
|
304
|
+
## UUID identifiers
|
|
305
|
+
|
|
306
|
+
Canonical UUID detection is opt-in: pass `{"detect_uuid":true}` to text or
|
|
307
|
+
structured scans, independently of locale. It emits `UUID` findings for versions
|
|
308
|
+
1–8 with the IETF variant and original casing/ranges. UUID syntax does not imply
|
|
309
|
+
sensitivity. See the [UUID reference](docs/reference/uuid.mdx) for boundaries,
|
|
310
|
+
excluded sentinel forms and transformation examples.
|
|
311
|
+
|
|
312
|
+
**0.4.0 is published** for Rust, Python, Node.js, and WASM. The higher-level Python adapter remains a separate unreleased integration in [draft PR #179](https://github.com/DataFog/datafog-python/pull/179). See the [release notes](docs/releases/0-4-0.mdx), [runtime capabilities](docs/reference/capabilities.mdx), and [0.4.x compatibility policy](docs/reference/compatibility.mdx).
|
|
313
|
+
|
|
314
|
+
The unreleased **0.4.1 candidate** adds three default detectors: provider-prefixed [`API_KEY`](docs/reference/api-keys.mdx), context-required [`BEARER_TOKEN`](docs/reference/bearer-token.mdx), and whole PostgreSQL [`CREDENTIAL_URI`](docs/reference/credential-uri.mdx). The registry reports 23 supported entities and 14 defaults with capability contract 1. See the [candidate release notes](docs/releases/0-4-1.mdx) for exact scopes, preserved legacy Python overlap behavior, and the required exact-wheel Python 4.9 integration gate. Python's default backend is unchanged.
|
|
@@ -956,6 +956,14 @@ fn privacy_error(py: Python<'_>, error: core::PrivacyError) -> PyErr {
|
|
|
956
956
|
exception
|
|
957
957
|
}
|
|
958
958
|
|
|
959
|
+
/// Return an independent, JSON-compatible snapshot of the Core capability contract.
|
|
960
|
+
#[pyfunction]
|
|
961
|
+
fn capabilities(py: Python<'_>) -> PyResult<Py<PyAny>> {
|
|
962
|
+
let json = serde_json::to_string(&core::capabilities())
|
|
963
|
+
.map_err(|_| internal_error(py, "failed to serialize Core capabilities"))?;
|
|
964
|
+
Ok(py.import("json")?.call_method1("loads", (json,))?.unbind())
|
|
965
|
+
}
|
|
966
|
+
|
|
959
967
|
/// Scan text for supported PII findings.
|
|
960
968
|
#[pyfunction]
|
|
961
969
|
#[pyo3(signature = (text, config=None))]
|
|
@@ -1300,6 +1308,7 @@ fn datafog_core(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
1300
1308
|
module.add_class::<Restoration>()?;
|
|
1301
1309
|
module.add_class::<RestoreResult>()?;
|
|
1302
1310
|
module.add_class::<PrivacyManager>()?;
|
|
1311
|
+
module.add_function(wrap_pyfunction!(capabilities, module)?)?;
|
|
1303
1312
|
module.add_function(wrap_pyfunction!(scan, module)?)?;
|
|
1304
1313
|
module.add_function(wrap_pyfunction!(transform, module)?)?;
|
|
1305
1314
|
module.add_function(wrap_pyfunction!(scan_and_transform, module)?)?;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Shared API-key fixtures against the installed wheel, including provider round trips."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import datafog_core as api
|
|
6
|
+
from synthetic_fixtures import expand_fixture
|
|
7
|
+
|
|
8
|
+
RECORDS = [
|
|
9
|
+
expand_fixture(json.loads(line))
|
|
10
|
+
for line in (Path(__file__).resolve().parents[3] / "fixtures/api-key.jsonl")
|
|
11
|
+
.read_text()
|
|
12
|
+
.splitlines()
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def project(text, findings):
|
|
17
|
+
result = []
|
|
18
|
+
for f in findings:
|
|
19
|
+
if f.entity_type != "API_KEY":
|
|
20
|
+
continue
|
|
21
|
+
assert (
|
|
22
|
+
text.encode()[f.byte_range.start : f.byte_range.end].decode()
|
|
23
|
+
== f.matched_text
|
|
24
|
+
)
|
|
25
|
+
assert text[f.codepoint_range.start : f.codepoint_range.end] == f.matched_text
|
|
26
|
+
assert f.confidence is None
|
|
27
|
+
assert f.detector_name == "datafog-core/" + f.entity_type.lower().replace(
|
|
28
|
+
"_", "-"
|
|
29
|
+
)
|
|
30
|
+
assert f.detector_version
|
|
31
|
+
result.append(
|
|
32
|
+
dict(
|
|
33
|
+
label=f.entity_type,
|
|
34
|
+
text=f.matched_text,
|
|
35
|
+
start=f.codepoint_range.start,
|
|
36
|
+
end=f.codepoint_range.end,
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def verify():
|
|
43
|
+
for row in RECORDS:
|
|
44
|
+
findings = api.scan(row["text"], row["config"])
|
|
45
|
+
assert project(row["text"], findings) == row["entities"], row["id"]
|
|
46
|
+
data = {"a/b": row["text"], "array": [row["text"]], "z": row["text"]}
|
|
47
|
+
located = api.scan_structured(data, row["config"]).findings
|
|
48
|
+
for path in ["/a~1b", "/array/0", "/z"]:
|
|
49
|
+
assert (
|
|
50
|
+
project(row["text"], [f.finding for f in located if f.path == path])
|
|
51
|
+
== row["entities"]
|
|
52
|
+
), row["id"]
|
|
53
|
+
for case in row.get("transforms", []):
|
|
54
|
+
result = api.scan_and_transform(
|
|
55
|
+
row["text"], {"scan": row["config"], "transform": case["config"]}
|
|
56
|
+
)
|
|
57
|
+
assert result.text == case["text"], row["id"]
|
|
58
|
+
assert api.transform(row["text"], findings, case["config"]) == result
|
|
59
|
+
for t in result.transformations:
|
|
60
|
+
assert (
|
|
61
|
+
result.text.encode()[
|
|
62
|
+
t.output_byte_range.start : t.output_byte_range.end
|
|
63
|
+
].decode()
|
|
64
|
+
== t.replacement
|
|
65
|
+
)
|
|
66
|
+
assert (
|
|
67
|
+
result.text[
|
|
68
|
+
t.output_codepoint_range.start : t.output_codepoint_range.end
|
|
69
|
+
]
|
|
70
|
+
== t.replacement
|
|
71
|
+
)
|
|
72
|
+
assert any(
|
|
73
|
+
f.byte_range == t.source_byte_range
|
|
74
|
+
and f.entity_type == t.entity_type
|
|
75
|
+
for f in findings
|
|
76
|
+
)
|
|
77
|
+
assert not hasattr(t, "matched_text") and not hasattr(t, "finding")
|
|
78
|
+
structured = api.scan_and_transform_structured(
|
|
79
|
+
data, {"scan": row["config"], "transform": case["config"]}
|
|
80
|
+
)
|
|
81
|
+
assert structured.data == {
|
|
82
|
+
"a/b": case["text"],
|
|
83
|
+
"array": [case["text"]],
|
|
84
|
+
"z": case["text"],
|
|
85
|
+
}
|
|
86
|
+
explicit = api.transform_structured(data, located, case["config"])
|
|
87
|
+
assert explicit.data == structured.data
|
|
88
|
+
assert [(t.path, t.transformation) for t in explicit.transformations] == [
|
|
89
|
+
(t.path, t.transformation) for t in structured.transformations
|
|
90
|
+
]
|
|
91
|
+
async def verify_providers(manager, token_manager, context):
|
|
92
|
+
for row in RECORDS:
|
|
93
|
+
if not row.get("sample"):
|
|
94
|
+
continue
|
|
95
|
+
entities = list(dict.fromkeys(e["label"] for e in row["entities"]))
|
|
96
|
+
pseudonyms = await manager.scan_and_transform(
|
|
97
|
+
row["text"],
|
|
98
|
+
{
|
|
99
|
+
"scan": row["config"],
|
|
100
|
+
"transform": {
|
|
101
|
+
"default": {"strategy": "pseudonymize", "key_ref": "german"},
|
|
102
|
+
"entities": entities,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
assert (
|
|
107
|
+
len(pseudonyms.transformations) == len(row["entities"])
|
|
108
|
+
and pseudonyms.text != row["text"]
|
|
109
|
+
)
|
|
110
|
+
tokens = await token_manager.scan_and_transform(
|
|
111
|
+
row["text"],
|
|
112
|
+
{
|
|
113
|
+
"scan": row["config"],
|
|
114
|
+
"transform": {
|
|
115
|
+
"default": {"strategy": "tokenize", "token_ref": "german"},
|
|
116
|
+
"entities": entities,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
context,
|
|
120
|
+
)
|
|
121
|
+
assert len(tokens.transformations) == len(row["entities"])
|
|
122
|
+
assert (await token_manager.restore(tokens.text, context)).text == row["text"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Shared BEARER_TOKEN fixtures against the installed wheel, including provider round trips."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import datafog_core as api
|
|
6
|
+
|
|
7
|
+
RECORDS = [
|
|
8
|
+
json.loads(line)
|
|
9
|
+
for line in (Path(__file__).resolve().parents[3] / "fixtures/bearer-token.jsonl")
|
|
10
|
+
.read_text()
|
|
11
|
+
.splitlines()
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def project(text, findings):
|
|
16
|
+
result = []
|
|
17
|
+
for f in findings:
|
|
18
|
+
if not f.entity_type == "BEARER_TOKEN":
|
|
19
|
+
continue
|
|
20
|
+
assert (
|
|
21
|
+
text.encode()[f.byte_range.start : f.byte_range.end].decode()
|
|
22
|
+
== f.matched_text
|
|
23
|
+
)
|
|
24
|
+
assert text[f.codepoint_range.start : f.codepoint_range.end] == f.matched_text
|
|
25
|
+
assert f.confidence is None
|
|
26
|
+
assert f.detector_name == "datafog-core/" + f.entity_type.lower().replace(
|
|
27
|
+
"_", "-"
|
|
28
|
+
)
|
|
29
|
+
assert f.detector_version
|
|
30
|
+
result.append(
|
|
31
|
+
dict(
|
|
32
|
+
label=f.entity_type,
|
|
33
|
+
text=f.matched_text,
|
|
34
|
+
start=f.codepoint_range.start,
|
|
35
|
+
end=f.codepoint_range.end,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def verify():
|
|
42
|
+
for row in RECORDS:
|
|
43
|
+
findings = api.scan(row["text"], row["config"])
|
|
44
|
+
if "overlap" in row:
|
|
45
|
+
assert any(f.entity_type == row["overlap"] for f in findings)
|
|
46
|
+
assert project(row["text"], findings) == row["entities"], row["id"]
|
|
47
|
+
data = {"a/b": row["text"], "array": [row["text"]], "z": row["text"]}
|
|
48
|
+
located = api.scan_structured(data, row["config"]).findings
|
|
49
|
+
for path in ["/a~1b", "/array/0", "/z"]:
|
|
50
|
+
assert (
|
|
51
|
+
project(row["text"], [f.finding for f in located if f.path == path])
|
|
52
|
+
== row["entities"]
|
|
53
|
+
), row["id"]
|
|
54
|
+
for case in row.get("transforms", []):
|
|
55
|
+
result = api.scan_and_transform(
|
|
56
|
+
row["text"], {"scan": row["config"], "transform": case["config"]}
|
|
57
|
+
)
|
|
58
|
+
assert result.text == case["text"], row["id"]
|
|
59
|
+
assert api.transform(row["text"], findings, case["config"]) == result
|
|
60
|
+
for t in result.transformations:
|
|
61
|
+
assert (
|
|
62
|
+
result.text.encode()[
|
|
63
|
+
t.output_byte_range.start : t.output_byte_range.end
|
|
64
|
+
].decode()
|
|
65
|
+
== t.replacement
|
|
66
|
+
)
|
|
67
|
+
assert (
|
|
68
|
+
result.text[
|
|
69
|
+
t.output_codepoint_range.start : t.output_codepoint_range.end
|
|
70
|
+
]
|
|
71
|
+
== t.replacement
|
|
72
|
+
)
|
|
73
|
+
assert any(
|
|
74
|
+
f.byte_range == t.source_byte_range
|
|
75
|
+
and f.entity_type == t.entity_type
|
|
76
|
+
for f in findings
|
|
77
|
+
)
|
|
78
|
+
assert not hasattr(t, "matched_text") and not hasattr(t, "finding")
|
|
79
|
+
structured = api.scan_and_transform_structured(
|
|
80
|
+
data, {"scan": row["config"], "transform": case["config"]}
|
|
81
|
+
)
|
|
82
|
+
assert structured.data == {
|
|
83
|
+
"a/b": case["text"],
|
|
84
|
+
"array": [case["text"]],
|
|
85
|
+
"z": case["text"],
|
|
86
|
+
}
|
|
87
|
+
explicit = api.transform_structured(data, located, case["config"])
|
|
88
|
+
assert explicit.data == structured.data
|
|
89
|
+
assert [(t.path, t.transformation) for t in explicit.transformations] == [
|
|
90
|
+
(t.path, t.transformation) for t in structured.transformations
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
data = {"👋/~":{"aUtHoRiZaTiOn":" bEaReR abc._~+/==\t"}, "Authorization":["Bearer hidden"], "X-Authorization":"Bearer hidden", "a":"Authorization:", "b":"Bearer hidden", "invalid":{"Authorization":"Bearer bad!value"}}
|
|
94
|
+
found = [f for f in api.scan_structured(data).findings if f.finding.entity_type == "BEARER_TOKEN"]
|
|
95
|
+
assert len(found) == 1
|
|
96
|
+
assert found[0].path == "/👋~1~0/aUtHoRiZaTiOn"
|
|
97
|
+
assert project(data["👋/~"]["aUtHoRiZaTiOn"], [found[0].finding]) == [dict(label="BEARER_TOKEN",text="abc._~+/==",start=9,end=19)]
|
|
98
|
+
expected = {**data,"👋/~":{"aUtHoRiZaTiOn":" bEaReR [BEARER_TOKEN]\t"}}
|
|
99
|
+
assert api.scan_and_transform_structured(data,{"transform":{"default":{"strategy":"redact"},"entities":["BEARER_TOKEN"]}}).data == expected
|
|
100
|
+
assert not any(f.entity_type == "BEARER_TOKEN" for f in api.scan("Bearer abc123"))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def verify_providers(manager, token_manager, context):
|
|
104
|
+
for row in RECORDS:
|
|
105
|
+
if not row.get("sample"):
|
|
106
|
+
continue
|
|
107
|
+
entities = list(dict.fromkeys(e["label"] for e in row["entities"]))
|
|
108
|
+
pseudonyms = await manager.scan_and_transform(
|
|
109
|
+
row["text"],
|
|
110
|
+
{
|
|
111
|
+
"scan": row["config"],
|
|
112
|
+
"transform": {
|
|
113
|
+
"default": {"strategy": "pseudonymize", "key_ref": "jwt"},
|
|
114
|
+
"entities": entities,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
assert (
|
|
119
|
+
len(pseudonyms.transformations) == len(row["entities"])
|
|
120
|
+
and pseudonyms.text != row["text"]
|
|
121
|
+
)
|
|
122
|
+
tokens = await token_manager.scan_and_transform(
|
|
123
|
+
row["text"],
|
|
124
|
+
{
|
|
125
|
+
"scan": row["config"],
|
|
126
|
+
"transform": {
|
|
127
|
+
"default": {"strategy": "tokenize", "token_ref": "jwt"},
|
|
128
|
+
"entities": entities,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
context,
|
|
132
|
+
)
|
|
133
|
+
assert len(tokens.transformations) == len(row["entities"])
|
|
134
|
+
assert (await token_manager.restore(tokens.text, context)).text == row["text"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Capability discovery contract against the installed, compiled Core wheel."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import datafog_core as api
|
|
6
|
+
|
|
7
|
+
SUPPORTED = sorted([
|
|
8
|
+
"BEARER_TOKEN", "CREDIT_CARD", "DATE", "DE_IBAN", "DE_PASSPORT_NUMBER", "DE_POSTAL_CODE", "API_KEY", "CREDENTIAL_URI",
|
|
9
|
+
"DE_RESIDENCE_PERMIT_NUMBER", "DE_SOCIAL_SECURITY_NUMBER", "DE_TAX_ID",
|
|
10
|
+
"DE_VAT_ID", "EMAIL", "IP_ADDRESS", "JWT", "NPI", "PERSON", "PHONE",
|
|
11
|
+
"PRIVATE_KEY", "SSN", "US_ROUTING_NUMBER", "UUID", "ZIP_CODE",
|
|
12
|
+
])
|
|
13
|
+
GERMAN = [entity for entity in SUPPORTED if entity.startswith("DE_")]
|
|
14
|
+
DEFAULT = [entity for entity in SUPPORTED if entity not in GERMAN + ["PERSON", "UUID"]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def verify():
|
|
18
|
+
assert callable(api.capabilities)
|
|
19
|
+
try:
|
|
20
|
+
api.capabilities({})
|
|
21
|
+
except TypeError:
|
|
22
|
+
pass
|
|
23
|
+
else:
|
|
24
|
+
raise AssertionError("capabilities accepted arguments")
|
|
25
|
+
snapshot = api.capabilities()
|
|
26
|
+
assert type(snapshot) is dict
|
|
27
|
+
assert snapshot["contract_version"] == 1
|
|
28
|
+
assert json.loads(json.dumps(snapshot)) == snapshot
|
|
29
|
+
assert snapshot["supported_entities"] == SUPPORTED
|
|
30
|
+
assert snapshot["default_entities"] == DEFAULT
|
|
31
|
+
assert sorted(snapshot["entities"]) == SUPPORTED
|
|
32
|
+
for key in ("supported_entities", "default_entities"):
|
|
33
|
+
assert snapshot[key] == sorted(set(snapshot[key]))
|
|
34
|
+
assert set(snapshot["locales"]) == {"de", "de-DE", "de_DE", "en-US", "fr"}
|
|
35
|
+
for locale, metadata in snapshot["locales"].items():
|
|
36
|
+
assert metadata["enabled_entities"] == (GERMAN if locale.startswith("de") else [])
|
|
37
|
+
for label, entity in snapshot["entities"].items():
|
|
38
|
+
assert entity["scopes"] == (["structured"] if label == "PERSON" else ["structured", "text"])
|
|
39
|
+
kind = "locale" if label in GERMAN else "config" if label == "UUID" else "structured" if label == "PERSON" else "default"
|
|
40
|
+
assert entity["activation"]["kind"] == kind
|
|
41
|
+
uuid_config = snapshot["entities"]["UUID"]["activation"]["scan_config"]
|
|
42
|
+
assert uuid_config == {"detect_uuid": True}
|
|
43
|
+
text = "👋 café é 550e8400-e29b-41d4-a716-446655440000"
|
|
44
|
+
assert not any(f.entity_type == "UUID" for f in api.scan(text))
|
|
45
|
+
uuid_findings = [f for f in api.scan(text, uuid_config) if f.entity_type == "UUID"]
|
|
46
|
+
assert len(uuid_findings) == 1
|
|
47
|
+
finding = uuid_findings[0]
|
|
48
|
+
assert text[finding.codepoint_range.start:finding.codepoint_range.end] == finding.matched_text
|
|
49
|
+
assert text.encode()[finding.byte_range.start:finding.byte_range.end].decode() == finding.matched_text
|
|
50
|
+
samples = {}
|
|
51
|
+
for line in (Path(__file__).resolve().parents[3] / "fixtures/german.jsonl").read_text().splitlines():
|
|
52
|
+
row = json.loads(line)
|
|
53
|
+
for expected in row["entities"]:
|
|
54
|
+
samples.setdefault(expected["label"], row["text"])
|
|
55
|
+
for locale in ("de", "de-DE", "de_DE", "DE-de", " De "):
|
|
56
|
+
for label, text in samples.items():
|
|
57
|
+
assert any(f.entity_type == label for f in api.scan(text, {"locale": locale})), (locale, label)
|
|
58
|
+
for label, text in samples.items():
|
|
59
|
+
activation = snapshot["entities"][label]["activation"]["scan_config"]
|
|
60
|
+
assert any(f.entity_type == label for f in api.scan(text, activation))
|
|
61
|
+
for locale in ("en-US", "fr"):
|
|
62
|
+
assert api.scan("jane@example.com", {"locale": locale}) == api.scan("jane@example.com")
|
|
63
|
+
for call, path in [
|
|
64
|
+
(lambda: api.scan("", {"locale": "zz-ZZ"}), "/locale"),
|
|
65
|
+
(lambda: api.scan_structured({}, {"locale": "zz-ZZ"}), "/locale"),
|
|
66
|
+
(lambda: api.scan_and_transform("", {"scan": {"locale": "zz-ZZ"}, "transform": {"default": {"strategy": "redact"}}}), "/scan/locale"),
|
|
67
|
+
(lambda: api.scan_and_transform_structured({}, {"scan": {"locale": "zz-ZZ"}, "transform": {"default": {"strategy": "redact"}}}), "/scan/locale"),
|
|
68
|
+
]:
|
|
69
|
+
try:
|
|
70
|
+
call()
|
|
71
|
+
except api.DataFogConfigurationError as error:
|
|
72
|
+
assert error.code == "invalid_configuration"
|
|
73
|
+
assert error.reason == "invalid_value"
|
|
74
|
+
assert error.path == path
|
|
75
|
+
else:
|
|
76
|
+
raise AssertionError("unsupported locale accepted")
|
|
77
|
+
assert not any(f.entity_type == "PERSON" for f in api.scan("Jane Doe"))
|
|
78
|
+
assert any(f.finding.entity_type == "PERSON" for f in api.scan_structured({"full_name": "Jane Doe"}).findings)
|
|
79
|
+
mutated = api.capabilities()
|
|
80
|
+
assert mutated is not snapshot
|
|
81
|
+
mutated["supported_entities"].clear()
|
|
82
|
+
mutated["default_entities"].append("FAKE")
|
|
83
|
+
mutated["locales"]["de"]["enabled_entities"].clear()
|
|
84
|
+
mutated["entities"]["EMAIL"]["scopes"].clear()
|
|
85
|
+
mutated["entities"]["UUID"]["activation"]["scan_config"]["detect_uuid"] = False
|
|
86
|
+
assert api.capabilities() == snapshot
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Cross-detector overlap selection against the installed wheel."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import datafog_core as api
|
|
6
|
+
from synthetic_fixtures import expand_fixture
|
|
7
|
+
|
|
8
|
+
RECORDS = [
|
|
9
|
+
expand_fixture(json.loads(line))
|
|
10
|
+
for line in (Path(__file__).resolve().parents[3] / "fixtures/credential-overlaps.jsonl")
|
|
11
|
+
.read_text()
|
|
12
|
+
.splitlines()
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def project(text, findings):
|
|
17
|
+
result = []
|
|
18
|
+
for f in findings:
|
|
19
|
+
if f.entity_type not in {"API_KEY", "BEARER_TOKEN", "CREDENTIAL_URI", "JWT"}:
|
|
20
|
+
continue
|
|
21
|
+
assert (
|
|
22
|
+
text.encode()[f.byte_range.start : f.byte_range.end].decode()
|
|
23
|
+
== f.matched_text
|
|
24
|
+
)
|
|
25
|
+
assert text[f.codepoint_range.start : f.codepoint_range.end] == f.matched_text
|
|
26
|
+
assert f.confidence is None
|
|
27
|
+
assert f.detector_name == "datafog-core/" + f.entity_type.lower().replace(
|
|
28
|
+
"_", "-"
|
|
29
|
+
)
|
|
30
|
+
assert f.detector_version
|
|
31
|
+
result.append(
|
|
32
|
+
dict(
|
|
33
|
+
label=f.entity_type,
|
|
34
|
+
text=f.matched_text,
|
|
35
|
+
start=f.codepoint_range.start,
|
|
36
|
+
end=f.codepoint_range.end,
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def verify():
|
|
43
|
+
for row in RECORDS:
|
|
44
|
+
findings = api.scan(row["text"], row["config"])
|
|
45
|
+
if "authorization_value" in row:
|
|
46
|
+
value = row["authorization_value"]
|
|
47
|
+
data = {"👋/headers": {"aUtHoRiZaTiOn": value}}
|
|
48
|
+
located = api.scan_structured(data).findings
|
|
49
|
+
assert all(item.path == "/👋~1headers/aUtHoRiZaTiOn" for item in located)
|
|
50
|
+
expected = [dict(entity, start=7, end=len(value)) for entity in row["entities"]]
|
|
51
|
+
assert project(value, [item.finding for item in located]) == expected
|
|
52
|
+
for entity in row["entities"]:
|
|
53
|
+
label = entity["label"]
|
|
54
|
+
result = api.scan_and_transform_structured(data, {"transform": {"default": {"strategy": "redact"}, "entities": [label]}})
|
|
55
|
+
assert result.data == {"👋/headers": {"aUtHoRiZaTiOn": f"Bearer [{label}]"}}
|
|
56
|
+
if "overlap" in row:
|
|
57
|
+
assert any(f.entity_type == row["overlap"] for f in findings)
|
|
58
|
+
assert project(row["text"], findings) == row["entities"], row["id"]
|
|
59
|
+
data = {"a/b": row["text"], "array": [row["text"]], "z": row["text"]}
|
|
60
|
+
located = api.scan_structured(data, row["config"]).findings
|
|
61
|
+
for path in ["/a~1b", "/array/0", "/z"]:
|
|
62
|
+
assert (
|
|
63
|
+
project(row["text"], [f.finding for f in located if f.path == path])
|
|
64
|
+
== row["entities"]
|
|
65
|
+
), row["id"]
|
|
66
|
+
for case in row.get("transforms", []):
|
|
67
|
+
result = api.scan_and_transform(
|
|
68
|
+
row["text"], {"scan": row["config"], "transform": case["config"]}
|
|
69
|
+
)
|
|
70
|
+
assert result.text == case["text"], row["id"]
|
|
71
|
+
assert api.transform(row["text"], findings, case["config"]) == result
|
|
72
|
+
for t in result.transformations:
|
|
73
|
+
assert (
|
|
74
|
+
result.text.encode()[
|
|
75
|
+
t.output_byte_range.start : t.output_byte_range.end
|
|
76
|
+
].decode()
|
|
77
|
+
== t.replacement
|
|
78
|
+
)
|
|
79
|
+
assert (
|
|
80
|
+
result.text[
|
|
81
|
+
t.output_codepoint_range.start : t.output_codepoint_range.end
|
|
82
|
+
]
|
|
83
|
+
== t.replacement
|
|
84
|
+
)
|
|
85
|
+
assert any(
|
|
86
|
+
f.byte_range == t.source_byte_range
|
|
87
|
+
and f.entity_type == t.entity_type
|
|
88
|
+
for f in findings
|
|
89
|
+
)
|
|
90
|
+
assert not hasattr(t, "matched_text") and not hasattr(t, "finding")
|
|
91
|
+
structured = api.scan_and_transform_structured(
|
|
92
|
+
data, {"scan": row["config"], "transform": case["config"]}
|
|
93
|
+
)
|
|
94
|
+
assert structured.data == {
|
|
95
|
+
"a/b": case["text"],
|
|
96
|
+
"array": [case["text"]],
|
|
97
|
+
"z": case["text"],
|
|
98
|
+
}
|
|
99
|
+
explicit = api.transform_structured(data, located, case["config"])
|
|
100
|
+
assert explicit.data == structured.data
|
|
101
|
+
assert [(t.path, t.transformation) for t in explicit.transformations] == [
|
|
102
|
+
(t.path, t.transformation) for t in structured.transformations
|
|
103
|
+
]
|