My Little Helpers: pseudonymize: filter for replacing person-identifiable data
Ziel
- Ich habe das Konzept der Pseudonymisierung verstanden und selbst umgesetzt.
- Ich habe einen einfachen Parser programmiert, um reguläre Ausdrücke programmatisch umzuschreiben.
- Ich habe Suchen-und-Ersetzen durchgeführt, bei dem die Ersetzung datenabhängig durch eine Funktion vorgenommen wird.
Hintergrund
Laut EU-DSGVO (engl.: EU-GDPR) unterliegen personenbezogene Daten einem starken Schutz. Deshalb will man oft Logfiles nur in einer solchen Form an andere Personen weiterreichen, dass darin kein Personenbezug mehr zu erkennen ist. Aber wenn man die personenbezogenen Daten einfach nur löscht, sind viele Zusammenhänge innerhalb des Logfiles nicht mehr zu erkennen (dass also hier oben die gleiche Person gestanden hat wie dort unten). Deshalb sollte man Personenbezüge einheitlich durch entsprechende eindeutige Pseudonyme ersetzen.
Im konkreten Einzelfall ist so etwas zwar nicht schwierig zu programmieren, macht aber viel Arbeit. Pseudonymisierung umgekehrt als ganz allgemein für alle Zwecke wiederverwendbares Werkzeug zu bauen, ist sehr schwierig. Wir gehen hier einen Mittelweg und bauen uns ein Pseudonymisierungswerkzeug, das geeignet ist für die meisten Arten zeilenbasierter Logfiles.
Arbeitsschritte
Aufrufformat
- Legen Sie die Datei
mlh/mlh/subcmds/pseudonimize.py
an. - Legen Sie darin ein Unterkommando an, das folgende Aufrufe unterstützt:
1 2 |
|
pseudonymize
arbeitet als Filter.
Die Bedeutung der Optionsparameter klären und bauen wir erst im
Teil 2 dieser Aufgabe.
configfile
ist eine Textdatei, in der jede Zeile per regulärem Ausdruck
ein Zeilenformat einer Logdatei beschreibt
und dabei markiert, welche Teile durch Pseudonyme ersetzt werden sollen.
Wirkung des Filters, Format des configfile
pseudonymize
kann zum Beispiel folgende Wirkung haben.
Angenommen, die Eingabe sieht so aus (übertragen Sie diese Daten in die Datei mlh/inputs/login1.log
):
1 2 3 |
|
Dann sieht die Ausgabe u.U. so aus:
1 2 3 |
|
Damit das passiert, sieht die configfile
z.B. so aus
(übertragen Sie diese Daten in die Datei mlh/config/login.pseu
)
1 |
|
Das Ganze ist ein regulärer Ausdruck in der normalen Python-Notation.
(?P<user>\w+)
ist eine benannte Gruppe (named group).
(Wenn Sie sich damit nicht auskennen, bearbeiten Sie jetzt passende Aufgaben
in der Gruppe RegExp, um sich das hier benötigte Grundwissen zuzulegen.)
Der zugehörige Aufruf wäre dann z.B.
1 |
|
An die Implementierung dieser Funktionalität arbeiten wir uns in den nächsten Abschnitten heran.
Implementierung 1: Linetype
-
Schreiben Sie eine Klasse
Linetype
, deren Exemplare je eine Zeile desconfigfile
repräsentieren, mit mindestens folgenden Attributen:orig
: Die Originalzeile aus dem Configfile (ohne das abschließende Newline)rewritten
: Eine Version vonorig
, die so umgeschrieben ist, wie sie später beim Ablauf benutzt werden soll.replacement
: Ein Deskriptor, der beschreibt, wie aus dem Regexp-Treffer zurewritten
die Ausgabezeile erzeugt wird.
-
Der Konstruktor erhält
orig
übergeben und berechnet darausrewritten
(auch ein regulärer Ausdruck) undreplacement
(eine Liste, deren Format sich aus den Überlegungen unten ergibt). -
Für die Verfahrensweise gelten folgende Überlegungen:
- R1: The replacement string must be able to refer to everything in orig, so we must enclose all toplevel stuff except parens in artificial parens.
- R2, R3: The replacement is a sequence of references to unnamed groups (R2) and pseudonym-replacement groups (R3), nothing else.
- To create it, we count groups and emit
R4, R5: group numbers when top-level groups open (whether original (R4) or artificial (R5))
R6: modified group names when top-level named groups open. - R7, R8: We copy unnamed inner-level groups (R7) and complain about named inner-level groups (R8).
- R9: Toplevel '|' is also not allowed, because its replacement would be unclear.
- R10: For simplicity, we forbid backreferences to named groups.
-
Schreiben Sie einen entsprechenden Konstruktor. Benutzen Sie die nachfolgenden Hinweise, wenn Sie nicht weiterkommen.
Hinweis (nur bei Bedarf): Welche Grundstruktur hat der Konstruktor?
Der Konstruktor geht Stück für Stück (meistens Zeichen für Zeichen)
von links nach rechts durch orig
und konstruiert unterwegs rewritten
und replacement
.
Hinweis (nur bei Bedarf): Welche Fälle sind zu bearbeiten?
Sie brauchen getrennte Logik für
(
, )
, \
, |
und alle übrigen Zeichen.
Die meiste Logik hängt am Fall (
.
Den Marker ?P<groupname>
für eine benannte Gruppe sollte man nicht zeichenweise bearbeiten,
sondern "in einem Rutsch" mit einem regulären Ausdruck.
Hinweis (nur bei Bedarf): Welchen zusätzlichen Zustand braucht man für diese Logik?
Für R1 und R4 bis R9 müssen Sie die Verschachtelungstiefe von Klammern mitzählen.
Für R1 und R9 müssen Sie mitzählen, ob die zusätzliche "künstliche" Klammer auf
der obersten Eben gerade geöffnet ist oder nicht.
Für R2 und R3 müssen Sie mitzählen, wie viele Regexp-Gruppen schon gebildet wurden.
Hinweis (nur bei Bedarf): Wie sollte man die Logik in der Programmstruktur abbilden?
- Obige Zustandsvariablen könnten z.B. heißen
paren_level: int
,artificial_paren_open: bool
,groups: int
. - Es kann bequem sein diese (und weitere) Zustandsvariable in
self
einzutragen, damit die Übergabe an Unterprogramme nicht zu umständlich wird. - Achten Sie darauf, Redundanz in ihrem Code zu vermeiden, sondern stets passende Hilfsfunktionen einzuführen. Das erleichtert die Korrektur Ihrer kaum vermeidlichen Programmierfehler sehr.
- Stellen Sie sich darauf ein, für den Konstruktur ca. 100 Programmzeilen zu brauchen.
Tests dazu
- Legen Sie die pytest-Datei
mlh/tests/test_pseudonymize.py
an. - Schreiben Sie darin 2-4 Tests, die sich für je einen Fall davon überzeugen,
dass für
Linetype(line_from_configfile)
inrewritten
undreplacement
das Erwartete herauskommt. - 1 Zeigen Sie einen erfolgreichen Aufruf von
pytest -v mlh/tests/test_pseudonymize.py
Implementierung 2: Pseudonymizer
-
Schreiben Sie eine Klasse
Pseudonymizer
, deren Exemplare je eine Zeile desconfigfile
repräsentieren, mit mindestens folgenden Attributen:linetypes
: Liste derLinetype
-Objekte, die dasconfigfile
repräsentieren.pseudonyms
: Abbildung von einem Originalstring auf sein Pseudonymname_counter
: Abbildung vom Namen einer Pseudonymklasse (Name der benannten Gruppe imconfigfile
) auf die Anzahl von Vorkommen, die es davon bislang gegeben hat.
-
Schreiben Sie dort die Methode
pseudonymize(line: str) -> str
, die dieLinetypes
durchgeht auf der Suche nach einem passenden, damit eine Ersetzung durchführt und das Ergebnis zurückgibt.
Hinweis (nur bei Bedarf): Wie geht eine Ersetzung ohne feste Ersetzungs-Regexp?
Erzeugen Sie ein Match-Objekt mit re.fullmatch(linetype.rewritten, line)
.
Holen Sie daraus die nötigen Teile gemäß linetype.replacement
.
Eine passende Signatur für eine Ersetzungsfunktion könnte lauten
_replace(linetype: Linetype, name_counter: collections.Counter, pseudonyms: dict[str, str], match: re.Match) -> str
.
Tests dazu
- Schreiben Sie in
mlh/tests/test_pseudonymize.py
ein paar Tests, die sich davon überzeugen, dassPseudonymizer.pseudonymize()
für einen Input den korrekten Output liefert. - 2 Zeigen Sie einen erfolgreichen Aufruf von
pytest -v mlh/tests/test_pseudonymize.py
Implementierung 3: execute()
Schreiben Sie nun die Routine execute
, die den configfile
liest und daraus
die Linetype
-Objekte erzeugt, dann den Pseudonymizer
erzeugt
und schließlich jede Eingabezeile liest, pseudonymisiert und wieder ausgibt.
Testen: login1.log
- Führen Sie nun den Test mit
login1.log
(aus der Einleitung) durch. - 1 Sobald der erfolgreich ist, legen Sie
mlh-pseudonymize.md
an.
Tragen Sie eine Überschrift login1.log ein.
Diskutieren Sie die Frage, wie weit Sie Ihrem Programm jetzt trauen: Wird es auch in anderen Fällen korrekt funktionieren? Warum glauben Sie das? In welchen Fällen funktioniert es vielleicht noch nicht? - 2 Checken Sie
mlh-pseudonymize.md
jetzt ein.
Testen: access.log
input1.log
war ein sehr einfaches, fiktives Logformat.
Jetzt schauen wir ein echtes an:
Übertragen Sie folgende Daten in die Datei mlh/input/access.log
.
Dies sind Logdaten im Format eines realen Webservers (Apache httpd):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
- Lesen Sie das Format nach: https://httpd.apache.org/docs/2.4/logs.html
- Zu pseudonymisieren sind dabei die IP-Adressen der anfragenden Hosts (
host
) sowie die Accountnamen bei URLs zu persönlichen Homepages, die also mit etwas wie/~username
beginnen (username
). - Schreiben Sie eine passende Konfigurationsdatei
mlh/config/access.pseu
. - 3 Zeigen Sie einen erfolgreichen Aufruf von
python mlh pseudonymize mlh/config/access.pseu < mlh/input/access.log | grep '/~'
. - 3 Ergänzen Sie
mlh-pseudonymize.md
.
Tragen Sie eine Überschrift access.log ein.
Diskutieren Sie die Frage, wie weit Sie Ihrem Programm jetzt trauen: Wird es auch in anderen Fällen korrekt funktionieren? Warum glauben Sie das? In welchen Fällen funktioniert es vielleicht noch nicht? - 4 Checken Sie
mlh-pseudonymize.md
jetzt ein.
Testen + Implementierung 4: Double Trouble
- Haben Sie daran gedacht, dass die gleiche Pseudonymklasse im selben linetype mehrfach auftreten könnte?
- Übertragen Sie folgende Daten in die Datei
mlh/input/from-to.log
(wieder ein fiktives Format):
1 2 3 |
|
- Beide Namen (hinter
from
wie auch hinterto
) sind Accountnamen und sollen auf die Synonymklasseusername
abgebildet werden. - Schreiben Sie eine passende Konfiguration
mlh/config/from-to.pseu
. - 5 Ergänzen Sie
mlh-pseudonymize.md
.
Tragen Sie eine Überschrift from-to.log ein.
Wird dieser Fall korrekt funktionieren? Warum glauben Sie das? - 6 Checken Sie
mlh-pseudonymize.md
jetzt ein. - 4 Zeigen Sie den erfolgreichen oder erfolglosen Aufruf von
python mlh pseudonymize mlh/config/from-to.pseu < mlh/input/from-to.log
- Korrigieren Sie Ihr Programm, falls nötig.
- 5 Zeigen Sie dann ggf. erstmals einen erfolgreichen Aufruf von
python mlh pseudonymize mlh/config/from-to.pseu < mlh/input/from-to.log
Hinweis (nur bei Bedarf): Was ist mein Problem, wenn es nicht geht?
Ein regulärer Ausdruck kann nicht zwei benannten Gruppen enthalten, die denselben Namen haben.
Hinweis (nur bei Bedarf): Wie löse ich das?
Man muss mitzählen, wie oft diese Synonymklasse im aktuellen Linetype
-Objekt schon gesehen wurde
und diese Zahl als Suffix _1
etc. an den Gruppennamen anhängen.
Zum Nachschlagen der Synonyme muss man das Suffix natürlich wieder entfernen.
Abgabe
Geben Sie ein Kommandoprotokoll ab, das genau nur die Eingaben und Ausgaben der obigen Kommandos 1, 2, … enthält. Entfernen Sie vor Abgabe eventuelle Fehlversuche und sonstige zusätzliche Kommandos aus dem Protokoll.
Geben Sie den Quellcode ab, wie er am Ende der Aufgabe vorliegt.
Checken Sie auch die Logdateien *.log
und die Linetype-Dateien *.pseu
ein.
Geben Sie ein Markdown-Dokument ab mit knappen Antworten zu den oben gestellten Fragen
1, 2, … Geben Sie diese Marker mit an.
Geben Sie ggf. Beispiele oder benutzte Quellen an.