datafog-core 0.4.1__tar.gz → 0.4.2__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.4.1 → datafog_core-0.4.2}/Cargo.lock +4 -4
- {datafog_core-0.4.1 → datafog_core-0.4.2}/PKG-INFO +10 -6
- {datafog_core-0.4.1/crates/core → datafog_core-0.4.2}/README.md +9 -5
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/Cargo.toml +1 -1
- datafog_core-0.4.2/bindings/python/tests/email_context_conformance.py +49 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/test_installed.py +2 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/test_typing.py +2 -1
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/Cargo.toml +1 -1
- {datafog_core-0.4.1 → datafog_core-0.4.2/crates/core}/README.md +9 -5
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/capabilities.rs +5 -1
- datafog_core-0.4.2/crates/core/src/email_context.rs +172 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/lib.rs +45 -3
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/structured.rs +5 -1
- datafog_core-0.4.2/crates/core/tests/email_context.rs +107 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/pyproject.toml +1 -1
- {datafog_core-0.4.1 → datafog_core-0.4.2}/python/datafog_core/__init__.pyi +1 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/Cargo.toml +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/src/lib.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/api_key_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/bearer_token_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/capabilities_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/credential_overlap_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/credential_uri_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/german_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/jwt_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/npi_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/private_key_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/requirements-typing.txt +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/synthetic_fixtures.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/test_python49_integration.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/us_routing_number_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/uuid_conformance.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/examples/german_benchmark.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/examples/scan_benchmark.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/api_key.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/bearer_token.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/credential_uri.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/german.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/jwt.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/npi.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/offsets.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/private_key.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/selection_tests.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/us_routing_number.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/src/uuid.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/api_key.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/bearer_token.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/capabilities.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/credential_overlaps.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/credential_uri.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/german.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/jwt.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/npi.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/private_key.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/support/synthetic_fixtures.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/us_routing_number.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/crates/core/tests/uuid.rs +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/python/datafog_core/__init__.py +0 -0
- {datafog_core-0.4.1 → datafog_core-0.4.2}/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.4.
|
|
83
|
+
version = "0.4.2"
|
|
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.4.
|
|
97
|
+
version = "0.4.2"
|
|
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.4.
|
|
107
|
+
version = "0.4.2"
|
|
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.4.
|
|
118
|
+
version = "0.4.2"
|
|
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.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: License :: OSI Approved :: MIT License
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -79,8 +79,12 @@ full-match regex values:
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
`scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean }, transform: ... }` so
|
|
82
|
+
`scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean, format?: "text" | "env" | "sql" }, transform: ... }` so
|
|
83
83
|
detection settings remain separate from transformation policy.
|
|
84
|
+
For `.env` assignments or standard SQL quoted strings, select `format: "env"`
|
|
85
|
+
or `format: "sql"` to preserve email boundaries. See the
|
|
86
|
+
[email source-context policy](docs/guides/email-boundaries.mdx) for supported
|
|
87
|
+
syntax, raw escaped spans, and limitations. Default text scanning is unchanged.
|
|
84
88
|
|
|
85
89
|
## Packages
|
|
86
90
|
|
|
@@ -321,8 +325,8 @@ boundaries, never newlines. Returned text and offsets preserve the source.
|
|
|
321
325
|
Passport and residence-permit patterns are legacy heuristics with limited
|
|
322
326
|
coverage. See the [German entity reference](docs/reference/german-entities.mdx)
|
|
323
327
|
and [migration differences](docs/guides/migrating-from-datafog-python.mdx).
|
|
324
|
-
|
|
325
|
-
|
|
328
|
+
The higher-level Python adapter and its dependency update were merged in
|
|
329
|
+
[PR #179](https://github.com/DataFog/datafog-python/pull/179). Its package release is separate from Core.
|
|
326
330
|
|
|
327
331
|
## UUID identifiers
|
|
328
332
|
|
|
@@ -332,7 +336,7 @@ structured scans, independently of locale. It emits `UUID` findings for versions
|
|
|
332
336
|
sensitivity. See the [UUID reference](docs/reference/uuid.mdx) for boundaries,
|
|
333
337
|
excluded sentinel forms and transformation examples.
|
|
334
338
|
|
|
335
|
-
**0.4.
|
|
339
|
+
**0.4.1 is published** for Rust, Python, Node.js, and WASM. The higher-level Python adapter is integrated in [merged PR #179](https://github.com/DataFog/datafog-python/pull/179); its package release is separate. See the [release notes](docs/releases/0-4-1.mdx), [runtime capabilities](docs/reference/capabilities.mdx), and [0.4.x compatibility policy](docs/reference/compatibility.mdx).
|
|
336
340
|
|
|
337
|
-
|
|
341
|
+
**Core 0.4.1** 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 [release notes](docs/releases/0-4-1.mdx) for exact scopes, preserved legacy Python overlap behavior, and the passed local exact-wheel Python 4.9 integration gate. Python's default backend is unchanged.
|
|
338
342
|
|
|
@@ -56,8 +56,12 @@ full-match regex values:
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
`scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean }, transform: ... }` so
|
|
59
|
+
`scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean, format?: "text" | "env" | "sql" }, transform: ... }` so
|
|
60
60
|
detection settings remain separate from transformation policy.
|
|
61
|
+
For `.env` assignments or standard SQL quoted strings, select `format: "env"`
|
|
62
|
+
or `format: "sql"` to preserve email boundaries. See the
|
|
63
|
+
[email source-context policy](docs/guides/email-boundaries.mdx) for supported
|
|
64
|
+
syntax, raw escaped spans, and limitations. Default text scanning is unchanged.
|
|
61
65
|
|
|
62
66
|
## Packages
|
|
63
67
|
|
|
@@ -298,8 +302,8 @@ boundaries, never newlines. Returned text and offsets preserve the source.
|
|
|
298
302
|
Passport and residence-permit patterns are legacy heuristics with limited
|
|
299
303
|
coverage. See the [German entity reference](docs/reference/german-entities.mdx)
|
|
300
304
|
and [migration differences](docs/guides/migrating-from-datafog-python.mdx).
|
|
301
|
-
|
|
302
|
-
|
|
305
|
+
The higher-level Python adapter and its dependency update were merged in
|
|
306
|
+
[PR #179](https://github.com/DataFog/datafog-python/pull/179). Its package release is separate from Core.
|
|
303
307
|
|
|
304
308
|
## UUID identifiers
|
|
305
309
|
|
|
@@ -309,6 +313,6 @@ structured scans, independently of locale. It emits `UUID` findings for versions
|
|
|
309
313
|
sensitivity. See the [UUID reference](docs/reference/uuid.mdx) for boundaries,
|
|
310
314
|
excluded sentinel forms and transformation examples.
|
|
311
315
|
|
|
312
|
-
**0.4.
|
|
316
|
+
**0.4.1 is published** for Rust, Python, Node.js, and WASM. The higher-level Python adapter is integrated in [merged PR #179](https://github.com/DataFog/datafog-python/pull/179); its package release is separate. See the [release notes](docs/releases/0-4-1.mdx), [runtime capabilities](docs/reference/capabilities.mdx), and [0.4.x compatibility policy](docs/reference/compatibility.mdx).
|
|
313
317
|
|
|
314
|
-
|
|
318
|
+
**Core 0.4.1** 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 [release notes](docs/releases/0-4-1.mdx) for exact scopes, preserved legacy Python overlap behavior, and the passed local exact-wheel Python 4.9 integration gate. Python's default backend is unchanged.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Email source-context fixtures against the installed extension."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import datafog_core as api
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def verify():
|
|
10
|
+
fixture = Path(__file__).resolve().parents[3] / "fixtures/email-context.jsonl"
|
|
11
|
+
for line in fixture.read_text().splitlines():
|
|
12
|
+
row = json.loads(line)
|
|
13
|
+
text = row["text"]
|
|
14
|
+
findings = api.scan(text, row["config"])
|
|
15
|
+
emails = [f for f in findings if f.entity_type == "EMAIL"]
|
|
16
|
+
assert [
|
|
17
|
+
dict(text=f.matched_text, start=f.codepoint_range.start, end=f.codepoint_range.end)
|
|
18
|
+
for f in emails
|
|
19
|
+
] == row["matches"], row["id"]
|
|
20
|
+
for finding in emails:
|
|
21
|
+
assert text.encode()[finding.byte_range.start:finding.byte_range.end].decode() == finding.matched_text
|
|
22
|
+
assert text[finding.codepoint_range.start:finding.codepoint_range.end] == finding.matched_text
|
|
23
|
+
data = {"source": text}
|
|
24
|
+
located = api.scan_structured(data, row["config"]).findings
|
|
25
|
+
assert [f.finding for f in located] == findings
|
|
26
|
+
for strategy in ["redact", "mask", "remove"]:
|
|
27
|
+
policy = {"default": {"strategy": strategy}, "entities": ["EMAIL"]}
|
|
28
|
+
envelope = {"scan": row["config"], "transform": policy}
|
|
29
|
+
result = api.scan_and_transform(text, envelope)
|
|
30
|
+
assert result.text == row["outputs"][strategy], (row["id"], strategy)
|
|
31
|
+
assert api.transform(text, findings, policy) == result
|
|
32
|
+
for record, finding in zip(result.transformations, emails):
|
|
33
|
+
assert record.source_byte_range == finding.byte_range
|
|
34
|
+
assert record.source_codepoint_range == finding.codepoint_range
|
|
35
|
+
assert result.text.encode()[record.output_byte_range.start:record.output_byte_range.end].decode() == record.replacement
|
|
36
|
+
assert api.scan_and_transform_structured(data, envelope).data == {"source": result.text}
|
|
37
|
+
if row["config"]["format"] == "text":
|
|
38
|
+
assert api.scan(text) == findings
|
|
39
|
+
for invalid in [None, 1, True, {}, "yaml", "ENV"]:
|
|
40
|
+
for call in [
|
|
41
|
+
lambda: api.scan("", {"format": invalid}),
|
|
42
|
+
lambda: api.scan_structured({"source": ""}, {"format": invalid}),
|
|
43
|
+
]:
|
|
44
|
+
try:
|
|
45
|
+
call()
|
|
46
|
+
except api.DataFogConfigurationError as error:
|
|
47
|
+
assert error.path == "/format"
|
|
48
|
+
else:
|
|
49
|
+
raise AssertionError("Invalid source format accepted")
|
|
@@ -12,6 +12,7 @@ import jwt_conformance
|
|
|
12
12
|
import bearer_token_conformance
|
|
13
13
|
import private_key_conformance
|
|
14
14
|
import api_key_conformance
|
|
15
|
+
import email_context_conformance
|
|
15
16
|
import uuid_conformance
|
|
16
17
|
import credential_uri_conformance
|
|
17
18
|
import us_routing_number_conformance
|
|
@@ -133,6 +134,7 @@ def main() -> None:
|
|
|
133
134
|
bearer_token_conformance.verify()
|
|
134
135
|
private_key_conformance.verify()
|
|
135
136
|
api_key_conformance.verify()
|
|
137
|
+
email_context_conformance.verify()
|
|
136
138
|
uuid_conformance.verify()
|
|
137
139
|
credential_uri_conformance.verify()
|
|
138
140
|
us_routing_number_conformance.verify()
|
|
@@ -42,7 +42,7 @@ config: _TransformationConfig = {
|
|
|
42
42
|
},
|
|
43
43
|
"allow": {"exact": {"EMAIL": ["safe@example.com"]}, "regex": {"EMAIL": [{"pattern": "safe", "case_sensitive": False}]}},
|
|
44
44
|
}
|
|
45
|
-
findings = scan("jane@example.com", {"locale": "en-US", "detect_uuid": True})
|
|
45
|
+
findings = scan("jane@example.com", {"locale": "en-US", "detect_uuid": True, "format": "env"})
|
|
46
46
|
assert_type(findings, list[Finding])
|
|
47
47
|
finding = findings[0]
|
|
48
48
|
assert_type(finding.entity_type, str)
|
|
@@ -127,6 +127,7 @@ INVALID = """
|
|
|
127
127
|
from datafog_core import capabilities, scan, transform, scan_structured, PrivacyManager, TransformResult
|
|
128
128
|
capabilities(1) # error
|
|
129
129
|
scan(123) # error
|
|
130
|
+
scan("text", {"format": "yaml"}) # error
|
|
130
131
|
scan("text", {"locale": 123}) # error
|
|
131
132
|
scan("text", {"detect_uuid": "true"}) # error
|
|
132
133
|
scan("text", {"max_bytes": 100}) # error
|
|
@@ -56,8 +56,12 @@ full-match regex values:
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
`scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean }, transform: ... }` so
|
|
59
|
+
`scan_and_transform` uses `{ scan?: { locale?: string, detect_uuid?: boolean, format?: "text" | "env" | "sql" }, transform: ... }` so
|
|
60
60
|
detection settings remain separate from transformation policy.
|
|
61
|
+
For `.env` assignments or standard SQL quoted strings, select `format: "env"`
|
|
62
|
+
or `format: "sql"` to preserve email boundaries. See the
|
|
63
|
+
[email source-context policy](docs/guides/email-boundaries.mdx) for supported
|
|
64
|
+
syntax, raw escaped spans, and limitations. Default text scanning is unchanged.
|
|
61
65
|
|
|
62
66
|
## Packages
|
|
63
67
|
|
|
@@ -298,8 +302,8 @@ boundaries, never newlines. Returned text and offsets preserve the source.
|
|
|
298
302
|
Passport and residence-permit patterns are legacy heuristics with limited
|
|
299
303
|
coverage. See the [German entity reference](docs/reference/german-entities.mdx)
|
|
300
304
|
and [migration differences](docs/guides/migrating-from-datafog-python.mdx).
|
|
301
|
-
|
|
302
|
-
|
|
305
|
+
The higher-level Python adapter and its dependency update were merged in
|
|
306
|
+
[PR #179](https://github.com/DataFog/datafog-python/pull/179). Its package release is separate from Core.
|
|
303
307
|
|
|
304
308
|
## UUID identifiers
|
|
305
309
|
|
|
@@ -309,6 +313,6 @@ structured scans, independently of locale. It emits `UUID` findings for versions
|
|
|
309
313
|
sensitivity. See the [UUID reference](docs/reference/uuid.mdx) for boundaries,
|
|
310
314
|
excluded sentinel forms and transformation examples.
|
|
311
315
|
|
|
312
|
-
**0.4.
|
|
316
|
+
**0.4.1 is published** for Rust, Python, Node.js, and WASM. The higher-level Python adapter is integrated in [merged PR #179](https://github.com/DataFog/datafog-python/pull/179); its package release is separate. See the [release notes](docs/releases/0-4-1.mdx), [runtime capabilities](docs/reference/capabilities.mdx), and [0.4.x compatibility policy](docs/reference/compatibility.mdx).
|
|
313
317
|
|
|
314
|
-
|
|
318
|
+
**Core 0.4.1** 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 [release notes](docs/releases/0-4-1.mdx) for exact scopes, preserved legacy Python overlap behavior, and the passed local exact-wheel Python 4.9 integration gate. Python's default backend is unchanged.
|
|
@@ -152,7 +152,11 @@ pub(super) fn detect(text: &str, config: &ScanConfig, candidates: &mut Vec<Candi
|
|
|
152
152
|
.iter()
|
|
153
153
|
.filter(|detector| detector.activation.enabled(config))
|
|
154
154
|
{
|
|
155
|
-
|
|
155
|
+
if detector.labels == [Label::Email] {
|
|
156
|
+
crate::email_context::detect(text, config.format(), candidates);
|
|
157
|
+
} else {
|
|
158
|
+
(detector.detect)(text, candidates);
|
|
159
|
+
}
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
//! Delimit original source slices before applying the shared email matcher.
|
|
2
|
+
use crate::{Candidate, ScanFormat, detect_email};
|
|
3
|
+
|
|
4
|
+
pub(super) fn detect(text: &str, format: ScanFormat, candidates: &mut Vec<Candidate>) {
|
|
5
|
+
match format {
|
|
6
|
+
ScanFormat::Text => detect_email(text, candidates),
|
|
7
|
+
ScanFormat::Env => detect_env(text, candidates),
|
|
8
|
+
ScanFormat::Sql => detect_sql(text, candidates),
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fn detect_slice(text: &str, start: usize, end: usize, candidates: &mut Vec<Candidate>) {
|
|
13
|
+
let first = candidates.len();
|
|
14
|
+
detect_email(&text[start..end], candidates);
|
|
15
|
+
for candidate in &mut candidates[first..] {
|
|
16
|
+
candidate.start_byte += start;
|
|
17
|
+
candidate.end_byte += start;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn line_end(text: &str, start: usize) -> usize {
|
|
22
|
+
text[start..]
|
|
23
|
+
.find('\n')
|
|
24
|
+
.map_or(text.len(), |end| start + end)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn skip_horizontal_space(bytes: &[u8], mut cursor: usize) -> usize {
|
|
28
|
+
while matches!(bytes.get(cursor), Some(b' ' | b'\t' | b'\r')) {
|
|
29
|
+
cursor += 1;
|
|
30
|
+
}
|
|
31
|
+
cursor
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn env_value_start(line: &str) -> Option<usize> {
|
|
35
|
+
let bytes = line.as_bytes();
|
|
36
|
+
let mut cursor = skip_horizontal_space(bytes, 0);
|
|
37
|
+
if line[cursor..].starts_with("export") && matches!(bytes.get(cursor + 6), Some(b' ' | b'\t')) {
|
|
38
|
+
cursor = skip_horizontal_space(bytes, cursor + 6);
|
|
39
|
+
}
|
|
40
|
+
if !bytes
|
|
41
|
+
.get(cursor)
|
|
42
|
+
.is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_')
|
|
43
|
+
{
|
|
44
|
+
return None;
|
|
45
|
+
}
|
|
46
|
+
cursor += 1;
|
|
47
|
+
while bytes
|
|
48
|
+
.get(cursor)
|
|
49
|
+
.is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
|
|
50
|
+
{
|
|
51
|
+
cursor += 1;
|
|
52
|
+
}
|
|
53
|
+
cursor = skip_horizontal_space(bytes, cursor);
|
|
54
|
+
if bytes.get(cursor) != Some(&b'=') {
|
|
55
|
+
return None;
|
|
56
|
+
}
|
|
57
|
+
Some(skip_horizontal_space(bytes, cursor + 1))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Return the closing delimiter or EOF. No values are decoded, so offsets and
|
|
61
|
+
// escaped local parts remain contiguous spans of the caller's source text.
|
|
62
|
+
fn quote_end(bytes: &[u8], opening: usize, doubled: bool, backslash: bool) -> usize {
|
|
63
|
+
let quote = bytes[opening];
|
|
64
|
+
let mut cursor = opening + 1;
|
|
65
|
+
while cursor < bytes.len() {
|
|
66
|
+
if backslash && bytes[cursor] == b'\\' {
|
|
67
|
+
cursor += if cursor + 1 < bytes.len() { 2 } else { 1 };
|
|
68
|
+
} else if bytes[cursor] == quote {
|
|
69
|
+
if doubled && bytes.get(cursor + 1) == Some("e) {
|
|
70
|
+
cursor += 2;
|
|
71
|
+
} else {
|
|
72
|
+
return cursor;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
cursor += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
bytes.len()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn detect_env(text: &str, candidates: &mut Vec<Candidate>) {
|
|
82
|
+
let bytes = text.as_bytes();
|
|
83
|
+
let mut cursor = 0;
|
|
84
|
+
while cursor < text.len() {
|
|
85
|
+
let end = line_end(text, cursor);
|
|
86
|
+
if let Some(value) = env_value_start(&text[cursor..end]) {
|
|
87
|
+
let start = cursor + value;
|
|
88
|
+
if matches!(bytes.get(start), Some(b'\'' | b'"')) {
|
|
89
|
+
let close = quote_end(bytes, start, false, bytes[start] == b'"');
|
|
90
|
+
detect_slice(text, start + 1, close, candidates);
|
|
91
|
+
if close == text.len() {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
let tail_end = line_end(text, close + 1);
|
|
95
|
+
detect_env_unquoted(text, close + 1, tail_end, candidates);
|
|
96
|
+
cursor = tail_end;
|
|
97
|
+
} else {
|
|
98
|
+
detect_env_unquoted(text, start, end, candidates);
|
|
99
|
+
cursor = end;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
detect_env_unquoted(text, cursor, end, candidates);
|
|
103
|
+
cursor = end;
|
|
104
|
+
}
|
|
105
|
+
if cursor < text.len() {
|
|
106
|
+
cursor += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn detect_env_unquoted(text: &str, start: usize, end: usize, candidates: &mut Vec<Candidate>) {
|
|
112
|
+
let bytes = text.as_bytes();
|
|
113
|
+
for cursor in start..end {
|
|
114
|
+
if bytes[cursor] == b'#' && (cursor == start || bytes[cursor - 1].is_ascii_whitespace()) {
|
|
115
|
+
detect_slice(text, start, cursor, candidates);
|
|
116
|
+
detect_slice(text, cursor + 1, end, candidates);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
detect_slice(text, start, end, candidates);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn detect_sql(text: &str, candidates: &mut Vec<Candidate>) {
|
|
124
|
+
let bytes = text.as_bytes();
|
|
125
|
+
let mut cursor = 0;
|
|
126
|
+
let mut plain_start = 0;
|
|
127
|
+
while cursor < bytes.len() {
|
|
128
|
+
let end = if matches!(bytes[cursor], b'\'' | b'"') {
|
|
129
|
+
detect_slice(text, plain_start, cursor, candidates);
|
|
130
|
+
let close = quote_end(bytes, cursor, true, false);
|
|
131
|
+
detect_slice(text, cursor + 1, close, candidates);
|
|
132
|
+
if close < bytes.len() {
|
|
133
|
+
close + 1
|
|
134
|
+
} else {
|
|
135
|
+
close
|
|
136
|
+
}
|
|
137
|
+
} else if bytes[cursor..].starts_with(b"--") {
|
|
138
|
+
detect_slice(text, plain_start, cursor, candidates);
|
|
139
|
+
let end = line_end(text, cursor);
|
|
140
|
+
detect_slice(text, cursor + 2, end, candidates);
|
|
141
|
+
end
|
|
142
|
+
} else if bytes[cursor..].starts_with(b"/*") {
|
|
143
|
+
detect_slice(text, plain_start, cursor, candidates);
|
|
144
|
+
let mut end = cursor + 2;
|
|
145
|
+
let mut comment_start = end;
|
|
146
|
+
let mut depth = 1;
|
|
147
|
+
while end < bytes.len() && depth > 0 {
|
|
148
|
+
if bytes[end..].starts_with(b"/*") {
|
|
149
|
+
detect_slice(text, comment_start, end, candidates);
|
|
150
|
+
depth += 1;
|
|
151
|
+
end += 2;
|
|
152
|
+
comment_start = end;
|
|
153
|
+
} else if bytes[end..].starts_with(b"*/") {
|
|
154
|
+
detect_slice(text, comment_start, end, candidates);
|
|
155
|
+
depth -= 1;
|
|
156
|
+
end += 2;
|
|
157
|
+
comment_start = end;
|
|
158
|
+
} else {
|
|
159
|
+
end += 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
detect_slice(text, comment_start, end, candidates);
|
|
163
|
+
end
|
|
164
|
+
} else {
|
|
165
|
+
cursor += 1;
|
|
166
|
+
continue;
|
|
167
|
+
};
|
|
168
|
+
cursor = end;
|
|
169
|
+
plain_start = end;
|
|
170
|
+
}
|
|
171
|
+
detect_slice(text, plain_start, text.len(), candidates);
|
|
172
|
+
}
|
|
@@ -3,6 +3,7 @@ mod api_key;
|
|
|
3
3
|
mod bearer_token;
|
|
4
4
|
mod capabilities;
|
|
5
5
|
mod credential_uri;
|
|
6
|
+
mod email_context;
|
|
6
7
|
pub use capabilities::{
|
|
7
8
|
ActivationScanConfig, Capabilities, EntityActivation, EntityCapabilities, LocaleCapabilities,
|
|
8
9
|
capabilities,
|
|
@@ -535,15 +536,39 @@ fn json_pointer_segment(segment: &str) -> String {
|
|
|
535
536
|
segment.replace('~', "~0").replace('/', "~1")
|
|
536
537
|
}
|
|
537
538
|
|
|
538
|
-
///
|
|
539
|
-
|
|
539
|
+
/// Source syntax used to delimit email candidates. Other detectors are unchanged.
|
|
540
|
+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
541
|
+
pub enum ScanFormat {
|
|
542
|
+
/// Unstructured text; retain the full supported email local-part alphabet.
|
|
543
|
+
#[default]
|
|
544
|
+
Text,
|
|
545
|
+
/// Environment assignments with optional `export` and quoted values.
|
|
546
|
+
Env,
|
|
547
|
+
/// SQL strings using standard doubled-quote escaping.
|
|
548
|
+
Sql,
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/// Scanner configuration. German locale aliases activate German detectors,
|
|
552
|
+
/// UUIDs are opt-in, and source format controls email boundaries.
|
|
540
553
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
541
554
|
pub struct ScanConfig {
|
|
542
555
|
locale: Option<String>,
|
|
543
556
|
detect_uuid: bool,
|
|
557
|
+
format: ScanFormat,
|
|
544
558
|
}
|
|
545
559
|
|
|
546
560
|
impl ScanConfig {
|
|
561
|
+
/// Select source syntax for email boundaries; defaults to unstructured text.
|
|
562
|
+
pub fn with_format(mut self, format: ScanFormat) -> Self {
|
|
563
|
+
self.format = format;
|
|
564
|
+
self
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/// Source syntax used for email boundaries.
|
|
568
|
+
pub fn format(&self) -> ScanFormat {
|
|
569
|
+
self.format
|
|
570
|
+
}
|
|
571
|
+
|
|
547
572
|
/// Create scanner configuration using detector defaults.
|
|
548
573
|
pub fn new() -> Self {
|
|
549
574
|
Self::default()
|
|
@@ -755,8 +780,11 @@ pub fn parse_scan_and_transform_config(
|
|
|
755
780
|
/// Parse canonical scanner configuration.
|
|
756
781
|
pub fn parse_scan_config(value: &serde_json::Value) -> Result<ScanConfig, PrivacyError> {
|
|
757
782
|
let object = require_object(value, "", "scan configuration must be an object")?;
|
|
758
|
-
reject_unknown_fields(object, &["locale", "detect_uuid"], "")?;
|
|
783
|
+
reject_unknown_fields(object, &["locale", "detect_uuid", "format"], "")?;
|
|
759
784
|
let mut config = ScanConfig::new();
|
|
785
|
+
if let Some(format) = object.get("format") {
|
|
786
|
+
config = config.with_format(parse_scan_format(format)?);
|
|
787
|
+
}
|
|
760
788
|
if let Some(enabled) = object.get("detect_uuid") {
|
|
761
789
|
config = config.with_uuid_detection(parse_uuid_detection(enabled)?);
|
|
762
790
|
}
|
|
@@ -767,6 +795,20 @@ pub fn parse_scan_config(value: &serde_json::Value) -> Result<ScanConfig, Privac
|
|
|
767
795
|
Ok(config)
|
|
768
796
|
}
|
|
769
797
|
|
|
798
|
+
fn parse_scan_format(value: &serde_json::Value) -> Result<ScanFormat, PrivacyError> {
|
|
799
|
+
let format = require_string(value, "/format", "scan format must be a string")?;
|
|
800
|
+
match format.as_str() {
|
|
801
|
+
"text" => Ok(ScanFormat::Text),
|
|
802
|
+
"env" => Ok(ScanFormat::Env),
|
|
803
|
+
"sql" => Ok(ScanFormat::Sql),
|
|
804
|
+
_ => Err(PrivacyError::invalid_configuration(
|
|
805
|
+
PrivacyErrorReason::InvalidValue,
|
|
806
|
+
"/format",
|
|
807
|
+
"scan format must be text, env, or sql",
|
|
808
|
+
)),
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
770
812
|
fn parse_uuid_detection(value: &serde_json::Value) -> Result<bool, PrivacyError> {
|
|
771
813
|
value.as_bool().ok_or_else(|| {
|
|
772
814
|
PrivacyError::invalid_configuration(
|
|
@@ -79,7 +79,7 @@ fn validate_pointer(pointer: &str, path: &str) -> Result<(), PrivacyError> {
|
|
|
79
79
|
Ok(())
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/// Parse `{ locale?, detect_uuid?, discover_person?, mappings?: {pointer: "PERSON"}, exclude?: [pointer] }`.
|
|
82
|
+
/// Parse `{ locale?, detect_uuid?, format?, discover_person?, mappings?: {pointer: "PERSON"}, exclude?: [pointer] }`.
|
|
83
83
|
pub fn parse_scan_config(value: &Value) -> Result<StructuredScanConfig, PrivacyError> {
|
|
84
84
|
let object = require_object(value, "", "structured scan configuration must be an object")?;
|
|
85
85
|
reject_unknown_fields(
|
|
@@ -87,6 +87,7 @@ pub fn parse_scan_config(value: &Value) -> Result<StructuredScanConfig, PrivacyE
|
|
|
87
87
|
&[
|
|
88
88
|
"locale",
|
|
89
89
|
"detect_uuid",
|
|
90
|
+
"format",
|
|
90
91
|
"discover_person",
|
|
91
92
|
"mappings",
|
|
92
93
|
"exclude",
|
|
@@ -94,6 +95,9 @@ pub fn parse_scan_config(value: &Value) -> Result<StructuredScanConfig, PrivacyE
|
|
|
94
95
|
"",
|
|
95
96
|
)?;
|
|
96
97
|
let mut config = StructuredScanConfig::default();
|
|
98
|
+
if let Some(format) = object.get("format") {
|
|
99
|
+
config.scan = config.scan.with_format(parse_scan_format(format)?);
|
|
100
|
+
}
|
|
97
101
|
if let Some(enabled) = object.get("detect_uuid") {
|
|
98
102
|
config.scan = config
|
|
99
103
|
.scan
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
use datafog_core::{
|
|
2
|
+
parse_scan_and_transform_config, parse_scan_config, parse_transformation_config, scan,
|
|
3
|
+
scan_and_transform, scan_with_config, structured, transform, utf16_range,
|
|
4
|
+
};
|
|
5
|
+
use serde_json::{Value, json};
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn email_context_preserves_source_spans_and_syntax() {
|
|
9
|
+
for line in include_str!("../../../fixtures/email-context.jsonl").lines() {
|
|
10
|
+
let row: Value = serde_json::from_str(line).unwrap();
|
|
11
|
+
let text = row["text"].as_str().unwrap();
|
|
12
|
+
let findings = scan_with_config(text, &parse_scan_config(&row["config"]).unwrap());
|
|
13
|
+
let emails: Vec<_> = findings
|
|
14
|
+
.iter()
|
|
15
|
+
.filter(|f| f.entity_type == "EMAIL")
|
|
16
|
+
.collect();
|
|
17
|
+
let matches: Vec<_> = emails.iter().map(|f| {
|
|
18
|
+
assert_eq!(&text[f.byte_range.start..f.byte_range.end], f.matched_text);
|
|
19
|
+
let range = utf16_range(text, f.byte_range).unwrap();
|
|
20
|
+
let units: Vec<_> = text.encode_utf16().collect();
|
|
21
|
+
assert_eq!(String::from_utf16(&units[range.start..range.end]).unwrap(), f.matched_text);
|
|
22
|
+
json!({"text":f.matched_text,"start":f.codepoint_range.start,"end":f.codepoint_range.end})
|
|
23
|
+
}).collect();
|
|
24
|
+
assert_eq!(json!(matches), row["matches"], "{}", row["id"]);
|
|
25
|
+
let data = json!({"source":text});
|
|
26
|
+
let located = structured::scan(
|
|
27
|
+
&data,
|
|
28
|
+
&structured::parse_scan_config(&row["config"]).unwrap(),
|
|
29
|
+
)
|
|
30
|
+
.unwrap();
|
|
31
|
+
assert_eq!(
|
|
32
|
+
located
|
|
33
|
+
.findings
|
|
34
|
+
.iter()
|
|
35
|
+
.map(|f| f.finding.clone())
|
|
36
|
+
.collect::<Vec<_>>(),
|
|
37
|
+
findings
|
|
38
|
+
);
|
|
39
|
+
for strategy in ["redact", "mask", "remove"] {
|
|
40
|
+
let policy = json!({"default":{"strategy":strategy},"entities":["EMAIL"]});
|
|
41
|
+
let envelope = json!({"scan":row["config"],"transform":policy});
|
|
42
|
+
let result =
|
|
43
|
+
scan_and_transform(text, &parse_scan_and_transform_config(&envelope).unwrap())
|
|
44
|
+
.unwrap();
|
|
45
|
+
assert_eq!(
|
|
46
|
+
result.text, row["outputs"][strategy],
|
|
47
|
+
"{} {strategy}",
|
|
48
|
+
row["id"]
|
|
49
|
+
);
|
|
50
|
+
assert_eq!(
|
|
51
|
+
transform(
|
|
52
|
+
text,
|
|
53
|
+
&findings,
|
|
54
|
+
&parse_transformation_config(&policy).unwrap()
|
|
55
|
+
)
|
|
56
|
+
.unwrap(),
|
|
57
|
+
result
|
|
58
|
+
);
|
|
59
|
+
for (record, finding) in result.transformations.iter().zip(&emails) {
|
|
60
|
+
assert_eq!(record.source_byte_range, finding.byte_range);
|
|
61
|
+
assert_eq!(record.source_codepoint_range, finding.codepoint_range);
|
|
62
|
+
assert_eq!(
|
|
63
|
+
&result.text[record.output_byte_range.start..record.output_byte_range.end],
|
|
64
|
+
record.replacement
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
let result = structured::scan_and_transform(
|
|
68
|
+
&data,
|
|
69
|
+
&structured::parse_scan_and_transform_config(&envelope).unwrap(),
|
|
70
|
+
)
|
|
71
|
+
.unwrap();
|
|
72
|
+
assert_eq!(result.data, json!({"source":row["outputs"][strategy]}));
|
|
73
|
+
}
|
|
74
|
+
if row["config"]["format"] == "text" {
|
|
75
|
+
assert_eq!(scan(text), findings);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn invalid_format_is_reported_at_the_configuration_path() {
|
|
82
|
+
for format in [
|
|
83
|
+
json!(null),
|
|
84
|
+
json!(1),
|
|
85
|
+
json!(true),
|
|
86
|
+
json!({}),
|
|
87
|
+
json!("yaml"),
|
|
88
|
+
json!("ENV"),
|
|
89
|
+
] {
|
|
90
|
+
let config = json!({"format":format});
|
|
91
|
+
assert_eq!(
|
|
92
|
+
parse_scan_config(&config).unwrap_err().path(),
|
|
93
|
+
Some("/format")
|
|
94
|
+
);
|
|
95
|
+
assert_eq!(
|
|
96
|
+
structured::parse_scan_config(&config).unwrap_err().path(),
|
|
97
|
+
Some("/format")
|
|
98
|
+
);
|
|
99
|
+
let envelope = json!({"scan":config,"transform":{"default":{"strategy":"redact"}}});
|
|
100
|
+
assert_eq!(
|
|
101
|
+
parse_scan_and_transform_config(&envelope)
|
|
102
|
+
.unwrap_err()
|
|
103
|
+
.path(),
|
|
104
|
+
Some("/scan/format")
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/credential_overlap_conformance.py
RENAMED
|
File without changes
|
{datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/credential_uri_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/test_python49_integration.py
RENAMED
|
File without changes
|
{datafog_core-0.4.1 → datafog_core-0.4.2}/bindings/python/tests/us_routing_number_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|