root@echelon:~# cat /var/log/cli-essentials/jq.md
// TUTORIAL

[*] cli-essentials #1 – jq

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.

[↓] MATERIAŁY DO ĆWICZEŃ

Plik z przykładowymi logami SOC użytymi w artykule. Pobierz i ćwicz komendy samodzielnie.

SHA256: 0cc4cb1610b4a3a84f1917ad6cc8bc351cd98fe39978fadd698ba20db720ab44 ✓ 0/61 VT

[↓] soc_data.json

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
Wynik komendy jq .metadata
Wynik komendy jq '.metadata'

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
Wynik komendy jq .alerts[0]
Wynik komendy jq '.alerts[0]'

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 klucza alerts i 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: klucz id z wartością .id oraz klucz lvl z wartością .level z 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
Wynik budowania obiektów w jq
Porównanie formatu {} i []

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.

Porównanie map() i pipe src_ip
map() vs | .src_ip – ten sam wynik, inny format

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
Wynik komendy unique_by w jq
Liczba unikalnych adresów IP – wynik unique_by()

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
Wynik komendy add w jq
Suma ruchu przychodzącego – wynik map() | add

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!

« POWRÓT DO LISTY ARTYKUŁÓW

system@echelon:~# cat /etc/privacy_notice.txt

Ta strona używa Google Analytics do zbierania anonimowych informacji o ruchu na stronie. Kontynuując korzystanie z tej strony, wyrażasz zgodę na używanie plików cookie zgodnie z naszą Polityką Prywatności.