// Variables globales let csvData = null; let detectedColumns = { identite: [], classe: [], mention: [], notes: [], moyennes: [] }; let allColumns = []; let manualMode = false; // Détection automatique du séparateur CSV function detectDelimiter(text) { const firstLine = text.split('\n')[0]; const delimiters = { ',': 0, ';': 0, '\t': 0 }; firstLine.split('').forEach(char => { if (delimiters[char] !== undefined) { delimiters[char]++; } }); return Object.keys(delimiters).reduce((a, b) => delimiters[a] > delimiters[b] ? a : b ); } // Détection automatique des types de colonnes function detectColumns(headers, parsedData) { const columns = { identite: [], classe: [], mention: [], notes: [], moyennes: [] }; headers.forEach(col => { const colLower = col.toLowerCase(); // Identité (élargi) const identiteKeywords = ['nom', 'prenom', 'prénom', 'name', 'surname', 'patient', 'client', 'employé', 'employee', 'matricule', 'id']; if (identiteKeywords.some(kw => colLower === kw || colLower.startsWith(kw + ' ') || colLower.endsWith(' ' + kw) || colLower.includes('_' + kw) )) { columns.identite.push(col); } // Élève/Étudiant/User else if (['eleve', 'élève', 'etudiant', 'étudiant', 'user', 'utilisateur'].some(kw => colLower.includes(kw) )) { columns.identite.push(col); } // Catégories/Classes (élargi) else if (['classe', 'section', 'filiere', 'filière', 'serie', 'série', 'grade', 'niveau', 'category', 'categorie', 'département', 'department', 'service', 'group', 'groupe'].some(kw => colLower.includes(kw))) { columns.classe.push(col); } // Mentions/Distinctions (élargi) else if (colLower === 'mention' || colLower.startsWith('mention ') || colLower.endsWith(' mention') || colLower.includes('distinction') || colLower.includes('recognition')) { columns.mention.push(col); } // Moyennes/Scores (élargi) else if (['moyenne', 'moy', 'mean', 'average', 'score', 'general', 'général', 'continu', 'terminal', 'rating', 'évaluation'].some(kw => colLower.includes(kw))) { columns.moyennes.push(col); } // Notes/Valeurs numériques (détection flexible) else if (isNumericColumn(parsedData.data.map(row => row[col]))) { columns.notes.push(col); } }); return columns; } // Vérifier si une colonne contient des valeurs numériques function isNumericColumn(values) { const numericValues = values.map(val => { let v = val; if (typeof v === 'string') { v = v.replace(',', '.'); } return parseFloat(v); }).filter(v => !isNaN(v)); // Au moins 50% de valeurs numériques if (numericValues.length < values.length * 0.5) { return false; } // Vérifier que les valeurs sont dans une plage raisonnable const min = Math.min(...numericValues); const max = Math.max(...numericValues); // Accepter toutes plages positives raisonnables return min >= 0 && max <= 1000000; } // Afficher la configuration manuelle function showManualConfig() { const selector = document.getElementById('columnSelector'); selector.innerHTML = ''; allColumns.forEach(col => { const item = document.createElement('div'); item.className = 'column-item'; item.innerHTML = ` `; selector.appendChild(item); }); document.getElementById('manualConfig').style.display = 'block'; } // Valider la configuration manuelle document.getElementById('btnValidateConfig').addEventListener('click', () => { detectedColumns = { identite: [], classe: [], mention: [], notes: [], moyennes: [] }; document.querySelectorAll('#columnSelector select').forEach(select => { const column = select.getAttribute('data-column'); const type = select.value; if (type !== 'ignore') { detectedColumns[type].push(column); } }); updateDetectionDisplay(); manualMode = true; document.getElementById('manualConfig').style.display = 'none'; document.getElementById('btnAnonymize').style.display = 'block'; }); // Mettre à jour l'affichage de la détection function updateDetectionDisplay() { const info = document.getElementById('detectionInfo'); info.innerHTML = ` 🔍 Configuration des colonnes ${manualMode ? '(manuelle)' : '(automatique)'} :

Identité : ${detectedColumns.identite.join(', ') || 'Aucune'}
Catégories : ${detectedColumns.classe.join(', ') || 'Aucune'}
Distinctions : ${detectedColumns.mention.join(', ') || 'Aucune'}
Valeurs numériques : ${detectedColumns.notes.length} colonnes
Moyennes/Scores : ${detectedColumns.moyennes.length} colonnes ${!manualMode ? '

' : ''} `; info.style.display = 'block'; } // Rendre la fonction accessible globalement window.showManualConfig = showManualConfig; // Gestion du chargement de fichier document.getElementById('fileInput').addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; document.getElementById('fileName').textContent = `✓ ${file.name}`; document.getElementById('fileName').style.display = 'block'; document.getElementById('loader').style.display = 'block'; manualMode = false; const reader = new FileReader(); reader.onload = (event) => { const text = event.target.result; const delimiter = detectDelimiter(text); Papa.parse(file, { header: true, skipEmptyLines: true, dynamicTyping: false, delimiter: delimiter, complete: (results) => { csvData = results; allColumns = results.meta.fields; detectedColumns = detectColumns(results.meta.fields, results); // Vérifier si la détection a fonctionné const hasDetection = detectedColumns.identite.length > 0 || detectedColumns.classe.length > 0 || detectedColumns.notes.length > 0; updateDetectionDisplay(); if (hasDetection) { document.getElementById('btnAnonymize').style.display = 'block'; } else { showManualConfig(); } document.getElementById('loader').style.display = 'none'; } }); }; reader.readAsText(file); }); // Fonction de hachage SHA-256 async function sha256(str) { const buffer = new TextEncoder().encode(str); const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } // Pseudonymisation async function pseudonymiser(nom, salt) { if (!nom) return 'ID_INCONNU'; const hash = await sha256(nom + salt); return `ID_${hash.substring(0, 8).toUpperCase()}`; } // Généralisation des notes (flexible) function generaliserNote(note) { if (typeof note === 'string') { note = note.replace(',', '.'); } const n = parseFloat(note); if (isNaN(n)) return null; // Trouver la plage max dans toutes les données const allNotes = csvData.data.map(row => Object.values(row).map(val => { let v = val; if (typeof v === 'string') { v = v.replace(',', '.'); } return parseFloat(v); }).filter(v => !isNaN(v)) ).flat(); const maxValue = Math.max(...allNotes); const trancheSize = maxValue / 5; if (n < trancheSize) return 'Tranche 1'; if (n < trancheSize * 2) return 'Tranche 2'; if (n < trancheSize * 3) return 'Tranche 3'; if (n < trancheSize * 4) return 'Tranche 4'; return 'Tranche 5'; } // Calcul du k-anonymat function calculerKAnonymat(data, colonnes) { if (!colonnes || colonnes.length === 0) return null; const groupes = {}; const groupesDetails = {}; data.forEach((row, index) => { const key = colonnes.map(col => row[col] || 'N/A').join('|'); groupes[key] = (groupes[key] || 0) + 1; if (!groupesDetails[key]) { groupesDetails[key] = []; } groupesDetails[key].push({ index: index, id: row['ID_Anonyme'] || row[Object.keys(row)[0]], caracteristiques: colonnes.map(col => `${col}: ${row[col] || 'N/A'}`).join(', ') }); }); const counts = Object.values(groupes); const isolables = []; // Trouver les individus isolables (k=1) Object.keys(groupes).forEach(key => { if (groupes[key] === 1) { isolables.push(groupesDetails[key][0]); } }); return { k: Math.min(...counts), nbUniques: counts.filter(c => c === 1).length, nbTotal: counts.length, isolables: isolables }; } // Évaluer le risque function evaluerRisque(k) { if (k === null) return { niveau: '❓ INCONNU' }; if (k === 1) return { niveau: '🔴 CRITIQUE' }; if (k < 3) return { niveau: '🟠 ÉLEVÉ' }; if (k < 5) return { niveau: '🟡 MODÉRÉ' }; return { niveau: '🟢 FAIBLE' }; } // Télécharger CSV function downloadCSV(data, filename) { let processedData = data; if (document.getElementById('optConvertirVirgules').checked) { processedData = data.map(row => { const newRow = {}; Object.keys(row).forEach(key => { let value = row[key]; if (typeof value === 'string' && /^\d+,\d+$/.test(value)) { newRow[key] = value.replace(',', '.'); } else { newRow[key] = value; } }); return newRow; }); } const csv = Papa.unparse(processedData); const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => URL.revokeObjectURL(link.href), 100); } // Anonymisation principale document.getElementById('btnAnonymize').addEventListener('click', async () => { if (!csvData) return; document.getElementById('loader').style.display = 'block'; document.getElementById('btnAnonymize').disabled = true; await new Promise(resolve => setTimeout(resolve, 100)); const salt = Math.random().toString(36).substring(2, 15); let data = [...csvData.data]; // Pseudonymisation if (detectedColumns.identite.length > 0) { for (let i = 0; i < data.length; i++) { const nom = data[i][detectedColumns.identite[0]]; data[i]['ID_Anonyme'] = await pseudonymiser(nom, salt); detectedColumns.identite.forEach(col => delete data[i][col]); } } else { data.forEach((row, i) => { row['ID_Anonyme'] = `ID_${String(i + 1).padStart(8, '0')}`; }); } // Généralisation des classes if (document.getElementById('optGeneraliserClasse').checked) { detectedColumns.classe.forEach(col => { data.forEach(row => { if (row[col]) { row[col] = row[col].split(' ')[0].split('-')[0]; } }); }); } // Regroupement des mentions if (document.getElementById('optRegrouperMentions').checked) { detectedColumns.mention.forEach(col => { data.forEach(row => { const mention = (row[col] || '').toUpperCase(); if (mention && mention !== 'AUCUN' && mention !== 'NON' && mention !== 'NONE') { row[col] = 'Avec distinction'; } else { row[col] = 'Sans distinction'; } }); }); } // Généralisation des notes if (document.getElementById('optGeneraliserNotes').checked) { [...detectedColumns.notes, ...detectedColumns.moyennes].forEach(col => { data.forEach(row => { if (row[col]) { row[col + '_tranche'] = generaliserNote(row[col]); delete row[col]; } }); }); } // Mélange data.sort(() => Math.random() - 0.5); // Calcul k-anonymat const quasiIds = [...detectedColumns.classe, ...detectedColumns.mention]; const kStandard = calculerKAnonymat(data, quasiIds); const risqueStandard = evaluerRisque(kStandard?.k); // Version K≥3 let dataK3 = null; let kK3 = null; if (document.getElementById('optVersionK3').checked) { dataK3 = JSON.parse(JSON.stringify(data)); detectedColumns.classe.forEach(col => { dataK3.forEach(row => { if (row[col]) row[col] = row[col].split(' ')[0].split('-')[0]; }); }); detectedColumns.mention.forEach(col => { dataK3.forEach(row => { const mention = (row[col] || '').toUpperCase(); if (mention && mention !== 'AUCUN' && mention !== 'NON' && mention !== 'NONE') { row[col] = 'Avec distinction'; } else { row[col] = 'Sans distinction'; } }); }); kK3 = calculerKAnonymat(dataK3, quasiIds); } // Affichage des résultats let resultText = `✓ ANONYMISATION RÉUSSIE ! Enregistrements traités : ${data.length} 🔍 COLONNES TRAITÉES : • Identité supprimée : ${detectedColumns.identite.join(', ') || 'Aucune'} • Catégories : ${detectedColumns.classe.join(', ') || 'Aucune'} • Distinctions : ${detectedColumns.mention.join(', ') || 'Aucune'} • Valeurs numériques : ${detectedColumns.notes.length} colonnes • Moyennes : ${detectedColumns.moyennes.length} colonnes ${'='.repeat(60)} 📊 ANALYSE VERSION STANDARD ${'='.repeat(60)} K-anonymat : ${kStandard?.k || 'N/A'} Individus isolables : ${kStandard?.nbUniques || 0}/${kStandard?.nbTotal || 0} ${kStandard?.isolables && kStandard.isolables.length > 0 ? ` ⚠️ INDIVIDUS ISOLABLES DÉTECTÉS (k=1) : ${kStandard.isolables.map((ind, i) => ` ${i+1}. ID: ${ind.id} Caractéristiques uniques : ${ind.caracteristiques} `).join('')} ` : '✓ Aucun individu isolable détecté'} `; if (dataK3 && kK3) { resultText += ` ${'='.repeat(60)} 📊 ANALYSE VERSION RENFORCÉE (k≥3) ${'='.repeat(60)} K-anonymat : ${kK3.k} Individus isolables : ${kK3.nbUniques}/${kK3.nbTotal} ${kK3.isolables && kK3.isolables.length > 0 ? ` ⚠️ INDIVIDUS ISOLABLES DÉTECTÉS (k=1) : ${kK3.isolables.map((ind, i) => ` ${i+1}. ID: ${ind.id} Caractéristiques uniques : ${ind.caracteristiques} `).join('')} ` : '✓ Aucun individu isolable détecté'} `; } resultText += ` ${'='.repeat(60)} 💡 RECOMMANDATIONS : `; if (kStandard?.k < 3) { resultText += ` • Version standard : NON recommandée pour diffusion\n • ${kStandard?.nbUniques || 0} individu(s) isolable(s) à protéger\n • Utilisez la version k≥3\n`; } else if (kStandard?.k < 5) { resultText += ` • Protection acceptable, k≥5 recommandé\n • Activez la généralisation des notes\n`; } else { resultText += ` • Conforme CNIL ✓\n`; } // Cartes K-anonymat const kIndicators = document.getElementById('kIndicators'); let cardsHTML = ''; const conformiteStandard = kStandard?.k >= 5 ? 'conforme' : kStandard?.k >= 3 ? 'acceptable' : 'non-conforme'; const conformiteTextStandard = kStandard?.k >= 5 ? '✅ CONFORME' : kStandard?.k >= 3 ? '⚠️ ACCEPTABLE' : '❌ NON CONFORME'; const risqueClass = kStandard?.k === 1 ? 'risk-critical' : kStandard?.k < 3 ? 'risk-high' : kStandard?.k < 5 ? 'risk-moderate' : 'risk-low'; cardsHTML += `
📁 Version Standard
${kStandard?.k || 'N/A'}
K-anonymat
${conformiteTextStandard}
${risqueStandard.niveau}
Individus isolables ${kStandard?.nbUniques || 0}/${kStandard?.nbTotal || 0}
CNIL recommande k ≥ 5
`; if (dataK3 && kK3) { const risqueK3 = evaluerRisque(kK3.k); const conformiteK3 = kK3.k >= 5 ? 'conforme' : kK3.k >= 3 ? 'acceptable' : 'non-conforme'; const conformiteTextK3 = kK3.k >= 5 ? '✅ CONFORME' : kK3.k >= 3 ? '⚠️ ACCEPTABLE' : '❌ NON CONFORME'; const risqueClassK3 = kK3.k === 1 ? 'risk-critical' : kK3.k < 3 ? 'risk-high' : kK3.k < 5 ? 'risk-moderate' : 'risk-low'; cardsHTML += `
🔒 Version Renforcée
${kK3.k}
K-anonymat
${conformiteTextK3}
${risqueK3.niveau}
Individus isolables ${kK3.nbUniques}/${kK3.nbTotal}
`; } kIndicators.innerHTML = cardsHTML; kIndicators.style.display = 'grid'; document.getElementById('results').textContent = resultText; document.getElementById('results').style.display = 'block'; document.getElementById('resultsSection').style.display = 'block'; // Stocker pour téléchargement window.dataStandard = data; window.dataK3 = dataK3; // Boutons de téléchargement const downloadButtons = document.getElementById('downloadButtons'); downloadButtons.innerHTML = ` ${dataK3 ? '' : ''} `; document.getElementById('btnDL1').addEventListener('click', () => { downloadCSV(window.dataStandard, 'anonymise.csv'); }); if (dataK3) { document.getElementById('btnDL2').addEventListener('click', () => { downloadCSV(window.dataK3, 'anonymise_k3.csv'); }); } document.getElementById('loader').style.display = 'none'; document.getElementById('btnAnonymize').disabled = false; document.getElementById('resultsSection').scrollIntoView({ behavior: 'smooth' }); });