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.
- wardcat/__init__.py +81 -0
- wardcat/_entity_policy.py +393 -0
- wardcat/config/__init__.py +0 -0
- wardcat/config/default.yaml +152 -0
- wardcat/config/loader.py +313 -0
- wardcat/core/__init__.py +0 -0
- wardcat/core/actions.py +151 -0
- wardcat/core/anonymizer.py +57 -0
- wardcat/core/engine.py +354 -0
- wardcat/core/models.py +222 -0
- wardcat/core/registry.py +49 -0
- wardcat/detectors/__init__.py +0 -0
- wardcat/detectors/base.py +59 -0
- wardcat/detectors/llm_detector.py +351 -0
- wardcat/detectors/ner_detector.py +241 -0
- wardcat/detectors/regex_detector.py +666 -0
- wardcat/entity_groups.py +87 -0
- wardcat/exceptions.py +35 -0
- wardcat/guard.py +735 -0
- wardcat/llm/__init__.py +0 -0
- wardcat/llm/backends/__init__.py +0 -0
- wardcat/llm/backends/base.py +107 -0
- wardcat/llm/backends/ollama.py +192 -0
- wardcat/llm/backends/openai_compat.py +135 -0
- wardcat/llm/backends/registry.py +94 -0
- wardcat/llm/backends/transformers_backend.py +251 -0
- wardcat/llm/backends/vllm.py +107 -0
- wardcat/llm/model_catalog.py +99 -0
- wardcat/llm/model_manager.py +106 -0
- wardcat/llm/prompt.py +522 -0
- wardcat/ner/__init__.py +0 -0
- wardcat/ner/downloader.py +151 -0
- wardcat/ner/spacy_catalog.py +385 -0
- wardcat/py.typed +0 -0
- wardcat/utils/__init__.py +0 -0
- wardcat/utils/hashing.py +7 -0
- wardcat/utils/normalize.py +133 -0
- wardcat/utils/text.py +41 -0
- wardcat-1.0.0.dist-info/METADATA +924 -0
- wardcat-1.0.0.dist-info/RECORD +42 -0
- wardcat-1.0.0.dist-info/WHEEL +4 -0
- 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 }
|