W tym artykule chciałbym przybliżyć program jq, który może nam posłużyć do analizy plików .json – w tym między innymi do analizy logów w tym formacie. Jest to pierwszy artykuł z planowanej przeze mnie serii cli-essentials, gdzie postaram się przedstawić przydatne programy w pracy analityka SOC (ale nie tylko) z poziomu terminala.
Kolejnym programem z serii będzie prawdopodobnie popularny cURL. Tymczasem wracamy do naszego głównego tematu, czyli jq. Zachęcam do pobrania przygotowanych przeze mnie logów w formacie .json i własnych ćwiczeń z programem.
Plik z przykładowymi logami SOC użytymi w artykule. Pobierz i ćwicz komendy samodzielnie.
SHA256: 0cc4cb1610b4a3a84f1917ad6cc8bc351cd98fe39978fadd698ba20db720ab44 ✓ 0/61 VT
PODSTAWY
Zacznijmy od pierwszej komendy – prostej kropki, która wykonuje pretty print, czyli wyświetla całą zawartość pliku w czytelnym formacie:
jq '.' soc_data.json
Kolejna komenda pozwala wyciągnąć zawartość konkretnego klucza. Dobra praktyka w dużych plikach jest dodanie sekcji z informacjami o zawartości – poniżej przykład wyciągnięcia klucza metadata, ale może to być równie dobrze info czy header:
jq '.metadata' soc_data.json
Następne komendy pozwolą nam na analizę wybranych indeksów tablicy. Poniższa zwróci każdy alert osobno, jeden po drugim:
jq '.alerts[]' soc_data.json
Aby pobrać konkretny element po indeksie (np. pierwszy), używamy zapisu z nawiasem:
jq '.alerts[0]' soc_data.json
Możemy też wyciągnąć zakres – poniższa komenda zwróci elementy o indeksach 1, 2 i 3:
jq '.alerts[1:4]' soc_data.json
PIPE I BUDOWANIE OBIEKTÓW
Przejdziemy do kolejnej sekcji – pipe. Zacznijmy od budowy nowych obiektów z wybranych przez nas pól:
jq '.alerts[] | {id:.id, lvl:.level}' soc_data.json
Rozbijmy tę komendę na części:
.alerts[]– wejście do kluczaalertsi rozwinięcie tablicy (każdy alert osobno)|– pipe, czyli weź wynik z lewej i przekaż jako wejście do prawej – dokładnie tak samo jak w bashu{id:.id, lvl:.level}– budujemy nowy obiekt JSON złożony z dwóch pól: kluczidz wartością.idoraz kluczlvlz wartością.levelz oryginalnego alertu
Zamiast dostawać cały alert ze wszystkimi polami, wybieramy tylko to co nas interesuje. Powyższa komenda budowała obiekt {}. Możemy też zbudować tablicę []:
jq '.alerts[0] | [.id,.level,.src_ip]' soc_data.json
Zwraca tablicę wartości bez kluczy. Kiedy używać którego formatu?
{}– gdy chcemy czytelny output, np. do dalszej analizy w terminalu[]– do eksportu, np. w formacie CSV
Kolejny przykład to komenda map(), która zwraca wyniki jako tablicę zamiast osobnych outputów:
jq '.alerts | map(.src_ip)' soc_data.json
Warto porównać to z alternatywnym zapisem:
jq '.alerts[] | .src_ip' soc_data.json
Otrzymujemy ten sam wynik, ale w innym formacie. map() przydaje się w momencie, gdy chcemy zachować tablicę, co pozwoli nam dalej analizować dane – np. za pomocą unique lub sort. Dla zapisu z | .src_ip nie będzie to już możliwe.
FILTROWANIE – select, test, contains
Komenda select() zwraca wszystkie obiekty spełniające podany warunek:
jq '.alerts[] | select(.level=="CRITICAL")' soc_data.json
Możemy łączyć warunki logiczne za pomocą and, or i not:
jq '.alerts[] | select(.severity>7 and .acknowledged==false)' soc_data.json
Filtrujemy obiekty, gdzie severity jest większe od 7 i acknowledged wynosi false.
Komenda test() pozwala filtrować za pomocą wyrażeń regularnych:
jq '.alerts[] | select(.src_ip | test("^10\\."))' soc_data.json
Zwraca wszystkie alerty, gdzie pole src_ip zaczyna się od 10. – czyli wewnętrzna adresacja IP. ^ oznacza początek stringa, a \\. to dosłowna kropka.
Komenda contains() filtruje po zawartości ciągu znaków:
jq '.alerts[] | select(.rule_name | contains("Cobalt"))' soc_data.json
Zwraca alerty, gdzie rule_name zawiera słowo "Cobalt" – szukamy zwykłego ciągu znaków, bez regex.
AGREGACJE
Jedna z prostszych komend na początek – length zwróci nam liczbę alertów w tablicy:
jq '.alerts | length' soc_data.json
Komenda sort_by() pozwala posortować wyniki. Połączona z reverse da nam alerty od najwyższej do najniższej wartości severity:
jq '.alerts | sort_by(.severity) | reverse' soc_data.json
Komenda unique_by() przydaje się do deduplikacji. Poniższy przykład zwraca liczbę unikalnych adresów IP w tablicy alerts:
jq '.alerts | unique_by(.src_ip) | length' soc_data.json
Na koniec komenda add, która pozwala zsumować elementy tablicy. Poniżej obliczamy cały ruch przychodzący – tworzymy tablicę wartości bytes_in za pomocą map(), a następnie sumujemy wszystkie elementy przez add:
jq '.network_flows | map(.bytes_in) | add' soc_data.json
To wszystko co chciałem przekazać w tym artykule. Zachęcam do własnych ćwiczeń z przygotowanymi przeze mnie logami i życzę powodzenia w pracy z jq!