809 lines
34 KiB
Python
809 lines
34 KiB
Python
#!/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()
|