Update Eigener Parser
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
|
||||
|
Loading…
Reference in New Issue
Block a user