wardcat 1.0.0__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.
Files changed (42) hide show
  1. wardcat/__init__.py +81 -0
  2. wardcat/_entity_policy.py +393 -0
  3. wardcat/config/__init__.py +0 -0
  4. wardcat/config/default.yaml +152 -0
  5. wardcat/config/loader.py +313 -0
  6. wardcat/core/__init__.py +0 -0
  7. wardcat/core/actions.py +151 -0
  8. wardcat/core/anonymizer.py +57 -0
  9. wardcat/core/engine.py +354 -0
  10. wardcat/core/models.py +222 -0
  11. wardcat/core/registry.py +49 -0
  12. wardcat/detectors/__init__.py +0 -0
  13. wardcat/detectors/base.py +59 -0
  14. wardcat/detectors/llm_detector.py +351 -0
  15. wardcat/detectors/ner_detector.py +241 -0
  16. wardcat/detectors/regex_detector.py +666 -0
  17. wardcat/entity_groups.py +87 -0
  18. wardcat/exceptions.py +35 -0
  19. wardcat/guard.py +735 -0
  20. wardcat/llm/__init__.py +0 -0
  21. wardcat/llm/backends/__init__.py +0 -0
  22. wardcat/llm/backends/base.py +107 -0
  23. wardcat/llm/backends/ollama.py +192 -0
  24. wardcat/llm/backends/openai_compat.py +135 -0
  25. wardcat/llm/backends/registry.py +94 -0
  26. wardcat/llm/backends/transformers_backend.py +251 -0
  27. wardcat/llm/backends/vllm.py +107 -0
  28. wardcat/llm/model_catalog.py +99 -0
  29. wardcat/llm/model_manager.py +106 -0
  30. wardcat/llm/prompt.py +522 -0
  31. wardcat/ner/__init__.py +0 -0
  32. wardcat/ner/downloader.py +151 -0
  33. wardcat/ner/spacy_catalog.py +385 -0
  34. wardcat/py.typed +0 -0
  35. wardcat/utils/__init__.py +0 -0
  36. wardcat/utils/hashing.py +7 -0
  37. wardcat/utils/normalize.py +133 -0
  38. wardcat/utils/text.py +41 -0
  39. wardcat-1.0.0.dist-info/METADATA +924 -0
  40. wardcat-1.0.0.dist-info/RECORD +42 -0
  41. wardcat-1.0.0.dist-info/WHEEL +4 -0
  42. wardcat-1.0.0.dist-info/licenses/LICENSE +21 -0
wardcat/__init__.py ADDED
@@ -0,0 +1,81 @@
1
+ import logging
2
+ from importlib.metadata import PackageNotFoundError, version
3
+
4
+ from wardcat.core.actions import ActionContext, register_action, registered_actions
5
+ from wardcat.core.models import (
6
+ KNOWN_ENTITY_TYPES,
7
+ Action,
8
+ Entity,
9
+ RedactedResult,
10
+ RedactedViolation,
11
+ ScanResult,
12
+ Violation,
13
+ )
14
+ from wardcat.entity_groups import (
15
+ all_entities,
16
+ core_entities,
17
+ european_entities,
18
+ financial_entities,
19
+ identity_entities,
20
+ network_entities,
21
+ turkish_entities,
22
+ uk_entities,
23
+ us_entities,
24
+ )
25
+ from wardcat.exceptions import (
26
+ ConfigError,
27
+ ModelDownloadError,
28
+ UnsupportedLanguageError,
29
+ WardcatError,
30
+ )
31
+ from wardcat.guard import Wardcat
32
+ from wardcat.llm.backends.base import Backend
33
+ from wardcat.ner.spacy_catalog import Language, supported_languages
34
+
35
+ try:
36
+ __version__: str = version("wardcat")
37
+ except PackageNotFoundError:
38
+ __version__ = "1.0.0" # development environment fallback
39
+
40
+ __all__ = [
41
+ "Wardcat",
42
+ "ScanResult",
43
+ "Violation",
44
+ "RedactedResult",
45
+ "RedactedViolation",
46
+ "Action",
47
+ "Entity",
48
+ "Language",
49
+ "supported_languages",
50
+ "Backend",
51
+ "register_action",
52
+ "registered_actions",
53
+ "ActionContext",
54
+ "KNOWN_ENTITY_TYPES",
55
+ "__version__",
56
+ "redacted",
57
+ # Exceptions
58
+ "WardcatError",
59
+ "ConfigError",
60
+ "ModelDownloadError",
61
+ "UnsupportedLanguageError",
62
+ # Entity group helpers
63
+ "core_entities",
64
+ "financial_entities",
65
+ "turkish_entities",
66
+ "european_entities",
67
+ "uk_entities",
68
+ "us_entities",
69
+ "network_entities",
70
+ "identity_entities",
71
+ "all_entities",
72
+ ]
73
+
74
+
75
+ def redacted(result: "ScanResult") -> "RedactedResult": # noqa: F821
76
+ """Convenience wrapper — equivalent to ``result.redacted()``."""
77
+ return result.redacted()
78
+
79
+
80
+ # Library logging best practice: NullHandler is added; the application configures the handler.
81
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,393 @@
1
+ """Entity-policy concern of :class:`~wardcat.Wardcat`, factored out as a mixin.
2
+
3
+ This owns *what to detect and how*: the add/remove/change entity operations and
4
+ the read-side introspection, plus their helpers. It operates on the host's
5
+ ``self._config`` dict and asks the host to rebuild via ``self._rebuild()`` — it
6
+ holds no detectors itself. Keeping it separate makes the entity rules a single,
7
+ cohesive unit and slims the ``Wardcat`` facade.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from collections.abc import Iterable, Mapping
14
+ from typing import Any, Self
15
+
16
+ from wardcat.core.models import KNOWN_ENTITY_TYPES, Action, Entity, warn_unknown_entity
17
+ from wardcat.core.registry import LAYER_ENTITIES, VALID_LAYERS
18
+ from wardcat.exceptions import ConfigError
19
+
20
+ logger = logging.getLogger("wardcat.guard")
21
+
22
+
23
+ class EntityPolicyMixin:
24
+ """Entity configuration + introspection for ``Wardcat``.
25
+
26
+ The composing class must provide ``_config`` (the config dict),
27
+ ``_default_action_warned`` (a bool flag), and a ``_rebuild()`` method.
28
+ """
29
+
30
+ # Provided by the composing class (Wardcat) — declared for type checkers.
31
+ _config: dict[str, Any]
32
+ _default_action_warned: bool
33
+
34
+ def _rebuild(self) -> None: # pragma: no cover - overridden by Wardcat
35
+ raise NotImplementedError
36
+
37
+ # ------------------------------------------------------------------
38
+ # Write API
39
+ # ------------------------------------------------------------------
40
+
41
+ def add_entity(
42
+ self,
43
+ entity_type: str | Entity,
44
+ action: str | Action | None = None,
45
+ layers: list[str] | None = None,
46
+ ) -> Self:
47
+ """
48
+ Enable a single entity type (or every type via :attr:`Entity.ALL`).
49
+
50
+ Adding an entity always enables it — to turn one off, use
51
+ :meth:`remove_entity`. Supports method chaining.
52
+
53
+ :param entity_type: An :class:`~wardcat.Entity` constant (recommended,
54
+ e.g. ``Entity.EMAIL``) or its string form (``"EMAIL"``).
55
+ Pass :attr:`Entity.ALL` to enable **every** known
56
+ entity in one call, then prune with
57
+ :meth:`remove_entity`.
58
+ :param action: An :class:`~wardcat.Action` constant or its string
59
+ form. **When omitted, defaults to** ``"hash"`` and a
60
+ warning is logged (once per guard).
61
+ :param layers: Which detector layers should look for this entity —
62
+ any of ``"regex"``, ``"ner"``, ``"llm"``. ``None``
63
+ (default) uses every layer that supports the entity.
64
+ :raises ConfigError: if ``entity_type`` is not a ``str``/``Entity``, the
65
+ ``action`` is invalid, or a ``layer`` is unknown.
66
+ """
67
+ action = self._action_or_default(action)
68
+ if self._is_all(entity_type):
69
+ for name in sorted(KNOWN_ENTITY_TYPES):
70
+ self._set_entity(name, enabled=True, action=action, layers=layers)
71
+ else:
72
+ self._set_entity(entity_type, enabled=True, action=action, layers=layers)
73
+ self._rebuild()
74
+ return self
75
+
76
+ def add_entities(
77
+ self,
78
+ entities: Iterable[str | Entity] | Mapping[str | Entity, Any],
79
+ *,
80
+ action: str | Action | None = None,
81
+ layers: list[str] | None = None,
82
+ ) -> Self:
83
+ """
84
+ Enable many entity types at once (single rebuild). Supports chaining.
85
+
86
+ ``entities`` may be an iterable of names, a ``{name: action}`` mapping, or
87
+ a ``{name: {"action": ..., "layers": ...}}`` mapping for per-entity
88
+ control. :attr:`Entity.ALL` may appear as an entry. Any entity left
89
+ without an action defaults to ``"hash"`` (warned once per guard).
90
+
91
+ :raises ConfigError: if ``entities`` is not a mapping or iterable, or any
92
+ spec/action/entity is invalid.
93
+ """
94
+ if isinstance(entities, dict):
95
+ specs = entities
96
+ elif isinstance(entities, str | Entity):
97
+ # A bare string/Entity is a common mistake — it would iterate chars.
98
+ raise ConfigError(
99
+ f"add_entities() expects a mapping or an iterable of entity types, "
100
+ f"not a single {type(entities).__name__}. Use add_entity() for one entity."
101
+ )
102
+ else:
103
+ try:
104
+ specs = dict.fromkeys(entities, {})
105
+ except TypeError as exc:
106
+ raise ConfigError(
107
+ f"add_entities() expects a mapping or an iterable of entity types, "
108
+ f"got {type(entities).__name__}."
109
+ ) from exc
110
+
111
+ for name, spec in specs.items():
112
+ if isinstance(spec, str):
113
+ spec = {"action": spec}
114
+ elif spec is None:
115
+ spec = {}
116
+ elif not isinstance(spec, dict):
117
+ raise ConfigError(
118
+ f"Invalid spec for {name!r}: expected str, dict, or None, "
119
+ f"got {type(spec).__name__}."
120
+ )
121
+ entity_action = self._action_or_default(spec.get("action", action))
122
+ entity_layers = spec.get("layers", layers)
123
+ names = sorted(KNOWN_ENTITY_TYPES) if self._is_all(name) else [name]
124
+ for n in names:
125
+ self._set_entity(n, enabled=True, action=entity_action, layers=entity_layers)
126
+ self._rebuild()
127
+ return self
128
+
129
+ def remove_entity(self, entity_type: str | Entity) -> Self:
130
+ """
131
+ Disable a single entity type (or every type via :attr:`Entity.ALL`).
132
+
133
+ Removing an entity that was never enabled is a no-op. An unknown entity
134
+ *name* (likely a typo) logs a warning, like :meth:`add_entity`. Chainable.
135
+
136
+ :raises ConfigError: if ``entity_type`` is not a ``str`` or ``Entity``.
137
+ """
138
+ if self._is_all(entity_type):
139
+ self._disable_all()
140
+ else:
141
+ name = self._normalize_entity(entity_type)
142
+ self._warn_if_unknown(name)
143
+ self._disable_entity(name)
144
+ self._rebuild()
145
+ return self
146
+
147
+ def remove_entities(self, entities: Iterable[str | Entity]) -> Self:
148
+ """
149
+ Disable many entity types at once (single rebuild). Supports chaining.
150
+
151
+ :raises ConfigError: if ``entities`` is not an iterable of entity types.
152
+ """
153
+ if isinstance(entities, str | Entity):
154
+ raise ConfigError(
155
+ f"remove_entities() expects an iterable of entity types, not a single "
156
+ f"{type(entities).__name__}. Use remove_entity() for one entity."
157
+ )
158
+ try:
159
+ items = list(entities)
160
+ except TypeError as exc:
161
+ raise ConfigError(
162
+ f"remove_entities() expects an iterable of entity types, "
163
+ f"got {type(entities).__name__}."
164
+ ) from exc
165
+
166
+ for entity_type in items:
167
+ if self._is_all(entity_type):
168
+ self._disable_all()
169
+ else:
170
+ name = self._normalize_entity(entity_type)
171
+ self._warn_if_unknown(name)
172
+ self._disable_entity(name)
173
+ self._rebuild()
174
+ return self
175
+
176
+ def change_entity_action(self, entity_type: str | Entity, action: str | Action) -> Self:
177
+ """
178
+ Change the action of an entity that is **currently enabled**.
179
+
180
+ Never enables a new entity: it only retargets the action of an active
181
+ one. If the entity was removed or never added, it raises. The layers it
182
+ runs on are unchanged. :attr:`Entity.ALL` changes every enabled entity.
183
+
184
+ :raises ConfigError: if ``entity_type`` is not a ``str``/``Entity``, the
185
+ ``action`` is invalid, or the entity (or, for
186
+ ``Entity.ALL``, *any* entity) is not currently enabled.
187
+ """
188
+ action = self._normalize_action(action)
189
+
190
+ if self._is_all(entity_type):
191
+ active = self._active_entities()
192
+ if not active:
193
+ raise ConfigError(
194
+ "change_entity_action(Entity.ALL) failed: no entities are currently "
195
+ "enabled. Enable some first with add_entity()."
196
+ )
197
+ for name in sorted(active):
198
+ self._apply_action(name, action)
199
+ else:
200
+ name = self._normalize_entity(entity_type)
201
+ if not self._is_entity_active(name):
202
+ raise ConfigError(
203
+ f"Cannot change action for {name!r}: it is not enabled "
204
+ "(it was removed or never added). Enable it first with add_entity()."
205
+ )
206
+ self._apply_action(name, action)
207
+ self._rebuild()
208
+ return self
209
+
210
+ # ------------------------------------------------------------------
211
+ # Read API (introspection)
212
+ # ------------------------------------------------------------------
213
+
214
+ def enabled_entities(self) -> set[str]:
215
+ """Return the set of entity types currently enabled on any detector layer."""
216
+ return self._active_entities()
217
+
218
+ def get_entity_action(self, entity_type: str | Entity) -> str | None:
219
+ """Return the action of a currently-enabled entity, or ``None`` if not enabled.
220
+
221
+ :raises ConfigError: if ``entity_type`` is not a ``str``/``Entity``, or is
222
+ :attr:`Entity.ALL` (use :meth:`entity_policy` instead).
223
+ """
224
+ if self._is_all(entity_type):
225
+ raise ConfigError(
226
+ "get_entity_action() does not accept Entity.ALL — use entity_policy()."
227
+ )
228
+ name = self._normalize_entity(entity_type)
229
+ if not self._is_entity_active(name):
230
+ return None
231
+ return self._entity_action(name)
232
+
233
+ def entity_policy(self) -> dict[str, str]:
234
+ """Return a ``{entity_type: action}`` mapping of every enabled entity."""
235
+ policy: dict[str, str] = {}
236
+ for name in sorted(self._active_entities()):
237
+ action = self._entity_action(name)
238
+ if action is not None:
239
+ policy[name] = action
240
+ return policy
241
+
242
+ # ------------------------------------------------------------------
243
+ # Helpers
244
+ # ------------------------------------------------------------------
245
+
246
+ @staticmethod
247
+ def _is_all(entity_type: object) -> bool:
248
+ """True if *entity_type* is the :attr:`Entity.ALL` sentinel (``Entity.All`` too)."""
249
+ return entity_type is Entity.ALL or entity_type == Entity.ALL.value
250
+
251
+ @staticmethod
252
+ def _normalize_entity(entity_type: str | Entity) -> str:
253
+ """Coerce an Entity/str to its canonical string value, validating the type."""
254
+ if isinstance(entity_type, Entity):
255
+ return entity_type.value
256
+ if isinstance(entity_type, str):
257
+ return entity_type
258
+ raise ConfigError(f"entity_type must be a str or Entity, got {type(entity_type).__name__}.")
259
+
260
+ def _action_or_default(self, action: str | Action | None) -> str | Action:
261
+ """Return *action*, or the default ``hash`` (warned once) when unspecified."""
262
+ if action is None:
263
+ self._warn_default_action_once()
264
+ return Action.HASH.value
265
+ return action
266
+
267
+ def _warn_default_action_once(self) -> None:
268
+ """Warn at most once per guard that a missing action defaulted to ``hash``."""
269
+ if self._default_action_warned:
270
+ return
271
+ self._default_action_warned = True
272
+ logger.warning(
273
+ "No action specified for one or more entities — defaulting to 'hash' (the safest "
274
+ "action). Pass action=... (e.g. Action.WARN) to choose explicitly. This is logged "
275
+ "once per guard."
276
+ )
277
+
278
+ @staticmethod
279
+ def _normalize_action(action: str | Action) -> str:
280
+ """Validate an Action/str against the action registry; return its name."""
281
+ from wardcat.core.actions import registered_actions
282
+
283
+ name = action.value if isinstance(action, Action) else action
284
+ if not isinstance(name, str) or name not in registered_actions():
285
+ raise ConfigError(
286
+ f"Invalid action {action!r}. Registered actions: {sorted(registered_actions())}. "
287
+ "Add one with wardcat.register_action(name, fn)."
288
+ )
289
+ return name
290
+
291
+ def _warn_if_unknown(self, entity_type: str) -> None:
292
+ """Log a warning if *entity_type* is neither a known nor a custom entity."""
293
+ if entity_type not in self._config.get("custom_patterns", {}):
294
+ warn_unknown_entity(entity_type)
295
+
296
+ def _is_entity_active(self, entity_type: str) -> bool:
297
+ """True if *entity_type* is currently enabled on any *active* detector layer."""
298
+ ent = self._config.get("entities", {}).get(entity_type)
299
+ if ent and ent.get("enabled"):
300
+ return True
301
+ # LLM entities only count when the LLM layer itself is enabled.
302
+ llm_cfg = self._config.get("llm_detector", {})
303
+ if llm_cfg.get("enabled"):
304
+ llm = llm_cfg.get("entities", {}).get(entity_type)
305
+ if llm and llm.get("enabled"):
306
+ return True
307
+ return False
308
+
309
+ def _entity_action(self, entity_type: str) -> str | None:
310
+ """Return the configured action for *entity_type*, or None if unset."""
311
+ ent = self._config.get("entities", {}).get(entity_type)
312
+ if ent and "action" in ent:
313
+ return str(ent["action"])
314
+ llm = self._config.get("llm_detector", {}).get("entities", {}).get(entity_type)
315
+ if llm and "action" in llm:
316
+ return str(llm["action"])
317
+ return None
318
+
319
+ def _active_entities(self) -> set[str]:
320
+ """The set of entity types currently enabled on any layer."""
321
+ names = set(self._config.get("entities", {})) | set(
322
+ self._config.get("llm_detector", {}).get("entities", {})
323
+ )
324
+ return {name for name in names if self._is_entity_active(name)}
325
+
326
+ def _apply_action(self, entity_type: str, action: str) -> None:
327
+ """Update an entity's action on every layer where it exists (no rebuild)."""
328
+ entities = self._config.get("entities", {})
329
+ if entity_type in entities:
330
+ entities[entity_type]["action"] = action
331
+ llm_entities = self._config.get("llm_detector", {}).get("entities", {})
332
+ if entity_type in llm_entities:
333
+ llm_entities[entity_type]["action"] = action
334
+
335
+ def _disable_entity(self, entity_type: str) -> None:
336
+ """Set an entity's ``enabled`` flag to False on every layer (no rebuild)."""
337
+ entities = self._config.get("entities", {})
338
+ if entity_type in entities:
339
+ entities[entity_type]["enabled"] = False
340
+ llm_entities = self._config.get("llm_detector", {}).get("entities", {})
341
+ if entity_type in llm_entities:
342
+ llm_entities[entity_type]["enabled"] = False
343
+
344
+ def _disable_all(self) -> None:
345
+ """Disable every configured entity on every layer (no rebuild)."""
346
+ for name in list(self._config.get("entities", {})):
347
+ self._disable_entity(name)
348
+ for name in list(self._config.get("llm_detector", {}).get("entities", {})):
349
+ self._disable_entity(name)
350
+
351
+ def _set_entity(
352
+ self,
353
+ entity_type: str | Entity,
354
+ *,
355
+ enabled: bool,
356
+ action: str | Action,
357
+ layers: list[str] | None,
358
+ ) -> None:
359
+ """Mutate config for one entity across the chosen layers (no rebuild)."""
360
+ # (Entity.ALL must be expanded by the caller, never reach here.)
361
+ if self._is_all(entity_type):
362
+ raise ConfigError(
363
+ "Entity.ALL cannot be set directly — it is expanded by add_entity()/"
364
+ "add_entities() into the individual known entity types."
365
+ )
366
+ entity_type = self._normalize_entity(entity_type)
367
+ action = self._normalize_action(action)
368
+ self._warn_if_unknown(entity_type)
369
+
370
+ if layers is None:
371
+ target = [lyr for lyr, ents in LAYER_ENTITIES.items() if entity_type in ents]
372
+ if not target: # unknown / custom entity → default to regex
373
+ target = ["regex"]
374
+ else:
375
+ invalid = set(layers) - VALID_LAYERS
376
+ if invalid:
377
+ raise ConfigError(
378
+ f"Invalid layer(s) {sorted(invalid)}. Valid: {sorted(VALID_LAYERS)}"
379
+ )
380
+ target = list(layers)
381
+
382
+ # config["entities"] holds the action (always, so the engine can apply
383
+ # it) and the shared enabled flag for the regex/NER layers (each
384
+ # of which only fires when its own layer switch is on).
385
+ uses_shared_map = ("regex" in target) or ("ner" in target)
386
+ self._config.setdefault("entities", {})[entity_type] = {
387
+ "enabled": enabled and uses_shared_map,
388
+ "action": action,
389
+ }
390
+ # The LLM layer keeps its own enabled set.
391
+ if "llm" in target:
392
+ llm_entities = self._config.setdefault("llm_detector", {}).setdefault("entities", {})
393
+ llm_entities[entity_type] = {"enabled": enabled, "action": action}
File without changes
@@ -0,0 +1,152 @@
1
+ # wardcat varsayılan politika dosyası
2
+ #
3
+ # LLM dedektörü hızlı başlangıç:
4
+ # 1. Ollama kur: https://ollama.com
5
+ # 2. Modeli indir: ollama pull llama3.2
6
+ # (veya Wardcat(...).with_llm(model="llama3.2", auto_pull=True) ile otomatik)
7
+ # 3. Bu dosyada llm_detector.enabled: true yap
8
+ # Bu dosyayı kopyalayarak projenize özel politika oluşturabilirsiniz.
9
+ #
10
+ # Kullanım:
11
+ # from wardcat import Wardcat
12
+ # guard = Wardcat(config_path="config/default.yaml")
13
+
14
+ # Hash tuzlama değeri. Boş bırakılırsa salt kullanılmaz.
15
+ # Üretim ortamında ortam değişkeninden okuyun, buraya yazmayın.
16
+ salt: ""
17
+
18
+ # NER varsayılan olarak KAPALIDIR. Açmak için bir model/dil belirtmelisiniz —
19
+ # kod tarafında Wardcat().with_ner(language=Language.EN) ya da Wardcat().with_ner(spacy_model="en_core_web_sm"),
20
+ # veya bu dosyada hem use_ner: true hem de bir spacy_model verin:
21
+ # use_ner: true
22
+ # spacy_model: "en_core_web_sm" # tek model
23
+ # spacy_models: ["en_core_web_sm", "de_core_news_sm"] # çoklu (multilingual)
24
+ use_ner: false
25
+
26
+ entities:
27
+ # ── Regex tabanlı ──────────────────────────────────────────
28
+ CREDIT_CARD:
29
+ enabled: true
30
+ action: hash # "warn" veya "hash"
31
+
32
+ EMAIL:
33
+ enabled: true
34
+ action: warn
35
+
36
+ PHONE:
37
+ enabled: true
38
+ action: warn
39
+
40
+ IBAN:
41
+ enabled: true
42
+ action: hash
43
+
44
+ IP_ADDRESS:
45
+ enabled: true
46
+ action: warn
47
+
48
+ TC_ID: # Türkiye Cumhuriyeti Kimlik Numarası
49
+ enabled: true
50
+ action: hash
51
+
52
+ ADDRESS: # Regex: Turkish address patterns (Cad., Sok., Mah. etc.)
53
+ enabled: true
54
+ action: warn
55
+
56
+ POSTAL_CODE: # Turkish postal code (01000-81999)
57
+ enabled: true
58
+ action: warn
59
+
60
+ FINANCIAL_AMOUNT: # Para tutarları: 45.000 TL, $85,000, €12.500, 2.1 milyon TL
61
+ enabled: false # Gürültülü olabilir; gizli belge taramasında açın
62
+ action: redact
63
+
64
+ VAT_NUMBER: # Vergi no: DE123456789, FRXX999999999, GB999999999, TR Vergi No
65
+ enabled: true
66
+ action: warn
67
+
68
+ # ── Global identity / technical ────────────────────────────
69
+ UUID: # RFC 4122 UUID: 550e8400-e29b-41d4-a716-446655440000
70
+ enabled: true
71
+ action: warn
72
+
73
+ SSN: # US Social Security Number: 123-45-6789
74
+ enabled: true
75
+ action: hash
76
+
77
+ MAC_ADDRESS: # Network hardware address: 00:1A:2B:3C:4D:5E
78
+ enabled: true
79
+ action: warn
80
+
81
+ JWT: # JSON Web Token (starts with eyJ)
82
+ enabled: true
83
+ action: hash
84
+
85
+ IPv6: # IPv6 address: 2001:db8::1
86
+ enabled: true
87
+ action: warn
88
+
89
+ NIN: # UK National Insurance Number: AB123456C
90
+ enabled: true
91
+ action: hash
92
+
93
+ # ── SpaCy NER tabanlı ──────────────────────────────────────
94
+ # Türkçe model için: spacy_model: "tr_core_news_sm"
95
+ # Kurulum: uv pip install <tr model wheel URL>
96
+ PERSON:
97
+ enabled: true
98
+ action: hash
99
+
100
+ ORG:
101
+ enabled: false # Kurumsal isimler, ihtiyaca göre açın
102
+ action: warn
103
+
104
+ # ── On-Prem LLM Dedektörü ──────────────────────────────────────────────────
105
+ # Regex ve NER'ın yanında üçüncü bir dedektör katmanı.
106
+ # Bağlamsal PII ve kuruma özgü sırları yakalamak için idealdir.
107
+ llm_detector:
108
+ enabled: false # true yaparak etkinleştir
109
+
110
+ # Backend seçimi:
111
+ # ollama — yerel Ollama servisi (model indirme desteği)
112
+ # openai_compatible — vLLM, LM Studio, LocalAI, LiteLLM
113
+ backend: ollama
114
+
115
+ model: llama3.2 # Önerilen: llama3.2, llama3.2:3b (hızlı), mistral
116
+ base_url: http://localhost:11434 # Ollama default
117
+ # base_url: http://10.0.0.5:11434 # Uzak Ollama sunucusu
118
+ # base_url: http://vllm-server:8000/v1 # vLLM için
119
+
120
+ api_key: "" # OpenAI-compat için; çoğu on-prem boş bırakır
121
+ timeout: 60 # Saniye cinsinden LLM yanıt bekleme süresi
122
+
123
+ # Ensemble adjudication: açıkken regex/NER'ın bulduğu adaylar LLM'e
124
+ # "doğrula / yeniden etiketle / ele" diye gönderilir ve LLM aynı çağrıda
125
+ # yeni PII de ekler. Deterministik (checksum'lı) regex sonuçları her zaman
126
+ # korunur. LLM tek başına kullanılıyorsa (regex/NER yok) etkisizdir.
127
+ adjudicate: false
128
+
129
+ # LLM'e hangi entity tipleri için bakması istensin
130
+ # Regex/NER'ın yakalayamadığı bağlamsal verileri buraya ekleyin
131
+ entities:
132
+ CREDIT_CARD: { enabled: true, action: hash }
133
+ EMAIL: { enabled: true, action: warn }
134
+ PHONE: { enabled: true, action: warn }
135
+ PERSON: { enabled: true, action: hash }
136
+ ORG: { enabled: false, action: warn }
137
+ ADDRESS: { enabled: true, action: warn }
138
+ IBAN: { enabled: true, action: hash }
139
+ TC_ID: { enabled: true, action: hash }
140
+ IP_ADDRESS: { enabled: true, action: warn }
141
+ IPv6: { enabled: true, action: warn }
142
+ UUID: { enabled: true, action: warn }
143
+ SSN: { enabled: true, action: hash }
144
+ MAC_ADDRESS: { enabled: true, action: warn }
145
+ JWT: { enabled: true, action: hash }
146
+ POSTAL_CODE: { enabled: true, action: warn }
147
+ NIN: { enabled: true, action: hash }
148
+ CUSTOM_SECRET: { enabled: false, action: hash } # context-specific secrets
149
+ # GDPR Madde 9 özel nitelikli veri (sağlık, din, etnik köken, siyasi görüş,
150
+ # cinsel yönelim, sendika, genetik/biyometrik). Anlamsal — sadece LLM yakalar.
151
+ # Öznel ve yanlış-pozitif riski yüksek olduğu için varsayılan kapalı.
152
+ SPECIAL_CATEGORY: { enabled: false, action: redact }