Update Eigener Parser
parent
82154b1486
commit
c4f1894872
@ -1,14 +1,14 @@
|
|||||||
# Warum ein Parser?
|
# Warum ein Parser?
|
||||||
Zunächst stellt sich die Frage, warum wird ein programmatischer Parser benötigt? Zu Beginn war dies zwar abzusehen, aber nicht zwingend notwendig. Nachdem allerdings die ersten Testläufe durchgeführt wurden, wurde schnell klar, dass es viel Arbeit wird, alles auszuwerten. Wir haben uns gemeinsam die Logdateien der einzelnen Tools angesehen, und versucht Metriken und Kategorien zu definieren. Alles in allem haben wir aus den drei Tools etwa 75, mehr oder weniger interessante, Kategorien definiert. Multipliziert man die Anzahl der Kategorien nun mit der Anzahl der Durchläufe, wird die Datenmenge besser ersichtlich. Aus 56 Durchläufen, aus denen je 75 Kategorien extrahiert werden (teilweise hatten Kategorien auch einen zusammengesetzten Wert) erhält man ca. 4200 Tabellen Zellen, die von Hand befüllt werden müssten. Dies ist ein enormer Zeitaufwand und sehr fehlerträchtig. Daher blieb nur die Lösung durch einen programmatischen Ansatz.
|
Zunächst stellt sich die Frage, warum wird ein programmatischer Parser benötigt? Zu Beginn war dies zwar abzusehen, aber nicht zwingend notwendig. Nachdem allerdings die ersten Testläufe durchgeführt wurden, wurde schnell klar, dass es viel Arbeit wird, alles auszuwerten. Wir haben uns gemeinsam die Logdateien der einzelnen Tools angesehen, und versucht, Metriken und Kategorien zu definieren. Alles in allem haben wir aus den drei Tools etwa 75, mehr oder weniger interessante, Kategorien definiert. Multipliziert man die Anzahl der Kategorien nun mit der Anzahl der Durchläufe, wird die Datenmenge besser ersichtlich. Aus 56 Durchläufen, aus denen je 75 Kategorien extrahiert werden, (teilweise hatten Kategorien auch einen zusammengesetzten Wert) erhält man ca. 4200 Tabellen Zellen, die von Hand befüllt werden müssten. Dies ist ein enormer Zeitaufwand und sehr fehlerträchtig. Daher blieb nur die Lösung durch einen programmatischen Ansatz.
|
||||||
|
|
||||||
# Umsetzung
|
# Umsetzung
|
||||||
## Technologieauswahl
|
## Technologieauswahl
|
||||||
Eine Art Parser schien die beste Möglichkeit zu sein, da alle Ausgabedateien von anderer Software generiert wurden. Aufgrund dieser Prämisse könnten sie bestimmt auch wieder einfach eingelesen und verarbeitet werden.
|
Eine Art Parser schien die beste Möglichkeit zu sein, da alle Ausgabedateien von anderer Software generiert wurden. Aufgrund dieser Prämisse könnten sie bestimmt auch wieder einfach eingelesen und verarbeitet werden.
|
||||||
|
|
||||||
Als Sprache war hier Python die erste Wahl. Python bietet sich enorm gut dafür an, da es nicht nötig ist, feste Datenstrukturen zu erstellen. Alle Typen sind dynamisch. Weiter war es nicht wichtig, ob die Laufzeit optimal ist. Ob der Parser nun 4 Sekunden oder 10 benötigt war nicht relevant.
|
Als Sprache war hier Python die erste Wahl. Python bietet sich enorm gut dafür an, da es nicht nötig ist, feste Datenstrukturen zu erstellen. Alle Typen sind dynamisch. Weiter war es nicht wichtig, ob die Laufzeit optimal ist. Ob der Parser nun 4 Sekunden oder 10 benötigt, war nicht relevant.
|
||||||
|
|
||||||
## Stage 1: Präparieren der Eingabedateien
|
## Stage 1: Präparieren der Eingabedateien
|
||||||
Durch die systematische Vorarbeit, die unser Script in Bezug auf die Verzeichnisstruktur erledigt hat, lagen alle Ergebnisse in der gleichen Struktur vor. Die einzige Schwierigkeit war es nun noch, jedem Test einen eindeutigen Namen zu geben, der Programmatisch verarbeitet werden kann. Folgendes Schema erschien uns hier für sinnvoll.
|
Durch die systematische Vorarbeit, die unser Script in Bezug auf die Verzeichnisstruktur erledigt hat, lagen alle Ergebnisse in der gleichen Struktur vor. Die einzige Schwierigkeit war es nun noch, jedem Test einen eindeutigen Namen zu geben, der programmatisch verarbeitet werden kann. Folgendes Schema erschien uns hier sinnvoll.
|
||||||
```json
|
```json
|
||||||
{GLOBAL_ID}_{VENDOR}_{SYSTEM}_{VERSION}
|
{GLOBAL_ID}_{VENDOR}_{SYSTEM}_{VERSION}
|
||||||
```
|
```
|
||||||
@ -61,7 +61,7 @@ Hieraus ergibt sich dann die folgende Dateistruktur (Ausschnittsweise):
|
|||||||
Insgesamt ergeben sich dann über alle Scans und Tests ca. 30 Dateien pro Test auf 18 Tests etwa 540 Logfiles. Zu finden sind alle Dateien im Repository unter "raw_scans".
|
Insgesamt ergeben sich dann über alle Scans und Tests ca. 30 Dateien pro Test auf 18 Tests etwa 540 Logfiles. Zu finden sind alle Dateien im Repository unter "raw_scans".
|
||||||
|
|
||||||
## Stage 2: Einlesen der Logs
|
## Stage 2: Einlesen der Logs
|
||||||
Die zweite Stufe ist dann die erste programmatische Arbeit. Der Parser liest über einen glob-Befehl das Verzeichnis aus, indem die Scans liegen. Danach wird das oben erklärte Benennungsschema aus dem Namen extrahiert. Ist der Parsevorgang erfolgreich (was er immer ist), wird ein neues Element in die liste aller Tests gelegt.
|
Die zweite Stufe ist dann die erste programmatische Arbeit. Der Parser liest über einen glob-Befehl das Verzeichnis aus, in dem die Scans liegen. Danach wird das oben erklärte Benennungsschema aus dem Namen extrahiert. Ist der Parsevorgang erfolgreich (was er immer ist), wird ein neues Element in die Liste aller Tests gelegt.
|
||||||
```python
|
```python
|
||||||
list_of_all = []
|
list_of_all = []
|
||||||
all_scans = glob.glob(os.path.join(BASE_SCAN_PATH, "*", ""))
|
all_scans = glob.glob(os.path.join(BASE_SCAN_PATH, "*", ""))
|
||||||
@ -118,11 +118,11 @@ Das Herzstück des Parsers ist die obige Schleife, sie durchläuft alle Runs und
|
|||||||
|
|
||||||
### Stage 3.1: Lynis
|
### Stage 3.1: Lynis
|
||||||
|
|
||||||
Da Lynis den umfangreichsten Report liefert haben wir hier auch fast alle Informationen gespeichert, die verfügbar waren. Alle Informationen wurden aus dem Konsolenoutput geparsed, da die Logdatei sehr viel unwichtiges entielt und die DAT Datei zu wenig Informationen gespeichert hat.
|
Da Lynis den umfangreichsten Report liefert, haben wir hier auch fast alle Informationen gespeichert, die verfügbar waren. Alle Informationen wurden aus dem Konsolenoutput geparsed, da die Logdatei sehr viel Unwichtiges entielt und die `DAT`-Datei zu wenig Informationen gespeichert hat.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
LYNIS_REGEX = {
|
LYNIS_REGEX = {
|
||||||
"green": r"\[1;32m", # more advanced "green": r"\[ .*\[1;32m([\w\d ]*).*",
|
"green": r"\[1;32m",
|
||||||
"yellow": r"\[1;33m",
|
"yellow": r"\[1;33m",
|
||||||
"red": r"\[1;31m",
|
"red": r"\[1;31m",
|
||||||
"white": r"\[1;37m",
|
"white": r"\[1;37m",
|
||||||
@ -130,7 +130,7 @@ LYNIS_REGEX = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Da der Konsolenoutput mit Farben arbeit, war es für uns sehr einfach nach Farben zu filtern. In obigem Auschnitt sieht man die Definitionen der Regular Expressions der benutzen Matcher.
|
Da der Konsolenoutput mit Farben arbeit, war es für uns sehr einfach, nach Farben zu filtern. In obigem Auschnitt sieht man die Definitionen der Regular Expressions der benutzen Matcher.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
LYNIS_BLOCKS = {
|
LYNIS_BLOCKS = {
|
||||||
@ -163,16 +163,16 @@ for block in blocks:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Um nun zu parsen, wurde das Logfile an jeder Kapitelüberschrift geteilt und die darin enthaltenen Anzahlen an grünen, gelben, roten und weißen angaben gespeichert.
|
Um nun zu parsen, wurde das Logfile an jeder Kapitelüberschrift geteilt und die darin enthaltenen Anzahlen an grünen, gelben, roten und weißen Angaben gespeichert.
|
||||||
|
|
||||||
Weiter wurden noch der Warning Count, Suggestion count, Hardening Index und die Anzahl an ausgeführen Tests gespeichert.
|
Weiter wurden noch der Warning Count, Suggestion count, Hardening Index und die Anzahl an ausgeführen Tests gespeichert.
|
||||||
|
|
||||||
Nach dem generellen step wurden dann noch spezielle props aus den obigen Blocks extrahiert, dies ginge aber zu weit, genaueres kann direkt aus dem Quellcode in der Datei [lynis.py](https://gitlab.com/marcel.schwarz/it-security-2/-/blob/master/scan_output_parser/lynis.py) herausgelesen werden.
|
Nach dem generellen step wurden dann noch spezielle props aus den obigen Blocks extrahiert, dies ginge aber zu weit, genaueres kann direkt aus dem Quellcode in der Datei [lynis.py](https://gitlab.com/marcel.schwarz/it-security-2/-/blob/master/scan_output_parser/lynis.py) herausgelesen werden.
|
||||||
|
|
||||||
### Stage 3.2: Testssl
|
### Stage 3.2: Testssl
|
||||||
Der TestSSL Parser ist einer der leichtesten zu implementierenden Parser gewesen. Die Logfile musste lediglich bei jedem neuen Scan geteilt werden und dann nach bestimmten Keywords ausschau gehalten werden.
|
Der TestSSL Parser ist einer der am einfachsten zu implementierenden Parser gewesen. Die Logfile musste lediglich bei jedem neuen Scan geteilt werden und dann nach bestimmten Keywords Ausschau gehalten werden.
|
||||||
|
|
||||||
Leider haben wir erst im Nachhinein bemerkt, dass es die Möglichkeit gegeben hätte, als Output-Format `JSON` anzugeben, was die programmatische verarbeitung noch einfacher gemacht hätte. So hatten wir nur eine Textdatei.
|
Leider haben wir erst im Nachhinein bemerkt, dass es die Möglichkeit gegeben hätte, als Output-Format `JSON` anzugeben, was die programmatische Verarbeitung noch einfacher gemacht hätte. So hatten wir nur eine Textdatei.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def testssl_parse(path):
|
def testssl_parse(path):
|
||||||
@ -205,7 +205,7 @@ def testssl_parse(path):
|
|||||||
continue
|
continue
|
||||||
return testssl_overall
|
return testssl_overall
|
||||||
```
|
```
|
||||||
Der Parser Arbeitet mit einem festen Array an Werten, die durch unser Run-Skript vorgegeben waren. Während des Parsens wird dann nur noch der entsprechende Boolean auf `True` gesetzt, sobald das Keyword gefunden wurde. Der oben genannte Trenner für die Tests wurde als "## Scan startet as: " gewählt.
|
Der Parser arbeitet mit einem festen Array an Werten, die durch unser Run-Skript vorgegeben waren. Während des Parsens wird dann nur noch der entsprechende Boolean auf `True` gesetzt, sobald das Keyword gefunden wurde. Der oben genannte Trenner für die Tests wurde als "## Scan startet as: " gewählt.
|
||||||
|
|
||||||
### Stage 3.3: Otseca
|
### Stage 3.3: Otseca
|
||||||
|
|
||||||
@ -227,7 +227,7 @@ def otseca_box_counts(path_to_report):
|
|||||||
return counts
|
return counts
|
||||||
```
|
```
|
||||||
|
|
||||||
Außerdem wurden weitere Betriebssystem-Infos, geholt welche in Lynis nicht verfügbar waren. Dazu gehören hauptsächlich die Gesamtanzahl Pakete, sowie die entsprechenden Counts für "Upgraded", "Newly Installed", "Remove" und "Not Upgraded".
|
Außerdem wurden weitere Betriebssystem-Infos geholt, welche in Lynis nicht verfügbar waren. Dazu gehören hauptsächlich die Gesamtanzahl Pakete, sowie die entsprechenden Counts für `Upgraded`, `Newly Installed`, `Remove` und `Not Upgraded`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def otseca_distro_info(path_to_report):
|
def otseca_distro_info(path_to_report):
|
||||||
@ -253,12 +253,12 @@ with open("export.json", "w") as handle:
|
|||||||
handle.write(Run.schema().dumps(list_of_all, many=True))
|
handle.write(Run.schema().dumps(list_of_all, many=True))
|
||||||
```
|
```
|
||||||
|
|
||||||
Zuerst wollten wir alles als JSON Objekt ausgeben, dies stellte sich aber sehr schell als unübersichtlich heraus. Die Resultierende JSON-Datei hatte für alle 56 Durchläufe etwa 19.000 Zeilen.
|
Zuerst wollten wir alles als JSON Objekt ausgeben, dies stellte sich aber sehr schell als unübersichtlich heraus. Die resultierende JSON-Datei hatte für alle 56 Durchläufe etwa 19.000 Zeilen.
|
||||||
|
|
||||||
Also Entschieden wir uns noch für eine andere Art der Ausgabe, eine strukturiertere, SQLite.
|
Also entschieden wir uns noch für eine andere Art der Ausgabe, eine strukturiertere, SQLite.
|
||||||
|
|
||||||
### Stage 4.2: SQLite-Ausgabe
|
### Stage 4.2: SQLite-Ausgabe
|
||||||
SQlite bietet sich hierfür sehr gut an, da es sich um eine serverlose strukturierte Datenbank handelt. Weiter gibt es uns auch zur Auswertung ganz neue Möglichkeiten durch die Datenbankfunktionen. Außerdem bietet Python eine native implementation von SQlite, ohne das Zusatzpakete benötigt werden.
|
SQlite bietet sich hierfür sehr gut an, da es sich um eine serverlose strukturierte Datenbank handelt. Weiter gibt es uns auch zur Auswertung ganz neue Möglichkeiten durch die Datenbankfunktionen. Außerdem bietet Python eine native Implementation von SQlite, ohne dass Zusatzpakete benötigt werden.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
for run in list_of_all:
|
for run in list_of_all:
|
||||||
@ -289,11 +289,11 @@ def write_run_to_db(run):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
```
|
```
|
||||||
|
|
||||||
In dieser Funktion werden dann in vier Tabellen alle gesamelten Informationen gespeichert die Tabellen umfassen zwischen fünf und 37 Spalten. Jeder Run wird hierbei in die `Run` Tabelle gespeichert. Außerdem bekommt jedes Tool ebenfalls seine eigene Tabelle (Lynis, TestSSL und Otseca). Die Tabellen von den Tools werden dann nach und nach gefüllt, und über Fremdschlüssen mit der Primärtabelle verbunden. Zuletzt wird die Transaktion dann geschrieben.
|
In dieser Funktion werden dann in vier Tabellen alle gesamelten Informationen gespeichert, die Tabellen umfassen zwischen fünf und 37 Spalten. Jeder Run wird hierbei in der `Run` Tabelle gespeichert. Außerdem bekommt jedes Tool ebenfalls seine eigene Tabelle (Lynis, TestSSL und Otseca). Die Tabellen von den Tools werden dann nach und nach gefüllt, und über Fremdschlüssel mit der Primärtabelle verbunden. Zuletzt wird die Transaktion dann geschrieben.
|
||||||
|
|
||||||
# Ergebnisse
|
# Ergebnisse
|
||||||
Insgesamt lässt sich sagen, dass etwa 30 Regular Expressions nötig waren, um alle gewollten Logfiles über alle Tools zu parsen. Die Laufzeit beträgt hierbei etwa 10 Sekunde, wobei das Einlesen der doch relativ langen Textdateien am längsten benötigt.
|
Insgesamt lässt sich sagen, dass etwa 30 Regular Expressions nötig waren, um alle gewollten Logfiles über alle Tools zu parsen. Die Laufzeit beträgt hierbei etwa 10 Sekunden, wobei das Einlesen der doch relativ langen Textdateien am längsten benötigt.
|
||||||
|
|
||||||
Der Export als SQlite hat sich im Nachhinein als sehr Vorteilhaft entpuppt, da SQlite nativ mit JSON Daten umgehen kann, was sehr leichte Abfragemechanismen erlaubt.
|
Der Export als SQlite hat sich im Nachhinein als sehr vorteilhaft entpuppt, da SQlite nativ mit JSON Daten umgehen kann, was sehr leichte Abfragemechanismen erlaubt.
|
||||||
|
|
||||||
Weiter wurden sogut wie alle menschlichen Fehler, die durch Unaufmerksamkeit verschultet wären, ausgeschlossen. Auch Ablese- oder Tippfehler sind kein Problem.
|
Weiter wurden sogut wie alle menschlichen Fehler, die durch Unaufmerksamkeit auftreten würden, ausgeschlossen. Auch Ablese- oder Tippfehler sind kein Problem.
|
Loading…
Reference in New Issue
Block a user