pseudonymize 0.1.0a1__py3-none-any.whl
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.
- pseudonymize/__init__.py +28 -0
- pseudonymize/api.py +50 -0
- pseudonymize/backends/__init__.py +5 -0
- pseudonymize/backends/base.py +14 -0
- pseudonymize/backends/composite.py +17 -0
- pseudonymize/backends/rules.py +19 -0
- pseudonymize/cli.py +125 -0
- pseudonymize/detectors/__init__.py +4 -0
- pseudonymize/detectors/base.py +10 -0
- pseudonymize/detectors/email.py +21 -0
- pseudonymize/detectors/iban.py +31 -0
- pseudonymize/detectors/ip_address.py +26 -0
- pseudonymize/detectors/payment_card.py +33 -0
- pseudonymize/detectors/phone.py +26 -0
- pseudonymize/detectors/registry.py +18 -0
- pseudonymize/detectors/secret.py +31 -0
- pseudonymize/detectors/url.py +53 -0
- pseudonymize/engine.py +214 -0
- pseudonymize/exceptions.py +10 -0
- pseudonymize/normalization.py +22 -0
- pseudonymize/policy.py +71 -0
- pseudonymize/py.typed +0 -0
- pseudonymize/resolution.py +28 -0
- pseudonymize/result.py +57 -0
- pseudonymize/spans.py +45 -0
- pseudonymize/transforms/__init__.py +27 -0
- pseudonymize/transforms/alias.py +74 -0
- pseudonymize/transforms/base.py +8 -0
- pseudonymize/transforms/hmac.py +7 -0
- pseudonymize/transforms/mode.py +8 -0
- pseudonymize/transforms/placeholder.py +11 -0
- pseudonymize/transforms/redact.py +13 -0
- pseudonymize-0.1.0a1.dist-info/METADATA +184 -0
- pseudonymize-0.1.0a1.dist-info/RECORD +37 -0
- pseudonymize-0.1.0a1.dist-info/WHEEL +4 -0
- pseudonymize-0.1.0a1.dist-info/entry_points.txt +2 -0
- pseudonymize-0.1.0a1.dist-info/licenses/LICENSE +201 -0
pseudonymize/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from pseudonymize.api import generate_key, pseudonymize, redact
|
|
2
|
+
from pseudonymize.backends import CompositeBackend, DetectionBackend, RulesBackend
|
|
3
|
+
from pseudonymize.engine import ProcessingScope, Pseudonymizer
|
|
4
|
+
from pseudonymize.policy import Policy
|
|
5
|
+
from pseudonymize.resolution import EntityResolver, ExactEntityResolver, ResolvedEntity
|
|
6
|
+
from pseudonymize.result import Detection, EntityType, Replacement, Result
|
|
7
|
+
from pseudonymize.transforms import Alias, TransformationMode
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Alias",
|
|
11
|
+
"CompositeBackend",
|
|
12
|
+
"Detection",
|
|
13
|
+
"DetectionBackend",
|
|
14
|
+
"EntityResolver",
|
|
15
|
+
"EntityType",
|
|
16
|
+
"ExactEntityResolver",
|
|
17
|
+
"Policy",
|
|
18
|
+
"ProcessingScope",
|
|
19
|
+
"Pseudonymizer",
|
|
20
|
+
"Replacement",
|
|
21
|
+
"ResolvedEntity",
|
|
22
|
+
"Result",
|
|
23
|
+
"RulesBackend",
|
|
24
|
+
"TransformationMode",
|
|
25
|
+
"generate_key",
|
|
26
|
+
"pseudonymize",
|
|
27
|
+
"redact",
|
|
28
|
+
]
|
pseudonymize/api.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from pseudonymize.backends import DetectionBackend
|
|
4
|
+
from pseudonymize.engine import Pseudonymizer
|
|
5
|
+
from pseudonymize.policy import Policy
|
|
6
|
+
from pseudonymize.transforms import TransformationMode, generate_key
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def pseudonymize(
|
|
10
|
+
text: str,
|
|
11
|
+
*,
|
|
12
|
+
mode: TransformationMode | str = TransformationMode.NUMBERED,
|
|
13
|
+
key: bytes | None = None,
|
|
14
|
+
namespace: str = "default",
|
|
15
|
+
policy: Policy | None = None,
|
|
16
|
+
backends: Sequence[DetectionBackend] | None = None,
|
|
17
|
+
) -> str:
|
|
18
|
+
return (
|
|
19
|
+
Pseudonymizer(
|
|
20
|
+
mode=mode,
|
|
21
|
+
key=key,
|
|
22
|
+
namespace=namespace,
|
|
23
|
+
policy=policy,
|
|
24
|
+
backends=backends,
|
|
25
|
+
)
|
|
26
|
+
.process(text)
|
|
27
|
+
.text
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def redact(
|
|
32
|
+
text: str,
|
|
33
|
+
*,
|
|
34
|
+
typed: bool = False,
|
|
35
|
+
policy: Policy | None = None,
|
|
36
|
+
backends: Sequence[DetectionBackend] | None = None,
|
|
37
|
+
) -> str:
|
|
38
|
+
return (
|
|
39
|
+
Pseudonymizer(
|
|
40
|
+
mode=TransformationMode.REDACTED,
|
|
41
|
+
policy=policy,
|
|
42
|
+
backends=backends,
|
|
43
|
+
typed_redaction=typed,
|
|
44
|
+
)
|
|
45
|
+
.process(text)
|
|
46
|
+
.text
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["generate_key", "pseudonymize", "redact"]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from pseudonymize.backends.base import DetectionBackend, EntityBackend
|
|
2
|
+
from pseudonymize.backends.composite import CompositeBackend
|
|
3
|
+
from pseudonymize.backends.rules import RulesBackend
|
|
4
|
+
|
|
5
|
+
__all__ = ["CompositeBackend", "DetectionBackend", "EntityBackend", "RulesBackend"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
from pseudonymize.result import Detection
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DetectionBackend(Protocol):
|
|
8
|
+
@property
|
|
9
|
+
def name(self) -> str: ...
|
|
10
|
+
|
|
11
|
+
def detect(self, text: str) -> Sequence[Detection]: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
EntityBackend = DetectionBackend
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pseudonymize.backends.base import DetectionBackend
|
|
5
|
+
from pseudonymize.result import Detection
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class CompositeBackend:
|
|
10
|
+
backends: Sequence[DetectionBackend]
|
|
11
|
+
name: str = "composite"
|
|
12
|
+
|
|
13
|
+
def __post_init__(self) -> None:
|
|
14
|
+
object.__setattr__(self, "backends", tuple(self.backends))
|
|
15
|
+
|
|
16
|
+
def detect(self, text: str) -> tuple[Detection, ...]:
|
|
17
|
+
return tuple(detection for backend in self.backends for detection in backend.detect(text))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pseudonymize.detectors import DEFAULT_DETECTORS, Detector
|
|
5
|
+
from pseudonymize.result import Detection
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class RulesBackend:
|
|
10
|
+
detectors: Sequence[Detector] = DEFAULT_DETECTORS
|
|
11
|
+
name: str = "rules"
|
|
12
|
+
|
|
13
|
+
def __post_init__(self) -> None:
|
|
14
|
+
object.__setattr__(self, "detectors", tuple(self.detectors))
|
|
15
|
+
|
|
16
|
+
def detect(self, text: str) -> tuple[Detection, ...]:
|
|
17
|
+
return tuple(
|
|
18
|
+
detection for detector in self.detectors for detection in detector.detect(text)
|
|
19
|
+
)
|
pseudonymize/cli.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import base64
|
|
3
|
+
import binascii
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pseudonymize.api import generate_key
|
|
12
|
+
from pseudonymize.detectors import DEFAULT_DETECTORS
|
|
13
|
+
from pseudonymize.engine import Data, Pseudonymizer
|
|
14
|
+
from pseudonymize.exceptions import PseudonymizeError
|
|
15
|
+
from pseudonymize.policy import Policy
|
|
16
|
+
from pseudonymize.transforms import TransformationMode
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parser() -> argparse.ArgumentParser:
|
|
20
|
+
parser = argparse.ArgumentParser(prog="pseudonymize")
|
|
21
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
subparsers.add_parser("keygen", help="print a new base64-encoded key")
|
|
23
|
+
subparsers.add_parser("detectors", help="list bundled detectors")
|
|
24
|
+
|
|
25
|
+
text_parser = subparsers.add_parser("text", help="process text")
|
|
26
|
+
text_parser.add_argument("text", help="text to process, or - for standard input")
|
|
27
|
+
_add_key_options(text_parser, allow_stdin=True)
|
|
28
|
+
text_parser.add_argument("--namespace", default="default")
|
|
29
|
+
_add_mode_options(text_parser)
|
|
30
|
+
|
|
31
|
+
json_parser = subparsers.add_parser("json", help="process JSON from standard input")
|
|
32
|
+
_add_key_options(json_parser)
|
|
33
|
+
json_parser.add_argument("--namespace", default="default")
|
|
34
|
+
_add_mode_options(json_parser)
|
|
35
|
+
return parser
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _add_key_options(parser: argparse.ArgumentParser, *, allow_stdin: bool = False) -> None:
|
|
39
|
+
sources = parser.add_mutually_exclusive_group()
|
|
40
|
+
sources.add_argument("--key-env", metavar="NAME")
|
|
41
|
+
sources.add_argument("--key-file", type=Path)
|
|
42
|
+
sources.add_argument("--key-fd", type=int)
|
|
43
|
+
if allow_stdin:
|
|
44
|
+
sources.add_argument("--key-stdin", action="store_true")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _add_mode_options(parser: argparse.ArgumentParser) -> None:
|
|
48
|
+
modes = parser.add_mutually_exclusive_group()
|
|
49
|
+
modes.add_argument(
|
|
50
|
+
"--mode", choices=tuple(TransformationMode), default=TransformationMode.NUMBERED
|
|
51
|
+
)
|
|
52
|
+
modes.add_argument(
|
|
53
|
+
"--redact", action="store_const", const=TransformationMode.REDACTED, dest="mode"
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument("--typed-redaction", action="store_true")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _has_key_source(arguments: argparse.Namespace) -> bool:
|
|
59
|
+
return any(
|
|
60
|
+
(
|
|
61
|
+
getattr(arguments, "key_env", None),
|
|
62
|
+
getattr(arguments, "key_file", None),
|
|
63
|
+
getattr(arguments, "key_fd", None) is not None,
|
|
64
|
+
getattr(arguments, "key_stdin", False),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_key(arguments: argparse.Namespace) -> bytes:
|
|
70
|
+
encoded: str
|
|
71
|
+
if getattr(arguments, "key_env", None):
|
|
72
|
+
encoded = os.environ.get(arguments.key_env, "")
|
|
73
|
+
if not encoded:
|
|
74
|
+
raise ValueError(f"environment variable {arguments.key_env!r} is empty or unset")
|
|
75
|
+
elif getattr(arguments, "key_file", None):
|
|
76
|
+
path: Path = arguments.key_file
|
|
77
|
+
if os.name != "nt" and stat.S_IMODE(path.stat().st_mode) & 0o077:
|
|
78
|
+
raise ValueError("key file must not be accessible by group or other users")
|
|
79
|
+
encoded = path.read_text(encoding="ascii").strip()
|
|
80
|
+
elif getattr(arguments, "key_fd", None) is not None:
|
|
81
|
+
with os.fdopen(os.dup(arguments.key_fd), encoding="ascii") as stream:
|
|
82
|
+
encoded = stream.read().strip()
|
|
83
|
+
else:
|
|
84
|
+
if arguments.text == "-":
|
|
85
|
+
raise ValueError("standard input cannot provide both the key and text")
|
|
86
|
+
encoded = sys.stdin.read().strip()
|
|
87
|
+
try:
|
|
88
|
+
return base64.b64decode(encoded, validate=True)
|
|
89
|
+
except (binascii.Error, ValueError) as error:
|
|
90
|
+
raise ValueError("key must be valid base64") from error
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
94
|
+
parser = _parser()
|
|
95
|
+
arguments = parser.parse_args(argv)
|
|
96
|
+
if arguments.command == "keygen":
|
|
97
|
+
print(base64.b64encode(generate_key()).decode("ascii"))
|
|
98
|
+
return 0
|
|
99
|
+
if arguments.command == "detectors":
|
|
100
|
+
print("\n".join(detector.name for detector in DEFAULT_DETECTORS))
|
|
101
|
+
return 0
|
|
102
|
+
try:
|
|
103
|
+
key = _read_key(arguments) if _has_key_source(arguments) else None
|
|
104
|
+
engine = Pseudonymizer(
|
|
105
|
+
mode=arguments.mode,
|
|
106
|
+
key=key,
|
|
107
|
+
namespace=arguments.namespace,
|
|
108
|
+
policy=Policy.llm() if arguments.command == "json" else Policy.default(),
|
|
109
|
+
typed_redaction=arguments.typed_redaction,
|
|
110
|
+
)
|
|
111
|
+
if arguments.command == "text":
|
|
112
|
+
source = sys.stdin.read() if arguments.text == "-" else arguments.text
|
|
113
|
+
print(engine.process(source).text)
|
|
114
|
+
return 0
|
|
115
|
+
payload = json.load(sys.stdin)
|
|
116
|
+
output: Data = engine.process_data(payload)
|
|
117
|
+
json.dump(output, sys.stdout, ensure_ascii=False)
|
|
118
|
+
sys.stdout.write("\n")
|
|
119
|
+
return 0
|
|
120
|
+
except (OSError, ValueError, json.JSONDecodeError, PseudonymizeError) as error:
|
|
121
|
+
parser.exit(2, f"pseudonymize: error: {error}\n")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pseudonymize.result import Detection, EntityType
|
|
5
|
+
|
|
6
|
+
_EMAIL = re.compile(
|
|
7
|
+
r"(?<![\w.+-])[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@"
|
|
8
|
+
r"(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}(?![\w-])"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class EmailDetector:
|
|
14
|
+
name: str = "email"
|
|
15
|
+
|
|
16
|
+
def detect(self, text: str) -> list[Detection]:
|
|
17
|
+
return [
|
|
18
|
+
Detection(EntityType.EMAIL, match.start(), match.end(), 0.99, self.name)
|
|
19
|
+
for match in _EMAIL.finditer(text)
|
|
20
|
+
if len(match.group().partition("@")[0]) <= 64 and len(match.group()) <= 254
|
|
21
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pseudonymize.result import Detection, EntityType
|
|
5
|
+
|
|
6
|
+
_IBAN = re.compile(r"(?<![A-Z0-9])[A-Z]{2}\d{2}(?:[ ]?[A-Z0-9]){11,30}(?![A-Z0-9])", re.I)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _valid_mod97(value: str) -> bool:
|
|
10
|
+
compact = "".join(value.split()).upper()
|
|
11
|
+
if not 15 <= len(compact) <= 34:
|
|
12
|
+
return False
|
|
13
|
+
rearranged = compact[4:] + compact[:4]
|
|
14
|
+
remainder = 0
|
|
15
|
+
for character in rearranged:
|
|
16
|
+
digits = str(ord(character) - 55) if character.isalpha() else character
|
|
17
|
+
for digit in digits:
|
|
18
|
+
remainder = (remainder * 10 + int(digit)) % 97
|
|
19
|
+
return remainder == 1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class IbanDetector:
|
|
24
|
+
name: str = "iban"
|
|
25
|
+
|
|
26
|
+
def detect(self, text: str) -> list[Detection]:
|
|
27
|
+
return [
|
|
28
|
+
Detection(EntityType.IBAN, match.start(), match.end(), 1.0, self.name)
|
|
29
|
+
for match in _IBAN.finditer(text)
|
|
30
|
+
if _valid_mod97(match.group())
|
|
31
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from pseudonymize.result import Detection, EntityType
|
|
6
|
+
|
|
7
|
+
_IP_CANDIDATE = re.compile(
|
|
8
|
+
r"(?<![\w:.])(?:\d{1,3}(?:\.\d{1,3}){3}|[0-9A-Fa-f]{0,4}:[0-9A-Fa-f:]+)(?![\w:.])"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class IpAddressDetector:
|
|
14
|
+
name: str = "ip_address"
|
|
15
|
+
|
|
16
|
+
def detect(self, text: str) -> list[Detection]:
|
|
17
|
+
detections: list[Detection] = []
|
|
18
|
+
for match in _IP_CANDIDATE.finditer(text):
|
|
19
|
+
try:
|
|
20
|
+
ipaddress.ip_address(match.group())
|
|
21
|
+
except ValueError:
|
|
22
|
+
continue
|
|
23
|
+
detections.append(
|
|
24
|
+
Detection(EntityType.IP_ADDRESS, match.start(), match.end(), 0.99, self.name)
|
|
25
|
+
)
|
|
26
|
+
return detections
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pseudonymize.result import Detection, EntityType
|
|
5
|
+
|
|
6
|
+
_CARD = re.compile(r"(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _valid_luhn(value: str) -> bool:
|
|
10
|
+
digits = [int(character) for character in value if character.isdigit()]
|
|
11
|
+
if not 13 <= len(digits) <= 19 or len(set(digits)) == 1:
|
|
12
|
+
return False
|
|
13
|
+
total = 0
|
|
14
|
+
parity = len(digits) % 2
|
|
15
|
+
for index, digit in enumerate(digits):
|
|
16
|
+
if index % 2 == parity:
|
|
17
|
+
digit *= 2
|
|
18
|
+
if digit > 9:
|
|
19
|
+
digit -= 9
|
|
20
|
+
total += digit
|
|
21
|
+
return total % 10 == 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class PaymentCardDetector:
|
|
26
|
+
name: str = "payment_card"
|
|
27
|
+
|
|
28
|
+
def detect(self, text: str) -> list[Detection]:
|
|
29
|
+
return [
|
|
30
|
+
Detection(EntityType.PAYMENT_CARD, match.start(), match.end(), 1.0, self.name)
|
|
31
|
+
for match in _CARD.finditer(text)
|
|
32
|
+
if _valid_luhn(match.group())
|
|
33
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pseudonymize.result import Detection, EntityType
|
|
5
|
+
|
|
6
|
+
_PHONE = re.compile(r"(?<![\w+])(?:\+\d{1,3}[ .()-]*)?(?:\d[ .()-]*){6,14}\d(?!\w)")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _credible_phone(value: str) -> bool:
|
|
10
|
+
digits = "".join(character for character in value if character.isdigit())
|
|
11
|
+
if not 7 <= len(digits) <= 15 or len(set(digits)) == 1:
|
|
12
|
+
return False
|
|
13
|
+
separators = sum(character in " .()-" for character in value)
|
|
14
|
+
return value.startswith("+") or separators >= 2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class PhoneDetector:
|
|
19
|
+
name: str = "phone"
|
|
20
|
+
|
|
21
|
+
def detect(self, text: str) -> list[Detection]:
|
|
22
|
+
return [
|
|
23
|
+
Detection(EntityType.PHONE, match.start(), match.end(), 0.86, self.name)
|
|
24
|
+
for match in _PHONE.finditer(text)
|
|
25
|
+
if _credible_phone(match.group())
|
|
26
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pseudonymize.detectors.base import Detector
|
|
2
|
+
from pseudonymize.detectors.email import EmailDetector
|
|
3
|
+
from pseudonymize.detectors.iban import IbanDetector
|
|
4
|
+
from pseudonymize.detectors.ip_address import IpAddressDetector
|
|
5
|
+
from pseudonymize.detectors.payment_card import PaymentCardDetector
|
|
6
|
+
from pseudonymize.detectors.phone import PhoneDetector
|
|
7
|
+
from pseudonymize.detectors.secret import SecretDetector
|
|
8
|
+
from pseudonymize.detectors.url import UrlDetector
|
|
9
|
+
|
|
10
|
+
DEFAULT_DETECTORS: tuple[Detector, ...] = (
|
|
11
|
+
EmailDetector(),
|
|
12
|
+
IpAddressDetector(),
|
|
13
|
+
PaymentCardDetector(),
|
|
14
|
+
IbanDetector(),
|
|
15
|
+
PhoneDetector(),
|
|
16
|
+
UrlDetector(),
|
|
17
|
+
SecretDetector(),
|
|
18
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pseudonymize.result import Detection, EntityType
|
|
5
|
+
|
|
6
|
+
_SECRET_PATTERNS = (
|
|
7
|
+
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
|
8
|
+
re.compile(r"\bgh[pousr]_[A-Za-z0-9]{36,255}\b"),
|
|
9
|
+
re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b"),
|
|
10
|
+
re.compile(
|
|
11
|
+
r"(?i)\b(?:api[_-]?key|client[_-]?secret|password|secret|token)\b\s*[:=]\s*"
|
|
12
|
+
r"(?P<value>['\"]?[A-Za-z0-9_./+=-]{8,}['\"]?)"
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class SecretDetector:
|
|
19
|
+
name: str = "secret"
|
|
20
|
+
|
|
21
|
+
def detect(self, text: str) -> list[Detection]:
|
|
22
|
+
detections: list[Detection] = []
|
|
23
|
+
for pattern in _SECRET_PATTERNS:
|
|
24
|
+
for match in pattern.finditer(text):
|
|
25
|
+
start, end = match.span("value") if "value" in pattern.groupindex else match.span()
|
|
26
|
+
if text[start : start + 1] in {'"', "'"}:
|
|
27
|
+
start += 1
|
|
28
|
+
if text[end - 1 : end] in {'"', "'"}:
|
|
29
|
+
end -= 1
|
|
30
|
+
detections.append(Detection(EntityType.SECRET, start, end, 0.96, self.name))
|
|
31
|
+
return detections
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import urllib.parse
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from pseudonymize.result import Detection, EntityType
|
|
6
|
+
|
|
7
|
+
_URL = re.compile(r"https?://[^\s<>\"']+", re.I)
|
|
8
|
+
_SENSITIVE_QUERY_NAMES = frozenset(
|
|
9
|
+
{"access_token", "api_key", "apikey", "auth", "key", "password", "secret", "token"}
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class UrlDetector:
|
|
15
|
+
name: str = "url"
|
|
16
|
+
|
|
17
|
+
def detect(self, text: str) -> list[Detection]:
|
|
18
|
+
detections: list[Detection] = []
|
|
19
|
+
for match in _URL.finditer(text):
|
|
20
|
+
url = match.group().rstrip(".,;:!?)]}")
|
|
21
|
+
parsed = urllib.parse.urlsplit(url)
|
|
22
|
+
if parsed.username is not None or parsed.password is not None:
|
|
23
|
+
authority_start = url.find("//") + 2
|
|
24
|
+
at = url.find("@", authority_start)
|
|
25
|
+
detections.append(
|
|
26
|
+
Detection(
|
|
27
|
+
EntityType.URL_CREDENTIAL,
|
|
28
|
+
match.start() + authority_start,
|
|
29
|
+
match.start() + at,
|
|
30
|
+
1.0,
|
|
31
|
+
self.name,
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
query_start = url.find("?") + 1
|
|
35
|
+
if query_start == 0:
|
|
36
|
+
continue
|
|
37
|
+
cursor = query_start
|
|
38
|
+
for item in url[query_start:].split("&"):
|
|
39
|
+
name, separator, value = item.partition("=")
|
|
40
|
+
if separator and urllib.parse.unquote_plus(name).lower() in _SENSITIVE_QUERY_NAMES:
|
|
41
|
+
value_start = cursor + len(name) + 1
|
|
42
|
+
if value:
|
|
43
|
+
detections.append(
|
|
44
|
+
Detection(
|
|
45
|
+
EntityType.URL_CREDENTIAL,
|
|
46
|
+
match.start() + value_start,
|
|
47
|
+
match.start() + value_start + len(value),
|
|
48
|
+
0.98,
|
|
49
|
+
self.name,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
cursor += len(item) + 1
|
|
53
|
+
return detections
|