#!/usr/bin/env python3 """Génère la fiche technique DSI / RSSI / DPO en DOCX.""" from docx import Document from docx.shared import Pt, Cm, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.table import WD_TABLE_ALIGNMENT from docx.oxml.ns import qn import os # ── Helpers ────────────────────────────────────────────────────────── def set_cell_shading(cell, color_hex: str): shading = cell._element.get_or_add_tcPr() shading_elem = shading.makeelement(qn('w:shd'), { qn('w:val'): 'clear', qn('w:color'): 'auto', qn('w:fill'): color_hex, }) shading.append(shading_elem) def add_heading_styled(doc, text, level=1, color=RGBColor(0x1A, 0x56, 0x8E)): h = doc.add_heading(text, level=level) for run in h.runs: run.font.color.rgb = color return h def add_bullet(doc, text, bold_prefix=None): p = doc.add_paragraph(style='List Bullet') if bold_prefix: run = p.add_run(bold_prefix) run.bold = True p.add_run(text) else: p.add_run(text) return p def add_para(doc, text, bold=False, italic=False, space_after=Pt(6)): p = doc.add_paragraph() run = p.add_run(text) run.bold = bold run.italic = italic p.paragraph_format.space_after = space_after return p def add_table_row(table, cells_data, header=False, header_color='1A568E'): row = table.add_row() for i, text in enumerate(cells_data): cell = row.cells[i] cell.text = '' p = cell.paragraphs[0] run = p.add_run(text) run.font.size = Pt(9.5) if header: run.bold = True run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) set_cell_shading(cell, header_color) p.paragraph_format.space_before = Pt(3) p.paragraph_format.space_after = Pt(3) def set_table_style(table): table.alignment = WD_TABLE_ALIGNMENT.CENTER for row in table.rows: for cell in row.cells: for p in cell.paragraphs: p.paragraph_format.space_before = Pt(2) p.paragraph_format.space_after = Pt(2) def add_section_break(doc): doc.add_page_break() def add_audience_tag(doc, text): tag = doc.add_paragraph() run = tag.add_run(text) run.italic = True run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) run.font.size = Pt(9) tag.paragraph_format.space_after = Pt(8) def add_labeled_para(doc, label, text): """Paragraphe avec un label en gras suivi du texte normal.""" p = doc.add_paragraph() run_label = p.add_run(label + ' — ') run_label.bold = True p.add_run(text) p.paragraph_format.space_after = Pt(5) return p # ── Couleurs ───────────────────────────────────────────────────────── BLUE_DARK = RGBColor(0x1A, 0x56, 0x8E) RED_DARK = RGBColor(0x8E, 0x1A, 0x1A) GREEN_DARK = RGBColor(0x2E, 0x7D, 0x32) # ══════════════════════════════════════════════════════════════════════ # PAGE DE TITRE # ══════════════════════════════════════════════════════════════════════ def build_title_page(doc): for _ in range(5): doc.add_paragraph() title = doc.add_heading('Pseudonymisation Automatique\nde Documents Médicaux', level=0) title.alignment = WD_ALIGN_PARAGRAPH.CENTER for run in title.runs: run.font.color.rgb = BLUE_DARK run.font.size = Pt(26) doc.add_paragraph() sub = doc.add_paragraph() sub.alignment = WD_ALIGN_PARAGRAPH.CENTER run = sub.add_run('Fiche technique — Sécurité et Conformité') run.font.size = Pt(14) run.font.color.rgb = RGBColor(0x66, 0x66, 0x66) doc.add_paragraph() audiences = doc.add_paragraph() audiences.alignment = WD_ALIGN_PARAGRAPH.CENTER run = audiences.add_run('DSI · RSSI · DPO') run.font.size = Pt(12) run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) run.italic = True doc.add_paragraph() doc.add_paragraph() # Encadré résumé summary = doc.add_paragraph() summary.alignment = WD_ALIGN_PARAGRAPH.CENTER run = summary.add_run( 'Logiciel d\'anonymisation fonctionnant 100% en local.\n' 'Aucune connexion réseau. Aucun sous-traitant. Aucune donnée exfiltrée.\n' 'Intelligence artificielle embarquée — masquage irréversible.' ) run.font.size = Pt(10.5) run.font.color.rgb = RGBColor(0x44, 0x44, 0x44) run.italic = True add_section_break(doc) # ══════════════════════════════════════════════════════════════════════ # 1. ARCHITECTURE ET FLUX DE DONNÉES # ══════════════════════════════════════════════════════════════════════ def build_architecture(doc): add_heading_styled(doc, '1. Architecture et flux de données', level=1) add_audience_tag(doc, '→ DSI, RSSI') add_para(doc, 'Principe fondamental : aucune donnée ne quitte le poste de travail.', bold=True, space_after=Pt(10)) points = [ ('Exécutable autonome', 'Un seul fichier .exe embarquant l\'intégralité des modèles d\'IA, ' 'des bases de référence et du moteur OCR. Pas d\'appel réseau, ' 'pas d\'API, pas de dépendance externe à l\'exécution.'), ('Traitement en mémoire', 'Les documents sont traités en mémoire vive. Aucune copie dans un ' 'répertoire temporaire partagé, aucun cache disque au-delà de la session.'), ('Aucune télémétrie', 'Pas de phoning home, pas de vérification de licence en ligne, ' 'pas de collecte de statistiques d\'usage.'), ('Stateless', 'Pas de base de données. Le logiciel ne conserve aucun état entre ' 'deux exécutions, hormis le fichier de configuration (whitelist/blacklist).'), ('Non destructif', 'Le document source n\'est jamais modifié. Un nouveau fichier ' 'anonymisé est créé dans le dossier de sortie.'), ] for label, text in points: add_labeled_para(doc, label, text) doc.add_paragraph() # Schéma flux add_heading_styled(doc, 'Flux de traitement', level=2) flow_steps = [ ('1', 'Entrée', 'Document source (poste local) — PDF, DOCX, image...'), ('2', 'Extraction', 'Extraction du texte en mémoire (layout-aware + OCR intégré si nécessaire)'), ('3', 'Détection', '4 moteurs IA analysent le texte en parallèle (CPU uniquement)'), ('4', 'Masquage', 'Remplacement irréversible des PII dans le PDF (texte supprimé + rectangle noir)'), ('5', 'Sortie', 'Écriture du PDF anonymisé sur disque local'), ('6', 'Nettoyage', 'Libération mémoire — aucune trace résiduelle'), ] table = doc.add_table(rows=0, cols=3) table.style = 'Table Grid' add_table_row(table, ['Étape', 'Phase', 'Description'], header=True) for step, phase, desc in flow_steps: add_table_row(table, [step, phase, desc]) set_table_style(table) # Largeurs colonnes for row in table.rows: row.cells[0].width = Cm(1.2) row.cells[1].width = Cm(2.8) row.cells[2].width = Cm(12) add_section_break(doc) # ══════════════════════════════════════════════════════════════════════ # 2. CONFORMITÉ RÉGLEMENTAIRE # ══════════════════════════════════════════════════════════════════════ def build_conformite(doc): add_heading_styled(doc, '2. Conformité réglementaire', level=1) add_audience_tag(doc, '→ DPO, RSSI, Direction') # ── RGPD ── add_heading_styled(doc, 'RGPD (Règlement 2016/679)', level=2) rgpd = [ ('Art. 25 — Privacy by design', 'Le traitement est conçu pour fonctionner sans transfert de données. ' 'L\'intelligence artificielle est embarquée dans l\'exécutable, ' 'pas hébergée sur un serveur distant.'), ('Art. 28 — Sous-traitance', 'Aucun sous-traitant au sens du RGPD. Le logiciel tourne sur ' 'l\'infrastructure de l\'établissement, sous sa responsabilité exclusive. ' 'Aucune donnée n\'est transmise à un tiers.'), ('Art. 32 — Sécurité du traitement', 'Les données ne transitent par aucun réseau. Le périmètre de sécurité ' 'est celui du poste de travail. Les mécanismes de protection existants ' '(chiffrement disque, contrôle d\'accès, antivirus) s\'appliquent.'), ('Art. 35 — Analyse d\'impact (AIPD)', 'Le logiciel réduit le risque sur les données personnelles : il anonymise, ' 'il ne crée pas de nouveau traitement. Pas de traitement à grande échelle ' 'externalisé. Une AIPD simplifiée peut suffire selon la politique de l\'établissement.'), ('Art. 17 — Droit à l\'effacement', 'Les documents anonymisés ne contiennent plus de données à caractère personnel. ' 'Ils sortent du champ d\'application du RGPD.'), ('Art. 5.1.e — Limitation de conservation', 'Le logiciel ne stocke aucune donnée personnelle au-delà de la session ' 'de traitement. Pas de base, pas de journal nominatif, pas d\'historique.'), ] for label, text in rgpd: add_labeled_para(doc, label, text) doc.add_paragraph() # ── AI Act ── add_heading_styled(doc, 'AI Act (Règlement 2024/1689)', level=2) ai_act = [ ('Classification du risque', 'Le logiciel utilise des modèles d\'IA pour la détection d\'entités nommées ' '(NER). Il ne prend aucune décision automatisée affectant des personnes. ' 'L\'IA est un outil d\'aide : l\'opérateur humain reste maître de la validation.'), ('Pas de système à haut risque', 'Pas de scoring, pas de profilage, pas de catégorisation de personnes. ' 'Le logiciel traite des documents, pas des individus.'), ('Transparence', 'Les modèles utilisés sont identifiés (CamemBERT, GLiNER). ' 'Leur fonctionnement est documenté. Les résultats sont vérifiables ' 'par l\'opérateur avant toute transmission.'), ] for label, text in ai_act: add_labeled_para(doc, label, text) doc.add_paragraph() # ── Cadre santé ── add_heading_styled(doc, 'Cadre réglementaire santé', level=2) sante = [ ('PGSSI-S', 'Compatible avec la Politique Générale de Sécurité des Systèmes ' 'd\'Information de Santé. Le logiciel fonctionne dans le périmètre SI ' 'de l\'établissement sans ouvrir de flux réseau.'), ('Hébergement de Données de Santé (HDS)', 'Pas d\'agrément HDS requis. Le logiciel n\'héberge rien — ' 'il traite en local et ne conserve aucune donnée.'), ('Découplage DPI', 'Le logiciel travaille sur des fichiers exportés du DPI. ' 'Aucune connexion directe au système d\'information clinique. ' 'Aucun risque d\'altération des données sources.'), ] for label, text in sante: add_labeled_para(doc, label, text) add_section_break(doc) # ══════════════════════════════════════════════════════════════════════ # 3. SÉCURITÉ TECHNIQUE # ══════════════════════════════════════════════════════════════════════ def build_securite(doc): add_heading_styled(doc, '3. Sécurité technique', level=1) add_audience_tag(doc, '→ RSSI') # ── Surface d'attaque ── add_heading_styled(doc, 'Surface d\'attaque', level=2) attack_surface = [ 'Aucun port réseau ouvert — le logiciel n\'écoute sur aucune interface', 'Aucune communication sortante — vérifiable par capture réseau (Wireshark, pare-feu)', 'Pas de serveur web embarqué, pas de socket, pas d\'API REST', 'Pas de service en arrière-plan ni de processus résident', 'Processus mono-instance protégé par verrou fichier (pas de double exécution)', ] for item in attack_surface: add_bullet(doc, item) doc.add_paragraph() # ── Exécutable ── add_heading_styled(doc, 'Exécutable', level=2) exe_points = [ ('Compilation', 'Compilé via PyInstaller — bundle Python, dépendances et modèles IA ' 'en un seul fichier .exe autonome.'), ('Signature de code', 'Compatible avec les outils de signature Windows (signtool). ' 'L\'établissement peut signer l\'exécutable avec son propre certificat ' 'si sa politique de sécurité l\'exige.'), ('Privilèges', 'Fonctionne en espace utilisateur standard. Pas d\'élévation de privilèges, ' 'pas d\'UAC, pas de droits administrateur requis.'), ('Registre Windows', 'Aucune écriture dans le registre Windows ni dans les répertoires système. ' 'L\'exécutable et sa configuration restent dans leur dossier.'), ('Empreinte', 'Exécutable unique (~720 Mo, modèles IA inclus). ' 'Aucune DLL externe, aucune dépendance système non standard.'), ] for label, text in exe_points: add_labeled_para(doc, label, text) doc.add_paragraph() # ── Données ── add_heading_styled(doc, 'Données au repos et en transit', level=2) table = doc.add_table(rows=0, cols=3) table.style = 'Table Grid' add_table_row(table, ['Catégorie', 'Localisation', 'Contenu sensible'], header=True) data_rows = [ ('Documents source', 'Inchangés, emplacement d\'origine', 'Oui — sous contrôle établissement'), ('Documents anonymisés', 'Dossier de sortie (local)', 'Non — PII supprimées'), ('Configuration (.yml)', 'À côté de l\'exécutable', 'Non — listes de termes uniquement'), ('Fichiers temporaires', 'Aucun', 'N/A'), ('Logs', 'Console uniquement (non persistés)', 'Non — pas de données nominatives'), ('Données en transit', 'Aucune', 'N/A — aucune communication réseau'), ] for cat, loc, sensible in data_rows: add_table_row(table, [cat, loc, sensible]) set_table_style(table) doc.add_paragraph() # ── Vérification indépendante ── add_heading_styled(doc, 'Vérification indépendante', level=2) add_para(doc, 'L\'absence de communication réseau est vérifiable de manière indépendante ' 'par l\'équipe sécurité de l\'établissement :', space_after=Pt(8)) verif = [ ('Capture réseau', 'Wireshark ou tcpdump pendant une session de traitement : aucun paquet émis.'), ('Pare-feu applicatif', 'Bloquer toute communication sortante pour le processus : aucun impact fonctionnel.'), ('Analyse statique', 'Aucune URL, aucun endpoint, aucune chaîne de connexion dans l\'exécutable.'), ('Sandbox', 'Exécution dans un environnement isolé (VM sans réseau) : fonctionnement nominal.'), ] for label, text in verif: add_labeled_para(doc, label, text) add_section_break(doc) # ══════════════════════════════════════════════════════════════════════ # 4. DÉPLOIEMENT # ══════════════════════════════════════════════════════════════════════ def build_deploiement(doc): add_heading_styled(doc, '4. Déploiement et maintenance', level=1) add_audience_tag(doc, '→ DSI') # ── Prérequis ── add_heading_styled(doc, 'Prérequis techniques', level=2) table = doc.add_table(rows=0, cols=2) table.style = 'Table Grid' add_table_row(table, ['Élément', 'Détail'], header=True) prereqs = [ ('Système d\'exploitation', 'Windows 10 / 11 (64 bits)'), ('Processeur', 'x86-64, 4 cœurs recommandés'), ('Mémoire vive', '8 Go minimum (16 Go recommandés pour lots volumineux)'), ('Espace disque', '~1 Go (exécutable + configuration)'), ('GPU', 'Non requis — inférence CPU uniquement'), ('Droits utilisateur', 'Utilisateur standard (pas d\'administrateur)'), ('Réseau', 'Aucun accès requis — fonctionnement 100% hors-ligne'), ('Logiciel tiers', 'Aucun — tout est embarqué dans l\'exécutable'), ] for elem, detail in prereqs: add_table_row(table, [elem, detail]) set_table_style(table) for row in table.rows: row.cells[0].width = Cm(4.5) row.cells[1].width = Cm(11.5) doc.add_paragraph() # ── Installation ── add_heading_styled(doc, 'Procédure d\'installation', level=2) steps = [ 'Copier l\'exécutable (.exe) sur le poste de travail', 'Premier lancement : le fichier de configuration (dictionnaires.yml) ' 'est créé automatiquement dans le même répertoire', 'Aucune autre manipulation — le logiciel est opérationnel', ] for i, step in enumerate(steps, 1): p = doc.add_paragraph() run = p.add_run(f'{i}. ') run.bold = True p.add_run(step) p.paragraph_format.space_after = Pt(4) add_para(doc, 'Pas d\'installeur MSI/EXE setup. Pas de modification du registre. ' 'Pas de redémarrage nécessaire.', italic=True, space_after=Pt(10)) # ── Mise à jour ── add_heading_styled(doc, 'Mise à jour', level=2) updates = [ 'Remplacer l\'exécutable par la nouvelle version', 'La configuration personnalisée est préservée (fichier séparé)', 'Pas de mise à jour automatique — contrôle total par la DSI', 'Pas de dépendance à un serveur de mise à jour', ] for item in updates: add_bullet(doc, item) doc.add_paragraph() # ── Déploiement multi-postes ── add_heading_styled(doc, 'Déploiement multi-postes', level=2) multi = [ ('Distribution', 'Copie simple via partage réseau, SCCM, GPO, ou clé USB.'), ('Configuration centralisée', 'Préparer un fichier .yml maître avec les paramètres communs, ' 'le distribuer avec l\'exécutable.'), ('Personnalisation par site', 'Chaque établissement peut ajuster sa configuration localement ' 'via l\'interface graphique (whitelist, blacklist).'), ('Échange de configuration', 'Export JSON depuis l\'interface → envoi par email → ' 'fusion centralisée → renvoi du YAML consolidé.'), ] for label, text in multi: add_labeled_para(doc, label, text) doc.add_paragraph() # ── Intégration SI ── add_heading_styled(doc, 'Intégration au SI', level=2) integration = [ 'Découplé du DPI — travaille sur des fichiers exportés, pas de connecteur direct', 'Utilisable en ligne de commande pour intégration dans un workflow batch', 'Compatible avec les espaces de travail sécurisés et les postes verrouillés', 'Aucune dépendance externe (pas d\'accès internet, pas de service tiers)', ] for item in integration: add_bullet(doc, item) add_section_break(doc) # ══════════════════════════════════════════════════════════════════════ # 5. GARANTIES D'ANONYMISATION # ══════════════════════════════════════════════════════════════════════ def build_garanties(doc): add_heading_styled(doc, '5. Garanties d\'anonymisation', level=1) add_audience_tag(doc, '→ DPO, RSSI') # ── Nature du masquage ── add_heading_styled(doc, 'Nature du masquage', level=2) masquage = [ ('Irréversibilité', 'Le texte original est supprimé du flux PDF, puis recouvert ' 'par un rectangle noir opaque. Il ne s\'agit pas d\'un calque amovible : ' 'l\'information est définitivement détruite dans le fichier de sortie.'), ('Métadonnées', 'Les métadonnées PDF contenant des données personnelles ' '(auteur, titre, sujet) sont nettoyées.'), ('Codes-barres', 'Les codes-barres contenant des identifiants patients sont ' 'détectés et masqués automatiquement.'), ('Intégrité du document', 'La structure, la mise en page et le contenu médical sont ' 'préservés. Seules les données à caractère personnel sont supprimées.'), ] for label, text in masquage: add_labeled_para(doc, label, text) doc.add_paragraph() # ── Tableau masqué / préservé ── add_heading_styled(doc, 'Périmètre de détection', level=2) table = doc.add_table(rows=0, cols=2) table.style = 'Table Grid' add_table_row(table, ['Données masquées (PII)', 'Données préservées'], header=True) rows_data = [ ('Noms et prénoms (patients)', 'Diagnostics et conclusions médicales'), ('Noms et prénoms (médecins, soignants)', 'Traitements et posologies'), ('Adresses postales', 'Actes médicaux (codage CCAM)'), ('Numéros de téléphone', 'Résultats d\'examens et de biologie'), ('Numéros de sécurité sociale', 'Dates de séjour et d\'intervention'), ('Dates de naissance', 'Codage CIM-10'), ('Noms d\'établissements', 'Comptes-rendus opératoires (contenu)'), ('Codes-barres d\'identification', 'Structure et mise en page'), ] for masked, preserved in rows_data: add_table_row(table, [masked, preserved]) set_table_style(table) doc.add_paragraph() # ── Moteurs de détection ── add_heading_styled(doc, 'Moteurs de détection', level=2) add_para(doc, 'Le logiciel combine 4 moteurs d\'intelligence artificielle spécialisés ' 'en français clinique. Chaque moteur analyse le document indépendamment. ' 'Les résultats sont croisés pour maximiser le rappel (recall) et minimiser ' 'les oublis.', space_after=Pt(8)) engines = [ ('Modèle CamemBERT médical', 'Modèle de langue français pré-entraîné, spécialisé dans la ' 'reconnaissance d\'entités nommées en contexte clinique.'), ('Détection zero-shot', 'Modèle capable de détecter des données personnelles sans ' 'entraînement spécifique, par compréhension sémantique du contexte.'), ('Modèle fine-tuné', 'Modèle entraîné spécifiquement sur plus de 1 000 documents médicaux ' 'réels pour une précision maximale sur les formats cliniques français.'), ('Bases de référence nationales', '219 000 noms de famille INSEE, 36 000 prénoms, ' '7 300 médicaments (BDPM), 108 000 établissements de santé (FINESS). ' 'Ces bases servent à la fois à la détection et à l\'élimination ' 'des faux positifs (termes médicaux confondus avec des noms).'), ] for label, text in engines: add_labeled_para(doc, label, text) doc.add_paragraph() # ── Performances ── add_heading_styled(doc, 'Performances mesurées', level=2) table2 = doc.add_table(rows=0, cols=2) table2.style = 'Table Grid' add_table_row(table2, ['Indicateur', 'Valeur'], header=True) perfs = [ ('Taux de détection (recall)', '> 99% sur corpus réel de contrôle T2A'), ('Types de documents validés', 'CR opératoires, anatomo-pathologie, bactériologie,\n' 'anesthésie, courriers, CR d\'hospitalisation'), ('Temps moyen par document', 'Quelques dizaines de secondes (poste standard)'), ('Traitement par lot', '20-30 documents en 10-15 minutes'), ('Principe de précaution', 'En cas de doute, le logiciel masque\n' '(préférence faux positif sur faux négatif)'), ] for ind, val in perfs: add_table_row(table2, [ind, val]) set_table_style(table2) for row in table2.rows: row.cells[0].width = Cm(5.5) row.cells[1].width = Cm(10.5) add_section_break(doc) # ══════════════════════════════════════════════════════════════════════ # 6. FAQ DSI / RSSI / DPO # ══════════════════════════════════════════════════════════════════════ def build_faq(doc): add_heading_styled(doc, '6. Questions fréquentes — DSI / RSSI / DPO', level=1) faq = [ ('Les données transitent-elles par un cloud ?', 'Non. Le logiciel fonctionne intégralement hors-ligne. Aucune connexion ' 'réseau n\'est établie, ni en entrée ni en sortie. Vérifiable par ' 'capture réseau.'), ('Faut-il un agrément Hébergeur de Données de Santé (HDS) ?', 'Non. Aucune donnée n\'est hébergée par un tiers. Le traitement est ' 'effectué localement sur l\'infrastructure de l\'établissement.'), ('Y a-t-il un sous-traitant au sens du RGPD ?', 'Non. Le logiciel est un outil exécuté en local. Aucune donnée n\'est ' 'transmise à l\'éditeur ni à un tiers. L\'établissement est seul ' 'responsable de traitement.'), ('Faut-il réaliser une AIPD ?', 'Le logiciel réduit le risque sur les données personnelles. Il n\'introduit ' 'pas de nouveau traitement à risque. Une AIPD simplifiée peut suffire, ' 'selon la politique de l\'établissement et l\'avis du DPO.'), ('Le masquage est-il réversible ?', 'Non. Le texte est supprimé du fichier PDF, pas simplement recouvert. ' 'Aucun outil ne permet de récupérer les données masquées dans le ' 'fichier anonymisé.'), ('Quelles données sont stockées par le logiciel ?', 'Aucune donnée personnelle. Le logiciel ne maintient ni base de données, ' 'ni journal nominatif, ni historique de traitement. Seul le fichier de ' 'configuration (listes de termes) est persisté.'), ('Peut-on auditer le comportement réseau ?', 'Oui. L\'absence de trafic est vérifiable par Wireshark, pare-feu ' 'applicatif, ou exécution dans une VM sans interface réseau. ' 'Le logiciel fonctionne de manière identique.'), ('L\'exécutable est-il signable ?', 'Oui. L\'exécutable est compatible avec les outils de signature ' 'Windows standard (signtool). L\'établissement peut appliquer ' 'son propre certificat de signature de code.'), ('Comment s\'intègre-t-il avec le DPI ?', 'Le logiciel est découplé du DPI. Il travaille sur des fichiers ' 'exportés (PDF, DOCX, etc.). Aucun connecteur direct, aucun accès ' 'aux bases cliniques, aucun risque d\'altération.'), ('Que se passe-t-il si un nom n\'est pas détecté ?', 'L\'établissement peut ajouter le terme dans la blacklist via ' 'l\'interface graphique. Il sera systématiquement masqué lors des ' 'prochains traitements. La configuration est immédiatement active.'), ('Que se passe-t-il si un terme médical est masqué par erreur ?', 'L\'établissement peut l\'ajouter dans la whitelist. Le terme ne sera ' 'plus masqué. La configuration est échangeable entre sites.'), ('Quel est l\'impact sur la performance du poste ?', 'Le traitement mobilise le processeur pendant quelques dizaines de ' 'secondes par document. L\'utilisation mémoire reste sous 4 Go. ' 'Le poste est utilisable pendant le traitement.'), ] for question, answer in faq: p = doc.add_paragraph() run_q = p.add_run(question) run_q.bold = True run_q.font.size = Pt(10) p.paragraph_format.space_after = Pt(2) p2 = doc.add_paragraph() run_a = p2.add_run(answer) run_a.font.size = Pt(9.5) run_a.font.color.rgb = RGBColor(0x33, 0x33, 0x33) p2.paragraph_format.space_after = Pt(10) p2.paragraph_format.left_indent = Cm(0.5) add_section_break(doc) # ══════════════════════════════════════════════════════════════════════ # 7. MATRICE DE CONFORMITÉ # ══════════════════════════════════════════════════════════════════════ def build_matrice(doc): add_heading_styled(doc, '7. Matrice de conformité', level=1) add_audience_tag(doc, '→ DPO, RSSI — synthèse décisionnelle') add_para(doc, 'Tableau récapitulatif des exigences réglementaires et de sécurité, ' 'et de la réponse apportée par le logiciel.', space_after=Pt(10)) table = doc.add_table(rows=0, cols=3) table.style = 'Table Grid' add_table_row(table, ['Exigence', 'Statut', 'Commentaire'], header=True) matrix = [ ('Données hors établissement', 'Conforme', 'Aucune donnée ne quitte le poste'), ('Sous-traitance RGPD', 'Conforme', 'Aucun sous-traitant'), ('Privacy by design (Art. 25)', 'Conforme', 'IA embarquée, hors-ligne natif'), ('Sécurité du traitement (Art. 32)', 'Conforme', 'Périmètre = poste local'), ('AIPD (Art. 35)', 'Simplifié', 'Réduit le risque, ne le crée pas'), ('Agrément HDS', 'Non requis', 'Pas d\'hébergement tiers'), ('PGSSI-S', 'Compatible', 'Aucun flux réseau ouvert'), ('AI Act — risque', 'Risque limité', 'Outil d\'aide, pas de décision automatisée'), ('Masquage irréversible', 'Conforme', 'Suppression + rectangle noir'), ('Signature de code', 'Compatible', 'Signable par certificat établissement'), ('Auditabilité réseau', 'Vérifiable', 'Wireshark / pare-feu / sandbox'), ('Droits administrateur', 'Non requis', 'Espace utilisateur standard'), ] for exigence, statut, comment in matrix: row = table.add_row() # Exigence cell0 = row.cells[0] cell0.text = '' p0 = cell0.paragraphs[0] run0 = p0.add_run(exigence) run0.font.size = Pt(9) p0.paragraph_format.space_before = Pt(3) p0.paragraph_format.space_after = Pt(3) # Statut avec couleur cell1 = row.cells[1] cell1.text = '' p1 = cell1.paragraphs[0] run1 = p1.add_run(statut) run1.font.size = Pt(9) run1.bold = True if statut in ('Conforme', 'Compatible', 'Vérifiable'): run1.font.color.rgb = GREEN_DARK elif statut in ('Non requis', 'Simplifié', 'Risque limité'): run1.font.color.rgb = BLUE_DARK p1.paragraph_format.space_before = Pt(3) p1.paragraph_format.space_after = Pt(3) # Commentaire cell2 = row.cells[2] cell2.text = '' p2 = cell2.paragraphs[0] run2 = p2.add_run(comment) run2.font.size = Pt(9) run2.font.color.rgb = RGBColor(0x55, 0x55, 0x55) p2.paragraph_format.space_before = Pt(3) p2.paragraph_format.space_after = Pt(3) set_table_style(table) for row in table.rows: row.cells[0].width = Cm(4.5) row.cells[1].width = Cm(2.5) row.cells[2].width = Cm(9) # ══════════════════════════════════════════════════════════════════════ # ASSEMBLAGE FINAL # ══════════════════════════════════════════════════════════════════════ def main(): doc = Document() # ── Style par défaut ── style = doc.styles['Normal'] style.font.name = 'Calibri' style.font.size = Pt(10) style.paragraph_format.space_after = Pt(4) # Marges for section in doc.sections: section.top_margin = Cm(1.5) section.bottom_margin = Cm(1.5) section.left_margin = Cm(2) section.right_margin = Cm(2) # Construction build_title_page(doc) build_architecture(doc) build_conformite(doc) build_securite(doc) build_deploiement(doc) build_garanties(doc) build_faq(doc) build_matrice(doc) # Sauvegarde output_path = os.path.join( os.path.dirname(os.path.dirname(__file__)), 'Fiche_Technique_DSI_RSSI_DPO.docx' ) doc.save(output_path) print(f'Fiche technique générée : {output_path}') if __name__ == '__main__': main()