fix(server): durcissement sanitizer PII suite revue adversariale Qwen
- FN-1/2/3 : ajout RE_PRENOM_NOM (« Prénom NOM » inversé sans parens/crochets, ex. « Alix DATTIN ») ; 2e mot tout-majuscules -> 0 FP sur « Mozilla Firefox ». - FN-4 (majeur, 228 events) : sanitize_event scanne désormais les titres RÉCURSIVEMENT (vision_info.window_capture.window_title et tout titre imbriqué), au lieu de 3 clés top-level hardcodées. 2 correctifs issus de la revue croisée Qwen. 11 tests verts, 0 FP. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,11 +38,15 @@ RE_NOM_NAISSANCE = re.compile(
|
||||
RE_NOM_BRACKET = re.compile(
|
||||
rf"\[((?:[{_MAJ}][\w{_MIN}'\-]*\s+){{1,3}}[{_MAJ}][\w{_MIN}'\-]*)\]"
|
||||
)
|
||||
# « Prénom NOM » inversé, sans parenthèses ni crochets (ex. « Alix DATTIN »).
|
||||
# 2e mot tout en MAJUSCULES → faible risque de FP (« Mozilla Firefox » ne matche pas).
|
||||
RE_PRENOM_NOM = re.compile(rf"\b[{_MAJ}][{_MIN}]+\s+[{_MAJ}][{_MAJ}\-']+\b")
|
||||
|
||||
# Ordre = priorité ; group = portion à remplacer (0 = match entier).
|
||||
_DETECTORS: List[Tuple[re.Pattern, str, int]] = [
|
||||
(RE_NOM_NAISSANCE, "NOM", 0),
|
||||
(RE_NOM_BRACKET, "NOM", 0),
|
||||
(RE_PRENOM_NOM, "NOM", 0),
|
||||
(RE_EMAIL, "EMAIL", 0),
|
||||
(RE_NIR, "NIR", 0),
|
||||
(RE_IPP, "IPP", 1),
|
||||
@@ -134,11 +138,26 @@ def anonymize_text(
|
||||
return out, entities
|
||||
|
||||
|
||||
# Conteneurs de titre de fenêtre dans les events (window_focus_change, clic, saisie).
|
||||
_TITLE_CONTAINERS = ("window", "to", "from")
|
||||
# Clés portant un titre de fenêtre, où qu'elles soient imbriquées dans l'event
|
||||
# (top-level `active_window_title`, `window/to/from.title`, et surtout
|
||||
# `vision_info.window_capture.window_title` — blind spot signalé par Qwen).
|
||||
_TITLE_KEYS = ("title", "window_title", "active_window_title")
|
||||
_PLACEHOLDER_SAISIE = "[SAISIE]"
|
||||
|
||||
|
||||
def _walk_titles(obj, mapping: Dict) -> None:
|
||||
"""Parcourt récursivement l'event et assainit toute valeur de titre de fenêtre."""
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if k in _TITLE_KEYS and isinstance(v, str):
|
||||
obj[k] = anonymize_text(v, mapping=mapping)[0]
|
||||
else:
|
||||
_walk_titles(v, mapping)
|
||||
elif isinstance(obj, list):
|
||||
for item in obj:
|
||||
_walk_titles(item, mapping)
|
||||
|
||||
|
||||
def sanitize_event(event: Dict, *, mapping: Optional[Dict] = None) -> Dict:
|
||||
"""Assainit un event capturé avant persistance (copie, ne mute pas l'original).
|
||||
|
||||
@@ -159,16 +178,8 @@ def sanitize_event(event: Dict, *, mapping: Optional[Dict] = None) -> Dict:
|
||||
if ev.get(k) not in (None, ""):
|
||||
ev[k] = _PLACEHOLDER_SAISIE
|
||||
|
||||
# titre direct (heartbeat)
|
||||
if isinstance(ev.get("active_window_title"), str):
|
||||
ev["active_window_title"] = anonymize_text(
|
||||
ev["active_window_title"], mapping=mapping
|
||||
)[0]
|
||||
|
||||
# titres imbriqués (window / to / from)
|
||||
for key in _TITLE_CONTAINERS:
|
||||
sub = ev.get(key)
|
||||
if isinstance(sub, dict) and isinstance(sub.get("title"), str):
|
||||
sub["title"] = anonymize_text(sub["title"], mapping=mapping)[0]
|
||||
# tous les titres de fenêtre, où qu'ils soient imbriqués
|
||||
# (active_window_title, window/to/from.title, vision_info.window_capture.window_title…)
|
||||
_walk_titles(ev, mapping)
|
||||
|
||||
return ev
|
||||
|
||||
@@ -136,3 +136,39 @@ def test_sanitize_action_result_inchange():
|
||||
|
||||
ev = {"type": "action_result", "base_shot_id": "shot_0003", "image": "x.png"}
|
||||
assert sanitize_event(ev) == ev
|
||||
|
||||
|
||||
def test_prenom_nom_inverse():
|
||||
"""FN-1/2/3 (Qwen) : « Prénom NOM » inversé (sans parens/crochets)."""
|
||||
from agent_v0.server_v1.pii_sanitizer import anonymize_text
|
||||
|
||||
m: dict = {}
|
||||
for s, leak in [("Alix DATTIN - Mozilla Firefox", "DATTIN"),
|
||||
("Agathe RONDOT - PACS CIM ARES", "RONDOT"),
|
||||
("Marie FLANDINETTE - Mozilla Firefox", "FLANDINETTE")]:
|
||||
out, _ = anonymize_text(s, mapping=m)
|
||||
assert leak not in out, out
|
||||
assert "[NOM_" in out
|
||||
# pas de faux positif sur les logiciels (2e mot non capitalisé tout en majuscules)
|
||||
out, ents = anonymize_text("Mozilla Firefox - Expert Sante - Consultation")
|
||||
assert out == "Mozilla Firefox - Expert Sante - Consultation"
|
||||
assert ents == []
|
||||
|
||||
|
||||
def test_sanitize_event_titre_imbrique_vision_info():
|
||||
"""FN-4 (Qwen) : titre PII imbriqué dans vision_info.window_capture (228 events)."""
|
||||
from agent_v0.server_v1.pii_sanitizer import sanitize_event
|
||||
|
||||
titre = "VIOLA (VIOLA) Liliane 90 ans - IPP: 168246 - Firefox"
|
||||
ev = {
|
||||
"type": "mouse_click",
|
||||
"window": {"title": titre, "app_name": "firefox.exe"},
|
||||
"vision_info": {"window_capture": {"window_title": titre, "app_name": "firefox.exe"}},
|
||||
}
|
||||
out = sanitize_event(ev)
|
||||
|
||||
wc = out["vision_info"]["window_capture"]["window_title"]
|
||||
assert "168246" not in wc and "VIOLA" not in wc, wc
|
||||
assert "[IPP_1]" in wc
|
||||
# cohérence : même titre dans window et vision_info -> même token
|
||||
assert out["window"]["title"] == wc
|
||||
|
||||
Reference in New Issue
Block a user