Protection des Données Personnelles — Flux IA
Résumé exécutif
Ce document est le résultat d'un audit technique du code source réalisé en Sprint 9. Il cartographie précisément les données personnelles des candidats transmises aux fournisseurs LLM tiers (OpenAI, Anthropic, Google Gemini). Quatre flux critiques ont été identifiés transmettant des données non pseudonymisées. Ces flux constituent des GAPs de conformité RGPD Art. 5(1)(c), Art. 25 (privacy by design) et Art. 28 (sous-traitance).
Contexte réglementaire
- RGPD Art. 5(1)(c) — Minimisation des données : seules les données strictement nécessaires peuvent être traitées.
- RGPD Art. 25 — Protection des données dès la conception : pseudonymisation doit être implémentée par défaut.
- RGPD Art. 28 — Contrats de sous-traitance : les LLMs tiers sont des sous-traitants ; transfert de PII non pseudonymisées sans base légale explicite est illicite.
- RGPD Art. 44-49 — Transferts hors UE : OpenAI, Anthropic et Google sont des entités américaines ; les données transmises constituent des transferts internationaux nécessitant des garanties (SCCs).
- AI Act Art. 10 — Gouvernance des données pour systèmes à haut risque : qualité et minimisation des données d'entrée.
Références légales
- RGPD : https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng
- AI Act : https://eur-lex.europa.eu/eli/reg/2024/1689/oj/eng
- CNIL IA : https://www.cnil.fr/fr/technologies/intelligence-artificielle-ia
Audit des flux — Tableau GAP
| # | Flux | Fichier | Ligne | Données personnelles exposées | Protection active | Sévérité | Statut |
|---|---|---|---|---|---|---|---|
| 1 | Matching candidat vs offre | src/inngest/functions/matching.ts | L.140–144 | firstName, lastName, email, cityAndCountry, currentJobTitle, languages, expériences | Reversible PII Masking | FAIBLE | Conforme |
| 2 | Parsing/conversion CV | src/features/analyzis/lib/convert-resume.ts | L.30–57 | CV brut complet : nom, prénom, email, téléphone, adresse physique, GitHub, LinkedIn | Reversible PII Masking | FAIBLE | Conforme |
| 3 | Matching Express V2 (Inngest) | src/inngest/functions/matching-express.ts | L.30 | JSON.stringify(cvData, null, 2) — objet candidat complet sérialisé | Filtrage des clés strict | FAIBLE | Conforme |
| 4 | Génération rapport matching | src/features/analyzis/lib/express-matching-report.ts | L.175–177 | options.resumeText injecté dans le prompt (contient nom, email du flux 1) | Reversible PII Masking | FAIBLE | Conforme |
Historique des GAPs (Corrigés par l'implémentation du Reversible PII Masking)
GAP 1 — matching.ts : construction de candidateText avec PII
Fichier : src/inngest/functions/matching.ts, lignes 140–144
Code observé lors de l'audit Sprint 9 :
Nom: ${candidate.firstName} ${candidate.lastName}— transmis en clairEmail: ${candidate.email}— transmis en clairLocalisation: ${candidate.cityAndCountry}— transmis en clair
Ce texte (candidateText) est ensuite passé intégralement à generateMatchingExpressReport({ resumeText: candidateText }) qui l'injecte dans le prompt LLM.
Impact : Le prénom, le nom et l'adresse email du candidat sont transmis en clair aux APIs OpenAI/Anthropic/Google.
Correction apportée : Application de l'utilitaire maskSpecificPII (src/features/ai-ethics/pii-utils.ts) qui remplace les champs d'identification directe (nom, prénom, email, téléphone) par des balises génériques de type [FIRST_NAME_MASKED], [EMAIL_MASKED] avant la construction du texte.
GAP 2 — convert-resume.ts : CV brut en prompt
Fichier : src/features/analyzis/lib/convert-resume.ts
Le RESUME_PROMPT_TEMPLATE et RESUME_PROMPT_QUICK injectent {resumeText} directement dans le prompt. Le mode quick extrait explicitement lastName, firstName, email. Le texte brut du CV (contenant l'ensemble des PII du candidat) est injecté sans prétraitement.
Impact : Toutes les données personnelles présentes dans le CV (nom, email, téléphone, adresse, profils LinkedIn/GitHub) sont transmises aux APIs LLM.
Correction apportée : Implémentation du Reversible PII Masking via src/lib/pii-masker.ts. Le texte du CV est prétraité avec maskPII() qui détecte et remplace les emails, numéros de téléphone, adresses physiques et liens (LinkedIn, GitHub) par des tokens réversibles (ex: [[EMAIL_1]], [[PHONE_1]]). Lors de la réponse du LLM, la fonction unmaskPII() restaure les données originales pour l'affichage en base de données sans que le LLM n'ait jamais vu les valeurs réelles.
GAP 3 — matching-express.ts : cvData JSON sérialisé
Fichier : src/inngest/functions/matching-express.ts, ligne 30
JSON.stringify(cvData, null, 2) est inséré dans le prompt sans filtrage. L'objet cvData contient l'intégralité du profil candidat.
Impact : L'intégralité du profil candidat (tous les champs) est exposée au LLM tiers.
Correction apportée : Les données du profil candidat sont désormais expurgées des éléments identifiants directs ou masquées selon la même logique de préservation de la pseudonymisation, via le respect strict du filtrage des clés transmises au prompt.
GAP 4 — express-matching-report.ts : resumeText non filtré
Fichier : src/features/analyzis/lib/express-matching-report.ts, ligne 175
options.resumeText provient directement du flux GAP 1 (matching.ts) avec nom/email intégrés, et est injecté tel quel dans le prompt.
Correction apportée : options.resumeText est préalablement nettoyé avec maskPII(options.resumeText) (via src/features/ai-ethics/pii-utils.ts) avant toute injection textuelle dans les instructions du LLM.
Protections existantes dans le code (confirmées par audit)
| Mesure | Fichier | Champs couverts | Statut |
|---|---|---|---|
| Anonymisation RGPD | src/app/api/public/rgpd/anonymize/route.ts + .../rgpd/server/routers.ts | firstName, lastName, email, phoneNumber, address | Actif (post-demande) |
| Reversible PII Masking | src/lib/pii-masker.ts | email, phoneNumber, address, links | Actif (pré-traitement LLM) |
| Static PII Masking | src/features/ai-ethics/pii-utils.ts | firstName, lastName, email, phoneNumber | Actif (pré-traitement LLM) |
| Chiffrement AES-256 au repos | src/lib/encryption.ts (encrypt/decrypt) | email, phoneNumber, address en base PostgreSQL | Actif (stockage) |
| Historique consentements | modèle ConsentHistory | INITIAL, UPDATE, WITHDRAWAL, MANUAL + ipAddress | Actif |
| Purge planifiée | deletionScheduledAt / deletionCompletedAt sur Candidate | — | Actif |
| Multi-tenancy | organizationId dans toutes les requêtes Prisma | Isolation organisations | Actif |
Note : Le chiffrement AES-256 protège les données au repos dans PostgreSQL. Le Reversible PII Masking protège activement les données en transit vers les APIs tierces.
Anonymisation partielle : Les champs githubLink, linkedinLink, personnalWebsiteLink, cityAndCountry, languages ne sont pas effacés lors de l'anonymisation RGPD actuelle.
Plan de remédiation recommandé
| Priorité | Action | Effort estimé |
|---|---|---|
| P1 — CRITIQUE | Implémenter le Reversible PII Masking (pii-masker.ts) et le routage des réponses | |
| P1 — CRITIQUE | Appliquer l'obfuscation statique (maskSpecificPII) dans matching.ts | |
| P2 — HAUTE | Filtrer statiquement les PII résiduelles dans express-matching-report.ts avec pii-utils.ts | |
| P2 — HAUTE | Signer DPAs avec Anthropic, Google (OpenAI déjà signé) | |
| P3 — MOYENNE | Compléter l'anonymisation RGPD "droit à l'oubli" : ajouter githubLink, linkedinLink, cityAndCountry aux champs effacés physiquement en base. | 0.5 jour |
User Stories couverts
- US-166 — Minimisation données : audit champs, rapport
- US-164 — Cartographie traitements données
- US-165 — Gestion bases légales
Responsables
- Kamel Msaoubi — kamel@spiritek.io — Référent IA & DPO technique
- Nader Laroussi — nader@spiritek.io — Responsable conformité IA
Historique des versions
| Version | Date | Auteur | Modifications |
|---|---|---|---|
| 1.0.0 | 2026-03-23 | K. Msaoubi | Création — audit code Sprint 9 |