Aller au contenu

Introduction à la génération de données de test

·7 mins
Sommaire

Normalement vous le savez déjà, lorsque l’on développe ou test une application il ne faut pas utiliser de données réelles. Lorsque les conséquences sont que vous envoyez par erreur un email de test à tout ou partie des utilisateurs c’est assez humiliant mais ce n’est pas encore très grave. En revanche, lorsque vos environnements de dev et de pré-production ont, de part leur nature, un niveau de sécurité inférieur à l’environnement de production, il se peut qu’un attaquant récupère les données. Et oui, c’est vraiment arrivé : je l’ai moi-même constaté lorsque j’étais contrôleur à la CNIL en intervenant suite à la dite violation de données. D’ailleurs, si vous utilisez des données personnelles réelles il s’agit d’un traitement distinct du traitement principal utilisé en production, et bon courage pour justifier ça sur le plan légal. Bref, il est bien plus pratique d’utiliser des données fictives qui ne sont soumises à aucune réglementation et n’ont strictement aucune sensibilité.

Méthodologie
#

Générer soi-même des données de test est assez fastidieux, même si l’on écrit des scripts spécialement pour ça. Et surtout ça comporte un gros problème : elles ressemblent rarement aux données réelles. En effet, on a tendance à n’inclure que des cas standards et parfois quelques cas non-conformes auxquels on a pensé, ce qui nous fait passer à côté de l’immensité de cas non-conformes rencontrés dans al réalité et auxquels l’on n’a pas pensé. C’est d’ailleurs pour ça que certains bravent l’interdit et développent en utilisant des données réelles.

Cependant, il y a un moyen d’obtenir le meilleur des deux mondes en anonymisant un jeu de données réelles et en y ajoutant éventuellement quelques cas non-conformes spécifiques pour des tests. Ainsi on ne travaille pas avec des données réelles mais notre jeu de données de test conserve toutes les propriétés des données réelles.

Notions d’anonymisation
#

Je vous arrête tout de suite : vous ne savez pas correctement anonymiser un jeu de données. Ce n’est pas que je vous prend pour des mauvais, c’est juste que processus est immensément plus complexe qu’il n’y parait et donc que statistiquement l’immense majorité des personnes lisant cet article ne maîtrisent pas le sujet.

La première chose à laquelle on pense pour anonymiser un jeu de données est de supprimer les données identifiantes. C’est bien, mais malheureusement ce n’est pas suffisant. En effet, la recherche a montré que même en l’absence de données identifiantes il est souvent possible de tout de même ré-identifier certaines personnes. Afin de correctement anonymiser le jeu de données il faut, en plus, procéder à une opération de généralisation des données. Ceci qui consiste à ne plus utiliser une donnée précise mais des catégories de données (par exemple une date de naissance est transformée en une catégorie d’âge).

La méthode consistant à appliquer une phase de suppression puis une phase de généralisation des données est appelée k-anonymisation. Afin de vérifier l’efficacité du procédé on calcule une valeur k. Plus la valeur de k est grande plus le jeu est correctement anonymisé, une valeur de 1 indiquant qu’il existe des entrées uniques qui sont donc ré-identifiables. Si la valeur de k n’est pas assez grande il faut soit réajuster l’étape de généralisation soit enlever les données causant une valeur si faible.

La k-anonymisation n’est pas si simple que ça à mettre en œuvre car la valeur de k est susceptible de varier à chaque modification des données. Chaque création de nouveau jeu de données anonymisées à partir d’un jeu de données réelles pourra donc nécessiter des ajustements. De plus, il faut déterminer une valeur minimale de k, ce qui varie en fonction du jeu de données concerné.

Dans le cas précis de la génération de données de tests, à la place de supprimer et généraliser les données, on remplace les donnés qui auraient du l’être par des données fictives. Afin de s’assurer de l’anonymisation effective, on calcule alors la valeur de k sans prendre en compte les données ainsi remplacées.

De nos jours la k-anonymisation n’est clairement plus la référence en matière d’anonymisation de données : afin d’accéder à ce qui se fait de mieux, il faut nous pencher du côté de la confidentialité différentielle. Si certains algorithmes de confidentialité différentielle permettent de générer des statistiques anonymes sur un jeu de données, d’autres permettent de générer un nouveau jeu de données qui conserve les propriétés du jeu de données initial.

Si la confidentialité différentielle est assez géniale pour générer un jeu de données de test à partir d’un jeu de données réelles, il faut cependant noter qu’elle reste difficile à mettre en œuvre. Il existe différentes implémentations et vous pourriez trouver votre bonheur dedans. Cependant, tout comme avec la k-anonymisation, vous n’échapperez pas à la définition d’un « niveau d’anonymisation », sauf qu’ici on ne parle pas de k mais d’ε (epsilon). À l’inverse de k, plus la valeur d’ε est petite mieux le jeu est anonymisé.

Remplacer des données identifiantes
#

Quoi que l’on choisisse de faire on se retrouvera à devoir générer de nouvelles données en remplacement des données personnelles. Voyons donc les principales manières de s’y prendre.

Les données réservées
#

Le cas le plus simple est celui des données disposant d’une plage garantie non-valide. C’est par exemple le cas des adresses IP dont les plages suivantes sont réservées pour la documentation :

  • 2001:db8::/32 (IPv6)
  • 192.0.2.0/24 (IPv4)
  • 198.51.100.0/24 (IPv4)
  • 203.0.113.0/24 (IPv4)

En remplaçant ainsi les données réelles par des donnés qui sont garanties non-valides, on s’assure un résultat réaliste.

La clé de contrôle
#

Certaines données intègrent une clé de contrôle permettant de vérifier que le contenu n’a pas été altéré. C’est par exemple le cas des numéro SIREN ou des IBAN. Soit dit en passant, les IBAN français contiennent 2 clés de contrôle : la partie BBAN est composée de ce qui était le RIB qui intègre déjà une clé de contrôle, il y a donc une clé de contrôle sur le RIB/BBAN et une sur l’IBAN complet lui-même. C’est plutôt pratique car dans le cas particulier de la France, le code pays s’intègre assez mal avec l’algorithme de la clé de contrôle de l’IBAN : si le BBAN est entièrement numérique (ce qui le cas la plupart du temps), la clé de contrôle de l’IBAN sera toujours 76, ce qui empêche de détecter la plupart des incohérences.

L’astuce ici est de volontairement générer des données avec une clé de contrôle invalide afin de s’assurer que la donnée n’est pas réellement utilisée. Bien entendu ceci risque de se heurter aux mécanismes du code permettant de justement détecter de telles données invalides. Idéalement de tels mécanismes ne devraient être mis en œuvre que lors de l’insertion des données par des moyens habituels (formulaire de saisie, récupération par API, etc.), ce qui permet de ne pas les déclencher lors de l’import de données de tests.

Voici un exemple de génération de numéros SIREN invalides en Python :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/usr/bin/env python3

from random import randint, shuffle
import sys


def get_part(nb, i):
    if i % 2 == 0:
        return nb
    new_nb = nb * 2
    if new_nb > 9:
        return new_nb - 9
    return new_nb


def is_valid(parts):
    parts_rev = list(reversed(parts))
    parts_d = [get_part(n, i) for i, n in enumerate(parts_rev)]
    s = sum(parts_d)
    return (s % 10) == 0


def get_verif_code(parts):
    code_lst = list(range(10))
    shuffle(code_lst)
    for code in code_lst:
        siren = parts + [code]
        if not is_valid(siren):
            return code


def new_invalid_siren():
    parts_int = [randint(0, 9) for _ in range(8)]
    parts_int.append(get_verif_code(parts_int))
    parts_str = [str(n) for n in parts_int]
    return "".join(parts_str)


def main():
    nb = 1
    if len(sys.argv) > 1:
        nb = int(sys.argv[1])
    for _ in range(nb):
        print(new_invalid_siren())


if __name__ == "__main__":
    main()

Génération aléatoire
#

S’il n’existe ni données réservées ni clé de contrôle, il ne reste plus qu’à générer de nouvelles données aléatoires. C’est là qu’il faut être créatif si l’on souhaite rester réaliste. Par exemple, pour générer des identités il est possible de s’appuyer sur les données fournies par l’INSEE, en particulier le fichier des noms et le fichier des prénoms. Pour les photos de profil, il y a une multitude de générateurs d’images par IA qui font ça.

Pour les adresses postales, il est possible d’utiliser la Base Adresse Nationale (BAN) afin de générer des adresses qui n’existent pas mais restent très réaliste. Une méthode que j’apprécie particulièrement consiste à séparer les données en 2 catégories, d’un coté la commune elle-même (nom de la commune, code postal, etc.) et de l’autre coté l’adresse à l’intérieur de la commune (numéro, voie, etc.). Une fois fait, on sélectionne aléatoirement une entrée dans chaque catégorie et on s’assure que ça n’existe pas dans la BAN.