Workaround zum Parsen von JSON Objekten in Arrays mit Wazuh-Decodern

Tobias Roggenhofer

Disclaimer

  • Die im nachfolgenden beschriebene Methode kann nur dann angewendet werden, wenn die Feldnamen innerhalb der Objekte stets in der gleichen Reihenfolge enthalten sind und die maximale Menge an Objekten im Array bekannt ist.
  • Ein Decoder ist nicht universell anwendbar, sodass für jedes unterschiedliche Array ein separater Decoder benötigt wird.

Was ist geschehen

Vor einiger Zeit sind wir auf das Problem gestoßen, dass auf einer unserer Wazuh Instanzen einige Daten innerhalb eines JSON Logs nicht herausgeparst werden konnten. So enthält ein Feld sämtliche Daten eines Arrays mit Objekten. Da es mittels des Standard Wazuh JSON Decoders nicht möglich ist, diese Daten in eigene Felder zu indizieren, haben wir folgenden Workaround erstellt, um diese Daten dennoch parsen zu können.

Kurze Einführung in Wazuh-Decoder

Wazuh Decoder funktionieren immer nach einem 2 stufigen Prinzip. In Stufe 1, dem Parent Decoder, wird überprüft, ob das Log einem bestimmten Format entspricht. In Stufe 2 werden die konkreten Felder aus dem Log extrahiert. Es ist möglich, beliebig viele Decoder zu einem Parent zu erstellen.

Das Matching in beiden Stufen erfolgt, sofern keine Plugins verwendet wurden, stets mit Hilfe von Regular Expressions.

Da die Standard Matching Language (OS Regex Syntax) bei den Decodern keine  vollständige Regex Logik ermöglicht, verwenden wir stets die “PCRE2 Syntax”.  Hierzu muss lediglich im Regex Tag “pcre2” als Typ angelegt werden (<regex type="pcre2">).

Unser Vorgehen

Da in unserem Fall die Felder in den Objekten im Array stets in der gleichen Reihenfolge übertragen wurden, ist es möglich, mit Hilfe von RegEx zu überprüfen ob die Felder in den Objekten übereinstimmen. Daher konnten wir einen eigenen Wazuh Decoder für unseren Anwendungsfall erstellen. Unser weiteres Vorgehen wird basierend auf dem folgenden Beispiel Log demonstriert:

{"datetime": "04/28/2024 13:13:40.664","cpu_usage_percent": "27.3474924","disk_bytes_written_per_second_avg": "23837.88832487309628", "disk_bytes_read_per_second_avg": "159616", "network":[{"interface-name": "wlan0", "network_bytes_sent_sec_avg": "2978.984211383679849", "network_bytes_received_sec_avg": "23837.88832487309628", "ip-address": "192.168.2.110"}, {"interface-name": "eth0", "network_bytes_sent_sec_avg": "0", "network_bytes_received_sec_avg": "0", "ip-address": ""}, {"interface-name": "docker0", "network_bytes_sent_sec_avg": "0", "network_bytes_received_sec_avg": "0", "ip-address": ""}], "gpu_usage_percent" : "16.093921168400814992"}

Zunächst haben wir deshalb einen neuen Decoder angelegt.  Als nächstes haben wir dem Decoder einen eindeutigen Namen gegeben.

<decoder name="test_decoder">

   <program_name>test_decoder</program_name>

</decoder>

Als nächstes haben wir zunächst die Grundstruktur des neuen Decoders angelegt. Bei diesem muss als Parent der JSON-Decoder eingetragen sein. Dies ist wichtig, da wir so sämtliche Logs im JSON Format zu unserem Decoder umleiten können.

<decoder name="test_target">

   <parent>json</parent>

   <regex type="pcre2">...</regex>

   <order>...</order>

</decoder>

Bevor wir auf den genauen Aufbau des neuen Decoders für unseren Spezialfall eingehen, müssen wir  vorher noch dafür sorgen, dass auch alle anderen Felder unseres Spezialfalls wie gehabt geparst werden können. Deshalb muss nach unserem neuen Decoder noch ein weiterer Decoder mit JSON Plugin hinzugefügt werden.

Unsere Tests haben ergeben, dass unser eigener JSON Decoder nur dann wie erwartet funktioniert, wenn der JSON Plugin Decoder auch tatsächlich als letzter Decoder eingebaut wird. Geschieht dies nicht, werden zwar die eignen, mittels RegEx gesuchten Felder aus dem JSON Log extrahiert, der Standard Wazuh JSON Decoder kommt jedoch nicht zur Anwendung.

<decoder name="test_target">

   <parent>json</parent>

   <plugin_decoder>JSON_Decoder</plugin_decoder>

</decoder>

Für den Bau des Decoders schauen wir uns zunächst mal den Aufbau der Logs an. (Aus Gründen der Darstellung und Übersichtlichkeit haben wir das folgende Log Beispiel formatiert.)

{

"datetime":"04/28/2024 13:13:40.664",

"cpu_usage_percent": "27.3474924",

"disk_bytes_written_per_second_avg": "23837.88832487309628",

"disk_bytes_read_per_second_avg": "159616",

"network":[

{

"interface-name": "wlan0",

"network_bytes_sent_sec_avg": "2978.984211383679849",

"network_bytes_received_sec_avg": "23837.88832487309628",

"ip-address": "192.168.2.110"

}, {

"interface-name": "eth0",

"network_bytes_sent_sec_avg": "0",

"network_bytes_received_sec_avg": "0",

"ip-address": ""

}, {

"interface-name": "docker0",

"network_bytes_sent_sec_avg": "0",

"network_bytes_received_sec_avg": "0",

"ip-address": ""

}

],

"gpu_usage_percent" : "16.093921168400814992"

}

Zunächst sieht man hier, dass Objekte innerhalb des “network” Arrays liegen. Außerdem ist zu erkennen, dass die Objekte stets gleich aufgebaut sind.

Deshalb haben wir zunächst die Regular Expression für ein Objekt erstellt.

Hinweis: Um Komplikationen in späteren Ausbauschritten zu vermeiden, haben wir an manchen Stellen bereits Lazy-Reading Pattern verwendet, da es sonst passieren kann, dass in ein Feld mehr reingeladen wird als erwünscht.

Achtung: Die Regex muss so geschrieben sein, dass sie auf das unformatierte Log passt. (So wie sie auch am Decoder ankommt)

{"interface-name": "([A-Za-z0-9]*?)", "network_bytes_sent_sec_avg": "([0-9]*\.?[0-9]*)", "network_bytes_received_sec_avg": "([0-9]*\.?[0-9]*)", "ip-address": "(\d{1,3}\.?[0-9]{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3})"\}

Anschließend haben wir den Anfang des Arrays als Präfix hinzugefügt. Dadurch ergibt sich folgendes Regex Pattern.

, "network":\[\{"interface-name": "([A-Za-z0-9]*?)", "network_bytes_sent_sec_avg": "([0-9]*\.?[0-9]*)", "network_bytes_received_sec_avg": "([0-9]*\.?[0-9]*)", "ip-address": "(\d{1,3}\.?[0-9]{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3})"\}

Dieses kann auch bereits getestet und verwendet werden und sollte bereits die Felder des ersten Objekts extrahieren können. Der Order-Bereich würde hierfür wie folgt aussehen:

<order>network.0.interface-name, network.0.bytes_sent_sec_avg, network.0.bytes_received_sec_avg, network.0.ip-address</order>

Für alle weiteren Objekt fügen wir beginnend mit einem “, “ das gleiche Regex-Pattern hinzu, setzen dies jedoch in runde Klammern, gefolgt von einem Fragezeichen, um zu signalisieren, dass der Inhalt 1 oder 0 mal vorkommen kann. Mit einem weiteren Objekt würde das Regex Pattern dann wie folgt aussehen

, "network":\[\{"interface-name": "([A-Za-z0-9]*?)", "network_bytes_sent_sec_avg": "([0-9]*\.?[0-9]*)", "network_bytes_received_sec_avg": "([0-9]*\.?[0-9]*)", "ip-address": "(\d{1,3}\.?[0-9]{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3})"\}(, \{"interface-name": "([A-Za-z0-9]*?)", "network_bytes_sent_sec_avg": "([0-9]*\.?[0-9]*)", "network_bytes_received_sec_avg": "([0-9]*?\.?[0-9]*?)", "ip-address": "(\d{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3})"\})?

In unserem Fall hat es nicht funktioniert. das “?” bei den Gruppen durch andere RegEx typische Quantifier (z.B. +, *, konkrete Häufigkeitsangaben in geschweiften Klammern) zu ersetzen.  Aus diesem Grund müssen wir Stand heute das Regex-Pattern für das Objekt so oft hintereinander setzen, wie Objekte im Array vorkommen können.

Der Order Bereich für unser Beispiel mit zwei Objekten sieht dann wie folgt aus. Zu beachten ist hier, dass für den optionalen Bereich aufgrund der runden Klammern noch ein zusätzliches Feld erstellt wird.

<order>network.0.interface-name, network.0.bytes_sent_sec_avg, network.0.bytes_received_sec_avg, network.0.ip-address, network.1.all, network.1.interface-name, network.1.bytes_sent_sec_avg, network.1.bytes_received_sec_avg, network.1.ip-address</order>

Alle Informationen zusammengefasst, erhalten wir somit die nachfolgende Konfiguration unseres Decoders für Logs mit maximal 3 Objekten im Array.

<decoder name="test_decoder">

   <program_name>test_decoder</program_name>

</decoder>

<decoder name="test_target">

   <parent>json</parent>

   <regex type="pcre2">, "network":\[\{"interface-name": "([A-Za-z0-9]*?)", "network_bytes_sent_sec_avg": "([0-9]*\.?[0-9]*)", "network_bytes_received_sec_avg": "([0-9]*\.?[0-9]*)", "ip-address": "(\d{1,3}\.?[0-9]{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3})"\}(, \{"interface-name": "([A-Za-z0-9]*?)", "network_bytes_sent_sec_avg": "([0-9]*\.?[0-9]*)", "network_bytes_received_sec_avg": "([0-9]*?\.?[0-9]*?)", "ip-address": "(\d{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3})"\})?(, \{"interface-name": "([A-Za-z0-9]*?)", "network_bytes_sent_sec_avg": "([0-9]*\.?[0-9]*)", "network_bytes_received_sec_avg": "([0-9]*\.?[0-9]*)", "ip-address": "(\d{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3}\.?[0-9]{0,3})"\})?]</regex>

   <order>network.0.interface-name, network.0.bytes_sent_sec_avg, network.0.bytes_received_sec_avg, network.0.ip-address, network.1.all, network.1.interface-name, network.1.bytes_sent_sec_avg, network.1.bytes_received_sec_avg, network.1.ip-address, network.2.all, network.2.interface-name, network.2.bytes_sent_sec_avg, network.2.bytes_received_sec_avg, network.2.ip-address</order>

</decoder>

<decoder name="test_target">

   <parent>json</parent>

   <plugin_decoder>JSON_Decoder</plugin_decoder>

</decoder>

Referenzen