Skip to content

Commit b59d7c5

Browse files
committed
Add PII, toxicity and regex scanners
Introduce three new scanner modules and associated docs/tests: - Add src/pytector/pii.py (PIIScanner) using a transformer NER pipeline (default: joneauxedgar/pasteproof-pii-detector-v2) to scan, redact and report PII entities. - Add src/pytector/toxicity.py (ToxicityDetector) using a text-classification pipeline (default: citizenlab/distilbert-base-multilingual-cased-toxicity) to score/flag toxic text. - Add src/pytector/regex_scanner.py (RegexScanner) — a pure-stdlib, rule-based scanner with sensible default patterns and runtime pattern management. Also: - Export the new classes from src/pytector/__init__.py and bump package version to 0.3.1. - Add unit tests for each new scanner (tests/test_pii.py, tests/test_regex_scanner.py, tests/test_toxicity.py). - Update README, docs (api, examples, index, quickstart), and the notebook demo with usage examples and feature descriptions.
1 parent dc83a7f commit b59d7c5

14 files changed

Lines changed: 1547 additions & 88 deletions

README.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ Pytector provides a defence-in-depth layer for prompt injection. Always combine
4747
- **LangChain Guardrail Runnable**: Adds `PytectorGuard` for LCEL pipelines (`guard | prompt | llm`) so unsafe prompts can be blocked before model execution.
4848
- **Keyword-Based Blocking**: Provides restrictive keyword filtering for both input and output layers with customizable keyword lists for immediate security control.
4949
- **Input Sanitization**: Cleans user input through a six-strategy pipeline — encoding detection (Base64/hex/ROT13), unicode normalization, regex pattern removal, sentence-level heuristic scoring, fuzzy matching, and keyword stripping — with an optional prompt enforcement layer for template escaping. Zero additional dependencies.
50+
- **PII Detection**: Scans text for personally identifiable information using the [PasteProof PII Detector](https://huggingface.co/joneauxedgar/pasteproof-pii-detector-v2) (ModernBERT NER, F1 0.97) with 27 entity types covering financial, credential, healthcare, GDPR, identity, contact, and address data. Supports scan, redact, and report workflows.
51+
- **Toxicity Detection**: Classifies text as toxic or non-toxic using a multilingual [citizenlab DistilBERT](https://huggingface.co/citizenlab/distilbert-base-multilingual-cased-toxicity) model (F1 0.94, 10 languages). Returns a toxicity score mirroring the prompt injection detector API.
52+
- **Regex Scanner**: Rule-based pattern matching for PII and credentials (email, phone, SSN, credit card, IP, API keys, JWT) using pure Python stdlib. Fully customizable — add, remove, or replace patterns at construction or runtime.
5053
- **Customizable Detection**: Allows switching between local model inference and API-based detection (Groq) with customizable thresholds.
5154
- **Flexible Model Options**: Use pre-defined models or provide a custom model URL.
5255
- **Rapid Deployment**: Designed for quick integration into projects that need immediate security layers beyond foundation model defaults.
@@ -132,7 +135,7 @@ To use Pytector, import the `PromptInjectionDetector` class and create an instan
132135
## Notebook Demo
133136

134137
An end-to-end Jupyter notebook is available at `notebooks/pytector_demo.ipynb`.
135-
It covers local inference, keyword blocking, Groq integration with `openai/gpt-oss-safeguard-20b`, both Prompt Guard 2 models (`meta-llama/llama-prompt-guard-2-22m` and `meta-llama/llama-prompt-guard-2-86m`), LangChain LCEL guardrail usage, and input sanitization with `PromptSanitizer`.
138+
It covers local inference, keyword blocking, Groq integration with `openai/gpt-oss-safeguard-20b`, both Prompt Guard 2 models (`meta-llama/llama-prompt-guard-2-22m` and `meta-llama/llama-prompt-guard-2-86m`), LangChain LCEL guardrail usage, input sanitization with `PromptSanitizer`, PII detection with `PIIScanner`, toxicity detection with `ToxicityDetector`, and regex-based scanning with `RegexScanner`.
136139

137140
### Groq Migration Note (March 5, 2026)
138141
`meta-llama/llama-guard-4-12b` was deprecated in favor of `openai/gpt-oss-safeguard-20b`.
@@ -395,6 +398,83 @@ sanitizer.remove_keywords("evil")
395398
print(sanitizer.get_keywords())
396399
```
397400

401+
### Example 10: PII Detection
402+
Scan text for personally identifiable information using a transformer NER model:
403+
404+
```python
405+
from pytector import PIIScanner
406+
407+
scanner = PIIScanner() # defaults to pasteproof-v3
408+
409+
# Scan returns (has_pii, entities) — same tuple style as check_input_keywords()
410+
has_pii, entities = scanner.scan("Contact john@acme.com, SSN 123-45-6789")
411+
for ent in entities:
412+
print(f" [{ent['type']}] {ent['text']} (score={ent['score']:.2f})")
413+
414+
# Redact PII in-place
415+
redacted = scanner.redact("Contact john@acme.com, SSN 123-45-6789")
416+
print(redacted) # "Contact [REDACTED], SSN [REDACTED]"
417+
418+
# Human-readable report
419+
scanner.report("Contact john@acme.com, SSN 123-45-6789")
420+
421+
# Filter to specific entity types
422+
scanner = PIIScanner(entity_types=["EMAIL", "CREDIT_CARD"])
423+
has_pii, entities = scanner.scan("Email: a@b.com, SSN: 123-45-6789")
424+
# Only EMAIL entities returned; SSN is ignored
425+
```
426+
427+
### Example 11: Toxicity Detection
428+
Classify text as toxic or non-toxic:
429+
430+
```python
431+
from pytector import ToxicityDetector
432+
433+
detector = ToxicityDetector() # defaults to citizenlab multilingual model
434+
435+
# Returns (is_toxic, score) — same shape as detect_injection()
436+
is_toxic, score = detector.detect("You are terrible and worthless")
437+
print(f"Toxic: {is_toxic}, Score: {score:.2f}")
438+
439+
# Adjust threshold per call
440+
is_toxic, score = detector.detect("You are terrible", threshold=0.8)
441+
442+
# Human-readable report
443+
detector.report("Have a wonderful day!")
444+
```
445+
446+
### Example 12: Regex Scanner (Customizable)
447+
Fast, rule-based pattern matching with no model downloads:
448+
449+
```python
450+
from pytector import RegexScanner
451+
452+
# Ships with defaults: EMAIL, PHONE, SSN, CREDIT_CARD, IP_ADDRESS, API_KEY, JWT_TOKEN
453+
scanner = RegexScanner()
454+
455+
has_match, matches = scanner.scan("Key: sk-live-abc123def456, IP: 10.0.0.1")
456+
for m in matches:
457+
print(f" [{m['pattern_name']}] {m['match']}")
458+
459+
# Redact all matches
460+
print(scanner.redact("Email me at user@example.com"))
461+
# "Email me at [REDACTED]"
462+
463+
# Add your own patterns
464+
scanner.add_pattern("AWS_ACCESS_KEY", r"AKIA[0-9A-Z]{16}")
465+
scanner.add_pattern("INTERNAL_ID", r"INT-\d{6}")
466+
467+
# Remove a default pattern
468+
scanner.remove_pattern("JWT_TOKEN")
469+
470+
# Use only custom patterns (no defaults)
471+
custom = RegexScanner(
472+
patterns={"ORDER_ID": r"ORD-\d{8}", "ZIP": r"\b\d{5}(?:-\d{4})?\b"},
473+
use_defaults=False,
474+
)
475+
print(custom.get_patterns()) # {'ORDER_ID': ..., 'ZIP': ...}
476+
```
477+
398478

399479

400480
## Security Best Practices

docs/api.rst

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,126 @@ Sanitizer Configuration
182182
* - sentence_threshold
183183
- 0.5
184184
- Heuristic score cutoff for sentence removal (0.0-1.0)
185+
186+
PIIScanner
187+
----------
188+
189+
.. automodule:: pytector.pii
190+
:members:
191+
:undoc-members:
192+
:show-inheritance:
193+
194+
Uses the `PasteProof PII Detector <https://huggingface.co/joneauxedgar/pasteproof-pii-detector-v2>`_
195+
(ModernBERT-base, F1 0.97) for NER-based PII detection across 27 entity types.
196+
Requires ``transformers >= 4.48.0`` for ModernBERT support.
197+
198+
.. code-block:: python
199+
200+
from pytector import PIIScanner
201+
202+
scanner = PIIScanner()
203+
has_pii, entities = scanner.scan("Email john@acme.com, SSN 123-45-6789")
204+
print(scanner.redact("Email john@acme.com, SSN 123-45-6789"))
205+
206+
# Filter to specific entity types
207+
scanner = PIIScanner(entity_types=["EMAIL", "CREDIT_CARD"], threshold=0.7)
208+
209+
.. list-table:: PIIScanner Parameters
210+
:widths: 20 20 60
211+
:header-rows: 1
212+
213+
* - Parameter
214+
- Type
215+
- Description
216+
* - model_name
217+
- str
218+
- Predefined key (``pasteproof-v3``) or HuggingFace model ID / local path
219+
* - threshold
220+
- float
221+
- Minimum confidence for an entity to be reported (default 0.5)
222+
* - entity_types
223+
- list[str] | None
224+
- Filter to specific types (e.g. ``["EMAIL", "SSN"]``); ``None`` = all
225+
226+
.. admonition:: Citation
227+
228+
.. code-block:: text
229+
230+
@model{pasteproof_pii_detector,
231+
author = {Jonathan Edgar},
232+
title = {PasteProof PII Detector},
233+
year = {2025},
234+
publisher = {Hugging Face},
235+
url = {https://huggingface.co/joneauxedgar/pasteproof-pii-detector-v2}
236+
}
237+
238+
ToxicityDetector
239+
----------------
240+
241+
.. automodule:: pytector.toxicity
242+
:members:
243+
:undoc-members:
244+
:show-inheritance:
245+
246+
Uses `citizenlab/distilbert-base-multilingual-cased-toxicity <https://huggingface.co/citizenlab/distilbert-base-multilingual-cased-toxicity>`_
247+
(F1-micro 0.94, 10 languages) for toxicity classification.
248+
249+
.. code-block:: python
250+
251+
from pytector import ToxicityDetector
252+
253+
detector = ToxicityDetector()
254+
is_toxic, score = detector.detect("You are terrible")
255+
detector.report("Have a wonderful day!")
256+
257+
.. list-table:: ToxicityDetector Parameters
258+
:widths: 20 20 60
259+
:header-rows: 1
260+
261+
* - Parameter
262+
- Type
263+
- Description
264+
* - model_name
265+
- str
266+
- Predefined key (``citizenlab``) or HuggingFace model ID / local path
267+
* - threshold
268+
- float
269+
- Score above which text is considered toxic (default 0.5)
270+
271+
RegexScanner
272+
------------
273+
274+
.. automodule:: pytector.regex_scanner
275+
:members:
276+
:undoc-members:
277+
:show-inheritance:
278+
279+
Pure-stdlib rule-based scanner with customizable patterns.
280+
281+
.. code-block:: python
282+
283+
from pytector import RegexScanner
284+
285+
scanner = RegexScanner()
286+
has_match, matches = scanner.scan("Key: sk-live-abc123def456")
287+
print(scanner.redact("Email user@example.com"))
288+
289+
# Custom patterns only
290+
custom = RegexScanner(
291+
patterns={"ORDER_ID": r"ORD-\d{8}"},
292+
use_defaults=False,
293+
)
294+
295+
.. list-table:: RegexScanner Parameters
296+
:widths: 20 20 60
297+
:header-rows: 1
298+
299+
* - Parameter
300+
- Type
301+
- Description
302+
* - patterns
303+
- dict[str, str] | None
304+
- ``{NAME: regex}`` mapping merged with defaults (or used alone)
305+
* - use_defaults
306+
- bool
307+
- Whether to include built-in patterns (EMAIL, PHONE, SSN, CREDIT_CARD, IP_ADDRESS, API_KEY, JWT_TOKEN)

docs/examples.rst

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,107 @@ Sanitize first, then run the detector for defence in depth:
304304
else:
305305
print(f"Safe: {cleaned}")
306306
307+
PII Detection
308+
-------------
309+
310+
Scan and redact personally identifiable information:
311+
312+
.. code-block:: python
313+
314+
from pytector import PIIScanner
315+
316+
scanner = PIIScanner()
317+
318+
# Scan
319+
has_pii, entities = scanner.scan("Contact john@acme.com, SSN 123-45-6789")
320+
for ent in entities:
321+
print(f" [{ent['type']}] {ent['text']} (score={ent['score']:.2f})")
322+
323+
# Redact
324+
print(scanner.redact("Contact john@acme.com, SSN 123-45-6789"))
325+
# "Contact [REDACTED], SSN [REDACTED]"
326+
327+
# Report
328+
scanner.report("Contact john@acme.com, SSN 123-45-6789")
329+
330+
Filter to specific entity types:
331+
332+
.. code-block:: python
333+
334+
scanner = PIIScanner(entity_types=["EMAIL", "CREDIT_CARD"])
335+
has_pii, entities = scanner.scan("Email: a@b.com, SSN: 123-45-6789")
336+
# Only EMAIL entities returned
337+
338+
Custom threshold:
339+
340+
.. code-block:: python
341+
342+
scanner = PIIScanner(threshold=0.9)
343+
has_pii, entities = scanner.scan("john@acme.com")
344+
# Only high-confidence entities
345+
346+
Toxicity Detection
347+
------------------
348+
349+
Classify text as toxic or non-toxic:
350+
351+
.. code-block:: python
352+
353+
from pytector import ToxicityDetector
354+
355+
detector = ToxicityDetector()
356+
357+
is_toxic, score = detector.detect("You are terrible and worthless")
358+
print(f"Toxic: {is_toxic}, Score: {score:.2f}")
359+
360+
# Adjust threshold per call
361+
is_toxic, score = detector.detect("Mildly rude remark", threshold=0.8)
362+
363+
# Human-readable report
364+
detector.report("Have a wonderful day!")
365+
366+
Regex Scanner (Customizable)
367+
-----------------------------
368+
369+
Fast rule-based scanning with full pattern customization:
370+
371+
.. code-block:: python
372+
373+
from pytector import RegexScanner
374+
375+
# Default patterns: EMAIL, PHONE, SSN, CREDIT_CARD, IP_ADDRESS, API_KEY, JWT_TOKEN
376+
scanner = RegexScanner()
377+
378+
has_match, matches = scanner.scan("Key: sk-live-abc123def456, IP: 10.0.0.1")
379+
for m in matches:
380+
print(f" [{m['pattern_name']}] {m['match']}")
381+
382+
# Redact
383+
print(scanner.redact("Email me at user@example.com"))
384+
385+
Add and remove patterns at runtime:
386+
387+
.. code-block:: python
388+
389+
scanner = RegexScanner()
390+
391+
scanner.add_pattern("AWS_ACCESS_KEY", r"AKIA[0-9A-Z]{16}")
392+
scanner.add_pattern("INTERNAL_ID", r"INT-\d{6}")
393+
scanner.remove_pattern("JWT_TOKEN")
394+
395+
print(scanner.get_patterns())
396+
397+
Use only custom patterns (no defaults):
398+
399+
.. code-block:: python
400+
401+
custom = RegexScanner(
402+
patterns={"ORDER_ID": r"ORD-\d{8}", "ZIP": r"\b\d{5}(?:-\d{4})?\b"},
403+
use_defaults=False,
404+
)
405+
has_match, matches = custom.scan("Order ORD-20260330, zip 90210")
406+
print(custom.redact("Order ORD-20260330, zip 90210"))
407+
307408
Error Handling
308409
--------------
309410

docs/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Features
2121
* **Content Safety**: Support for Groq-hosted safeguard models for safety detection
2222
* **Keyword-Based Blocking**: Restrictive keyword filtering for immediate security control
2323
* **Input Sanitization**: Six-strategy pipeline to clean injection content from user input (encoding detection, unicode normalization, pattern removal, sentence scoring, fuzzy matching, keyword stripping) with zero additional dependencies
24+
* **PII Detection**: NER-based PII scanning using PasteProof PII Detector (ModernBERT, F1 0.97) covering 27 entity types — financial, credential, healthcare, GDPR, identity, contact, and address data
25+
* **Toxicity Detection**: Multilingual toxicity classification using citizenlab DistilBERT (F1 0.94, 10 languages)
26+
* **Regex Scanner**: Customizable rule-based pattern matching for PII and credentials (email, phone, SSN, credit card, IP, API keys, JWT) using pure Python stdlib
2427
* **Multiple Model Backends**: Support for Hugging Face Transformers and GGUF models
2528
* **Rapid Deployment**: Designed for quick integration into projects needing immediate security layers
2629
* **Configurable**: Customizable detection parameters, thresholds, and security policies

0 commit comments

Comments
 (0)