feat(server): assainissement PII des logs clients à la réception
sanitize_log_entries (réutilise anonymize_text, mapping partagé = tokens cohérents), branché dans POST /api/v1/agents/logs avant le store : message + logger tokenisés, ts/level préservés. 7 tests TDD. Rempart PII central du push-log (couvre les postes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,7 @@ from fastapi import BackgroundTasks, Depends, FastAPI, File, HTTPException, Requ
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .pii_sanitizer import sanitize_event
|
||||
from .pii_sanitizer import sanitize_event, sanitize_log_entries
|
||||
from .replay_failure_logger import log_replay_failure
|
||||
from .replay_verifier import ReplayVerifier, VerificationResult
|
||||
from .replay_learner import ReplayLearner
|
||||
@@ -7263,7 +7263,10 @@ async def agents_logs(request: AgentLogsRequest):
|
||||
# Bloque les postes révoqués/désinstallés + met à jour last_seen_at.
|
||||
_guard_agent_registry_access(machine_id, endpoint="agents/logs")
|
||||
|
||||
received = agent_logs_store.append(machine_id, request.logs)
|
||||
# Assainissement PII côté serveur avant persistance (couche 1 regex, sans NER).
|
||||
# Un mapping partagé sur le batch garantit la cohérence des tokens ([NOM_1]…).
|
||||
safe_logs = sanitize_log_entries(request.logs)
|
||||
received = agent_logs_store.append(machine_id, safe_logs)
|
||||
return {"status": "ok", "received": received, "machine_id": machine_id}
|
||||
|
||||
|
||||
|
||||
@@ -203,6 +203,40 @@ def sanitize_event(event: Dict, *, mapping: Optional[Dict] = None) -> Dict:
|
||||
return ev
|
||||
|
||||
|
||||
def sanitize_log_entries(
|
||||
entries: List[Dict], *, mapping: Optional[Dict] = None
|
||||
) -> List[Dict]:
|
||||
"""Assainit un batch de log-entries reçues d'un client Léa avant persistance.
|
||||
|
||||
Pour chaque entrée, renvoie une **copie** où les champs texte porteurs de PII
|
||||
sont passés par `anonymize_text` :
|
||||
- `message` (str) : assaini par `anonymize_text`.
|
||||
- `logger` (str) : assaini de la même façon (peut porter un chemin patient).
|
||||
- `ts` et `level` : préservés à l'identique, jamais touchés.
|
||||
|
||||
Un `mapping` partagé est utilisé pour **toutes** les entrées du batch afin de
|
||||
garantir la cohérence des tokens (même PII → même token). Si `mapping` est
|
||||
None, un mapping local est créé et partagé entre toutes les entrées du batch.
|
||||
|
||||
Tolère les valeurs absentes, None ou non-str sans lever d'exception.
|
||||
N'utilise que `anonymize_text` — aucune regex supplémentaire.
|
||||
"""
|
||||
if not entries:
|
||||
return []
|
||||
if mapping is None:
|
||||
mapping = {}
|
||||
|
||||
result: List[Dict] = []
|
||||
for entry in entries:
|
||||
item = copy.copy(entry) # copie superficielle suffit (valeurs scalaires)
|
||||
for field in ("message", "logger"):
|
||||
v = item.get(field)
|
||||
if isinstance(v, str):
|
||||
item[field] = anonymize_text(v, mapping=mapping)[0]
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
# Clés d'un workflow core portant du texte potentiellement PII : cible OCR
|
||||
# (`by_text`), noms d'écrans/labels dérivés des titres. Le contenu saisi est
|
||||
# déjà neutralisé à la source (sanitize_event → [SAISIE]).
|
||||
|
||||
Reference in New Issue
Block a user