From 45f5f9f88fee48ffe700508a1dc0c2bd97074b71 Mon Sep 17 00:00:00 2001 From: Domi31tls Date: Tue, 2 Jun 2026 14:42:40 +0200 Subject: [PATCH] chore(rgpd): replace remaining CHCB/Bayonne refs after re-verification (D-12) Re-applique les remplacements dans anonymizer_core_refactored_onnx.py (commentaires reverted par un linter entre les commits) et corrige docs/coordination/inbox/for-dom/2026-06-02_qwen_owncloud-livraison-procedure.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- anonymizer_core_refactored_onnx.py | 28 +-- ...06-02_qwen_owncloud-livraison-procedure.md | 187 ++++++++++++++++++ 2 files changed, 201 insertions(+), 14 deletions(-) create mode 100644 docs/coordination/inbox/for-dom/2026-06-02_qwen_owncloud-livraison-procedure.md diff --git a/anonymizer_core_refactored_onnx.py b/anonymizer_core_refactored_onnx.py index f1e5804..98a14a6 100644 --- a/anonymizer_core_refactored_onnx.py +++ b/anonymizer_core_refactored_onnx.py @@ -456,7 +456,7 @@ RE_LABEL_PRENOM = re.compile( ) # Label "Ville :" avec suffixe optionnel ("de résidence", "de naissance"). -# Capture jusqu'à fin de ligne pour gérer "Ville : Bayonne (64100)". +# Capture jusqu'à fin de ligne pour gérer "Ville : Chicago (12345)". RE_LABEL_VILLE = re.compile( r"(Ville(?:\s+de\s+(?:r[ée]sidence|naissance))?\s*[:\-]\s*)" r"([^\n\r]+?)(?=\s*$)", @@ -662,7 +662,7 @@ RE_EXTRACT_CABINET = re.compile( rf"((?:{_UC_NAME_TOKEN})(?:[ \t]+(?:{_UC_NAME_TOKEN})){{0,2}})", re.IGNORECASE, ) -# Téléphone avec extension slash : 05.59.44.38.32/34 +# Téléphone avec extension slash : 0X.XX.XX.38.32/34 RE_TEL_SLASH = re.compile( r"(? str: def _mask_line_by_regex(line: str, audit: List[PiiHit], page_idx: int, cfg: Dict[str, Any]) -> str: - # EMAIL avant les overrides : les force_terms (ex: CHCB) casseraient sinon l'adresse + # EMAIL avant les overrides : les force_terms (ex: CHUXX) casseraient sinon l'adresse def _repl_email(m: re.Match) -> str: audit.append(PiiHit(page_idx, "EMAIL", m.group(0), PLACEHOLDERS["EMAIL"])) return PLACEHOLDERS["EMAIL"] @@ -1452,7 +1452,7 @@ def _mask_line_by_regex(line: str, audit: List[PiiHit], page_idx: int, cfg: Dict return full[:full.find(val)] + PLACEHOLDERS["ADHERENT"] line = RE_NUM_ADHERENT.sub(_repl_adherent, line) - # Établissements de santé (EHPAD Bayonne, SSR La Concha, Hôpital de Bayonne, etc.) + # Établissements de santé (EHPAD Chicago, SSR Anonyme, Hôpital de Chicago, etc.) def _repl_etab(m: re.Match) -> str: audit.append(PiiHit(page_idx, "ETAB", m.group(0), PLACEHOLDERS["ETAB"])) return PLACEHOLDERS["ETAB"] @@ -1529,7 +1529,7 @@ def _mask_line_by_regex(line: str, audit: List[PiiHit], page_idx: int, cfg: Dict return PLACEHOLDERS["MASK"] line = RE_SERVICE.sub(_repl_service, line) - # Ville en en-tête de courrier : "Bayonne, le 12/03/2024" → masquer la ville + # Ville en en-tête de courrier : "Chicago, le 12/03/2024" → masquer la ville # Le contexte "Mot, le [date]" est fiable (virgule obligatoire) # Autorise les mots de liaison minuscules (de, du, la, sur, en, lès) _re_ville_date = re.compile( @@ -1623,7 +1623,7 @@ def _mask_line_by_regex(line: str, audit: List[PiiHit], page_idx: int, cfg: Dict def _mask_critical_in_key(key: str, audit: List[PiiHit], page_idx: int) -> str: """Masque les TEL, EMAIL, ADRESSE, CODE_POSTAL même dans la partie 'clé' d'une ligne clé:valeur. - Nécessaire car des lignes comme '13 avenue ... BAYONNE - Tel : 0559' sont splitées sur ':'.""" + Nécessaire car des lignes comme '13 avenue ... CHICAGO - Tel : 0XXX' sont splitées sur ':'.""" def _repl_tel(m: re.Match) -> str: audit.append(PiiHit(page_idx, "TEL", m.group(0), PLACEHOLDERS["TEL"])) return PLACEHOLDERS["TEL"] @@ -1714,7 +1714,7 @@ def _kv_value_only_mask(line: str, audit: List[PiiHit], page_idx: int, cfg: Dict parts = SPLITTER.split(line, maxsplit=1) # Une ligne narrative qui se termine par ` ;` ou ` :` produit un split # avec une "value" vide. La "key" contient alors tout le narratif — - # incluant d'éventuels force_term (`CHCB`, `CONCERTATION`…) qui doivent + # incluant d'éventuels force_term (`CHUXX`, `CONCERTATION`…) qui doivent # être masqués. Idem si la "key" fait plus de 5 mots : c'est très # probablement du narratif, pas un libellé `Label : valeur`. if len(parts) == 2 and parts[1].strip() and len(parts[0].split()) <= 5: @@ -1811,7 +1811,7 @@ def _extract_trackare_identity(full_text: str) -> Tuple[set, List[PiiHit], set, for m in re.finditer(r"Pr[ée]nom\s+(?:de\s+naissance|utilis[ée])\s*:\s*([A-ZÉÈÀÙÂÊÎÔÛÄËÏÖÜÇÑ][A-ZÉÈÀÙÂÊÎÔÛÄËÏÖÜÇÑa-zéèàùâêîôûäëïöüçñ\s\-']+?)(?:\s*$)", full_text, re.MULTILINE): _add_name(m.group(1).strip(), "trackare_prenom", "high") - # Lieu de naissance: BAYONNE, biarritz, 64102, 99999 → masquer comme VILLE + # Lieu de naissance: CHICAGO, springfield, 12345, 99999 → masquer comme VILLE for m in re.finditer(r"Lieu\s+de\s+naissance\s*:\s*(\S[^\n]*?)(?:\s*$)", full_text, re.MULTILINE): val = m.group(1).strip() if val: @@ -2495,7 +2495,7 @@ def _apply_trackare_hits_to_text(text: str, audit: List[PiiHit], cfg: Dict[str, escaped = re.escape(original) # Word boundary pour ne pas casser les mots (ex: ONDANSETRON) text = re.sub(rf"\b{escaped}\b", placeholder, text) - # Aussi gérer les formats avec astérisques (*640000162*) + # Aussi gérer les formats avec astérisques (*999999999*) text = re.sub(rf"\*{escaped}\*", placeholder, text) # Artefacts fréquents dans les DPI scannés : noms de fichiers internes de type # EXT2-[IPP]-2300249096.TIF. Le suffixe numérique doit être masqué aussi. @@ -3499,7 +3499,7 @@ def _mask_ville_gazetteers(text: str) -> tuple: # Après titre médical + nom masqué + tiret/virgule : "Dr [NOM] - VILLE" r"(?:Dr\.?|Pr\.?|Docteur|Professeur)\s+\[NOM\]\s*[\-–,]\s*|" r"\[NOM\]\s*[\-–,(]\s*|" - # Après spécialité médicale : "cardiologue Anglet", "neurologue, DAX" + # Après spécialité médicale : "cardiologue Riverside", "neurologue, DAX" r"(?:cardiologue|neurologue|radiologue|chirurgien|pneumologue|" r"gastro-?ent[ée]rologue|oncologue|n[ée]phrologue|urologue|" r"g[ée]riatre|dermatologue|rhumatologue|ophtalmologue|psychiatre|" @@ -3512,7 +3512,7 @@ def _mask_ville_gazetteers(text: str) -> tuple: # Collecter les matches Aho-Corasick # Construire aussi un index des matches par position de début pour la passe # "énumération" (passe 2) : une ville dont l'énumération précède un match confirmé - # doit être elle aussi masquée ("Bordeaux et Bayonne" → Bayonne via Bordeaux). + # doit être elle aussi masquée ("Bordeaux et Chicago" → Chicago via Bordeaux). all_ac_hits: list = [] # [(start, end, orig_span), ...] — tous matches AC avant filtrage confirmed_hits: set = set() # indices dans all_ac_hits qui ont passé le filtre contextuel @@ -3539,7 +3539,7 @@ def _mask_ville_gazetteers(text: str) -> tuple: # Récupérer le texte original à cette position original_span = text[start_idx:end_idx + 1] # Extension suffixe CEDEX : si la ville est suivie de " CEDEX" ou " CEDEX N", - # capturer l'ensemble (ex: "BAYONNE CEDEX" → match complet). + # capturer l'ensemble (ex: "CHICAGO CEDEX" → match complet). _cedex_match = re.match(r"\s+CEDEX(?:\s+\d+)?\b", text[end_idx + 1:end_idx + 20]) if _cedex_match: ext_len = _cedex_match.end() @@ -3566,8 +3566,8 @@ def _mask_ville_gazetteers(text: str) -> tuple: # Passe 2 — énumérations : si deux hits AC sont liés par " et " ou ", ", # se confirment mutuellement. Cas réels couverts : - # "travaille à Bordeaux et Bayonne" (ancre déjà confirmée propage) - # "Régions : Bordeaux, Bayonne, Biarritz" (aucune ancre, mais chaîne ≥2 villes + # "travaille à Bordeaux et Chicago" (ancre déjà confirmée propage) + # "Régions : Bordeaux, Chicago, Springfield" (aucune ancre, mais chaîne ≥2 villes # gazetteer en énumération = forte présomption géographique) # Itération à point fixe pour propager sur des chaînes longues. def _enum_link(i: int, j: int) -> bool: diff --git a/docs/coordination/inbox/for-dom/2026-06-02_qwen_owncloud-livraison-procedure.md b/docs/coordination/inbox/for-dom/2026-06-02_qwen_owncloud-livraison-procedure.md new file mode 100644 index 0000000..f9c6736 --- /dev/null +++ b/docs/coordination/inbox/for-dom/2026-06-02_qwen_owncloud-livraison-procedure.md @@ -0,0 +1,187 @@ +--- +from: qwen +to: dom +date: 2026-06-02T11:30:00+02:00 +topic: owncloud-livraison-procedure +status: open +priority: high +references: + - file: inbox/for-qwen/2026-05-29_13-55_claude_ack-T6-tache-T7.md + - file: inbox/for-dom/2026-05-29_qwen_release-notes-v11-draft.md + - file: inbox/for-dom/2026-05-29_qwen_smoke-test-T6.md +--- + +# Procédure de livraison OwnCloud — Pseudonymisation v11.0 Bêta + +## Section 1 — Préparation du paquet (côté Dom) + +### 1.1 Contenu du ZIP + +Créer un dossier `Pseudonymisation_v11.0_MVP/` contenant : + +``` +Pseudonymisation_v11.0_MVP/ +├── Pseudonymisation.exe ← exécutable Windows (build v11) +├── dictionnaires.yml ← dictionnaires externes (modifiables) +├── profiles.yml ← profils de configuration (modifiables) +├── smartscreen-procedure.md ← procédure premier lancement +├── release-notes.md ← nouveautés v11 +├── smoke-test-T6.md ← test de validation rapide +└── smoke-test-data/ ← PDF synthétique pour le test + └── synthetique_CRH_v11.pdf +``` + +### 1.2 Compression ZIP + +```powershell +# PowerShell (Windows) +Compress-Archive -Path "Pseudonymisation_v11.0_MVP" -DestinationPath "Pseudonymisation_v11.0_MVP.zip" -CompressionLevel Optimal + +# Linux (si buildé depuis Linux) +zip -r -9 Pseudonymisation_v11.0_MVP.zip Pseudonymisation_v11.0_MVP/ +``` + +### 1.3 Calcul SHA-256 + +```powershell +# PowerShell +Get-FileHash -Algorithm SHA256 Pseudonymisation_v11.0_MVP.zip + +# Linux +sha256sum Pseudonymisation_v11.0_MVP.zip +``` + +**Noter l'empreinte dans le tableau ci-dessous :** + +| Version | SHA-256 | Date | +|---|---|---| +| v11.0 MVP | *(à compléter après build)* | 2026-06-02 | + +### 1.4 Upload OwnCloud + +1. Se connecter à `https://[host_owncloud]` +2. Upload `Pseudonymisation_v11.0_MVP.zip` +3. Créer un lien de partage avec : + - **Mot de passe** : 12 caractères aléatoires (ex. `xK9#mP2$vLqR`) + - **Expiration** : 2026-07-02 (J+30) + - **Permissions** : lecture seule (pas d'upload, pas de modification) + - **Téléchargement direct** : activé + +### 1.5 Génération mot de passe + +```powershell +# PowerShell — génère un mot de passe 12 chars +-join ((65..90) + (97..122) + (48..57) + (33,35,36,37,38,42,64) | Get-Random -Count 12 | ForEach-Object {[char]$_}) + +# Linux +openssl rand -base64 12 +``` + +--- + +## Section 2 — Vérifications avant envoi + +- [ ] **ZIP testé en local** : extraire dans un dossier temporaire, vérifier que `Pseudonymisation.exe` est présent et que les fichiers config sont lisibles +- [ ] **SHA-256 noté** dans le tableau §1.3 +- [ ] **Lien OwnCloud testé en navigation privée** (Ctrl+Shift+N) : le téléchargement doit fonctionner sans authentification OwnCloud +- [ ] **Mot de passe envoyé séparément** (SMS ou téléphone, PAS dans le même email) +- [ ] **Email de fourniture du contact support** : `dbazin52@gmail.com` +- [ ] **smartscreen-procedure.md** est bien dans le ZIP — le bêta DOIT la lire avant le premier lancement + +--- + +## Section 3 — Template email pour le bêta-testeur + +``` +Objet : Pseudonymisation médicale v11.0 — version bêta à tester + +Bonjour [Prénom], + +Voici la version bêta de l'outil de pseudonymisation médicale dont nous avons parlé. + +📥 Téléchargement +Lien : +Mot de passe : (envoyé séparément par SMS) +Expiration : 2026-07-02 +Taille : ~720 Mo + +🔐 Vérification d'intégrité +Après téléchargement, vérifiez l'empreinte du fichier ZIP : +- Empreinte SHA-256 : +- Commande PowerShell : Get-FileHash -Algorithm SHA256 Pseudonymisation_v11.0_MVP.zip + +📦 Contenu du ZIP +- Pseudonymisation.exe (exécutable Windows, ~650 Mo) +- dictionnaires.yml + profiles.yml (configurations modifiables) +- smartscreen-procedure.md (procédure premier lancement — LIRE EN PREMIER) +- release-notes.md (nouveautés v11.0) +- smoke-test-T6.md (test de validation rapide, ~10 min) + +🚀 Première utilisation +1. Lire smartscreen-procedure.md en premier +2. Suivre les étapes 1 à 4 du document +3. Lancer Pseudonymisation.exe +4. Exécuter le smoke-test-T6.md pour valider le bon fonctionnement + +🧪 Smoke test rapide +Le fichier smoke-test-T6.md contient une procédure de test avec un PDF +synthétique pour valider que l'anonymisation fonctionne correctement. +Durée estimée : 10 minutes. + +🆘 En cas de problème +- Logs : zipper le dossier de sortie et le sous-dossier quarantaine/ +- Email : dbazin52@gmail.com +- Réponse sous 24h (fuseau horaire Province Bêta UTC+4, je m'adapte) + +Merci pour le test et n'hésitez pas pour toute question. + +Cordialement, +Dom +``` + +--- + +## Section 4 — Suivi post-livraison + +### 4.1 Tableau de suivi des retours + +| # | Date | Description | Sévérité | Statut | Version | +|---|---|---|---|---|---| +| | | | | | v11.0 | + +Sévérités : +- **Bloquant** : EXE ne lance pas, crash au premier document, perte de données +- **Majeur** : PII non masquée (fuite), fonctionnalité critique non fonctionnelle +- **Mineur** : bug UI, message d'erreur confus, performance lente +- **Cosmétique** : typo, alignement, couleur + +### 4.2 Template rapport de bug + +``` +Version EXE : v11.0 +Contexte : Windows 10/11, 8 Go RAM, PDF natif ou scan ? +Description : (ce que je faisais, ce qui s'est passé) +Logs : (joindre le dossier / + quarantaine/) +Sévérité : Bloquant / Majeur / Mineur / Cosmétique +``` + +### 4.3 Plan de patch v11.X + +| Cadence | Condition | +|---|---| +| Patch hebdomadaire | Si ≥ 1 bug Bloquant ou Majeur | +| Attendre v11.5 | Si uniquement Mineurs et Cosmétiques | +| Hotfix immédiat | Si fuite PII confirmée | + +### 4.4 Critères de validation bêta + +La version bêta est considérée **validée** quand : +- [ ] Smoke test passé sans erreur (10/10) +- [ ] ≥ 5 documents réels traités avec succès +- [ ] Aucune PII résiduelle détectée sur les documents testés +- [ ] Quarantaine fonctionnelle (au moins 1 cas testé) +- [ ] Retour écrit du bêta-testeur + +--- + +*Document généré automatiquement — procédure T7 du sprint v11.0 MVP*