Update Eigener Parser

Marcel Schwarz 2021-02-05 15:37:33 +00:00
parent 244670a69b
commit d5f59c6020

@ -1,8 +1,104 @@
# 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.
# Umsetzung
## 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.
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
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.
```json
{GLOBAL_ID}_{VENDOR}_{SYSTEM}_{VERSION}
```
Also beispielsweise:
```
10_gcp_ubuntu_16.04
2_hetzner_ubuntu_16.04
```
Diese Benennung wurde von Hand durchgeführt.
Hieraus ergibt sich dann die folgende Dateistruktur (Ausschnittsweise):
```
9_gcp_ubuntu_20.04
├── lynis-console-1.log
├── lynis-console-2.log
├── lynis-console-3.log
├── lynis-log-1.log
├── lynis-log-2.log
├── lynis-log-3.log
├── lynis-report-1.dat
├── lynis-report-2.dat
├── lynis-report-3.dat
├── otseca-1
│   └── report.1610030682
│   ├── kernel.all.log.html
│   ├── network.all.log.html
│   ├── permissions.all.log.html
│   ├── services.all.log.html
│   ├── system.all.log.html
│   └── vendor
├── otseca-2
│   └── report.1610031105
│   ├── kernel.all.log.html
│   ├── network.all.log.html
│   ├── permissions.all.log.html
│   ├── services.all.log.html
│   ├── system.all.log.html
│   └── vendor
├── otseca-3
│   └── report.1610032314
│   ├── kernel.all.log.html
│   ├── network.all.log.html
│   ├── permissions.all.log.html
│   ├── services.all.log.html
│   ├── system.all.log.html
│   └── vendor
├── testssl-1.log
├── testssl-2.log
└── testssl-3.log
```
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
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.
```python
list_of_all = []
all_scans = glob.glob(os.path.join(BASE_SCAN_PATH, "*", ""))
for scan in all_scans:
findings = re.findall(r"(\d+)_(.*)_(.*)_(.*)", os.path.dirname(scan))
findings = findings[0]
list_of_all.append(
Run(findings[0], findings[1], findings[2], findings[3], scan, [], [], [])
)
```
Die hier verwendete "Run" Datenstruktur ist eine von zwei Datenstrukturen, die wir angelegt haben, um den späteren Export zu erleichtern. Zum einen wäre hier der "Run" Datentyp:
```python
@dataclass_json
@dataclass
class Run:
id: int
platform: str
system: str
version: str
path: str
otseca_results: List[Result]
lynis_results: List[Result]
testssl_results: List[Result]
```
Er speichert alle Informationen zu einem Test und zu jedem Tool eine Liste an "Results". "Results" ist hierbei der zweite eigene Datentyp:
```python
@dataclass_json
@dataclass
class Result:
path: str
run_nr: int
result: dict
```
Jedes Result bildet hierbei einen einzelnen Durchlauf eines Tools ab, z.B. "otseca-1" wäre ein "Result" Objekt oder aber auch "lynis-3".
Weiter haben beide Klassen den `@dataclass` und `@dataclass_json` Decorator. Dieser sorgt dafür, dass das Objekt später leicht serialisiert werden kann und erspart Boiler-Plate Code.
## Stage 3: Parsen der Daten
### Stage 3.1: Lynis
### Stage 3.2: Testssl