Analiza zagrożeń przy użyciu Pythona – praktyczne ćwiczenia z cyberbezpieczeństwa
Analiza zagrożeń przy użyciu Pythona może być świetnym sposobem na naukę programowania bez odrywania się od realnych problemów cyberbezpieczeństwa.
Zamiast zaczynać od klasycznych ćwiczeń w rodzaju „policz średnią ocen” albo „napisz program obliczający pole prostokąta”, możemy od pierwszych minut pracować na danych, które przypominają to, z czym faktycznie spotyka się analityk bezpieczeństwa.
Czyli na przykład:
- logach,
- adresach IP,
- nieudanych logowaniach,
- hashach,
- zdarzeniach malware,
- potencjalnych IOC,
- alertach,
- prostych regułach detekcyjnych.
I właśnie na tym polega pomysł na ten materiał.
Jestem ekspertką w programie SupermOcNA w IT i prowadzę szkolenia z cyberbezpieczeństwa. Ten wpis przygotowałam jako materiał pomocniczy przede wszystkim dla kursantek, które chcą wrócić do ćwiczeń po zajęciach, ale również dla wszystkich innych osób zainteresowanych praktyczną nauką Pythona w kontekście bezpieczeństwa.
Jeżeli więc:
- uczysz się Pythona,
- zaczynasz przygodę z cyberbezpieczeństwem,
- interesuje Cię SOC,
- chcesz lepiej rozumieć analizę logów,
- albo po prostu chcesz zobaczyć, do czego Python może przydać się poza typowymi zadaniami programistycznymi,
to ten wpis jest właśnie dla Ciebie. Kod do ćwiczeń znajdziesz również w moim repozytorium: Analiza zagrożeń przy użyciu Pythona – Github.
Poniżej pokazane przykłady zostały wykonane z wykorzystaniem maszyny wirtualnej z Ubuntu. Jeśli chcesz testować poniższe przykłady w bezpieczny sposób, przygotuj sobie wcześniej maszynę wirtualną na Ubuntu zgodnie z poradnikiem: Utworzenie i konfiguracja maszyny wirtualnej z systemem Ubuntu w Oracle VirtualBox – instrukcja. Znajdziesz w nim także instrukcję, jak na Windowsie zainstalować Oracle VirtualBox.
Dlaczego analiza zagrożeń przy użyciu Pythona ma sens?
W nauce automatyzacji bezpieczeństwa łatwo zacząć od zbyt trudnych tematów. Frameworki, SIEM, API, machine learning czy rozbudowane pipeline’y brzmią atrakcyjnie, jednak na początku często utrudniają zrozumienie podstaw. Dlatego lepiej najpierw rozłożyć cały proces na małe elementy. W wielu przypadkach mechanizm bezpieczeństwa zaczyna się od prostego pytania:
Czy wydarzyło się coś, co spełnia określony warunek?
Na przykład:
if failed_logins >= 10:
print("ALERT")
Kod jest bardzo prosty. Mimo to prowadzi bezpośrednio do tematów takich jak:
- progi detekcji,
- zdarzenia,
- agregacja,
- korelacja,
- false positives,
- priorytetyzacja,
- ocena ryzyka (risk scoring).
W efekcie analiza zagrożeń przy użyciu Pythona staje się nie tylko ćwiczeniem programistycznym, lecz także sposobem na zrozumienie logiki systemów bezpieczeństwa.
Dwie możliwe ścieżki wykonywania ćwiczeń
W praktyce ucząc się się Pythona i przerabiając ćwiczenia w praktyce, możesz to zrobić na kilka sposobów. My tutaj omówimy 2 z nich.
Wariant 1 – najprostszy, czyli Python bez instalacji
Jeżeli dopiero zaczynasz naukę Pythona, nie musisz od razu instalować Pythona, konfigurować środowiska wirtualnego ani przygotowywać własnej maszyny. Pierwsze ćwiczenia z tego artykułu możesz wykonać bezpośrednio w przeglądarce internetowej. Do tego celu możesz wykorzystać serwis: Online Python – uruchamianie kodu Python w przeglądarce. Wystarczy przeglądarka internetowa. Nie musisz instalować dodatkowego oprogramowania. Cały proces wygląda następująco:
przeglądarka internetowa
↓
Online Python
↓
wklejenie kodu
↓
uruchomienie programu – Run
↓
wykonanie kodu przez interpreter Python
↓
wynik programu

Dla kogo jest ten wariant?
Online Python jest dobrym rozwiązaniem, jeśli:
-
dopiero zaczynasz programowanie,
-
nie masz jeszcze zainstalowanego Pythona,
-
nie chcesz na początku konfigurować środowiska,
-
korzystasz z komputera, na którym nie możesz instalować oprogramowania,
-
chcesz szybko sprawdzić przykład ze szkolenia lub artykułu,
-
zależy Ci przede wszystkim na zrozumieniu kodu.
Dzięki temu na początku możesz skupić się na tym, czym są zmienne, listy, indeksy, pętle i instrukcje warunkowe, zamiast walczyć jednocześnie z konfiguracją systemu.
Czy Online Python wystarczy do wszystkich ćwiczeń?
Do prostych, jednoplikowych przykładów – jak najbardziej. Jednak wraz z rozwojem naszych ćwiczeń będziemy chcieli pracować z większym projektem, plikami, logami i dodatkowymi bibliotekami. Wtedy wygodniej będzie przejść na własne środowisko. Dlatego możesz zacząć od tego lub innego programu online, a później przygotować własne środowisko.
Wariant 2 – własne środowisko laboratoryjne
Drugim sposobem wykonywania ćwiczeń jest praca we własnym środowisku laboratoryjnym. I właśnie z tego wariantu będziemy korzystać w dalszej części tego poradnika.
Możesz pracować na przygotowanej maszynie z systemem operacyjnym albo na własnym komputerze. Może to być na przykład:
-
Ubuntu,
-
Windows,
-
macOS.
Ważne jest przede wszystkim to, aby mieć możliwość uruchomienia Pythona, tworzenia własnych plików oraz pracy w terminalu.
Nasze środowisko będzie wyglądało w uproszczeniu tak:
Ubuntu / Windows / macOS
maszyna laboratoryjna lub własny komputer
↓
katalog projektu
↓
virtual environment
↓
cwiczenie1.py
↓
uruchomienie programu
↓
python cwiczenie1.py
↓
wynik
W porównaniu z Online Python wymaga to trochę więcej przygotowań. Zyskujemy jednak środowisko znacznie bardziej zbliżone do tego, w jaki sposób rzeczywiście pracuje się z Pythonem.
Co oznacza „własne środowisko laboratoryjne”?
Nie musi to oznaczać specjalistycznego laboratorium ani dodatkowego fizycznego komputera. Środowiskiem laboratoryjnym może być:
Twój komputer
+
system operacyjny
+
Python
+
katalog projektu
+
virtual environment
+
pliki .py
Możesz również wykorzystać maszynę wirtualną przygotowaną specjalnie do nauki. Dzięki temu możesz swobodnie eksperymentować z kodem, zmieniać go, powodować błędy, poprawiać je i uruchamiać program ponownie. Właśnie tak będziemy pracować w dalszej części poradnika.
Nasz cykl pracy będzie bardzo prosty
W większości ćwiczeń będziemy powtarzać podobny schemat:
otwieramy katalog projektu
↓
aktywujemy virtual environment
↓
tworzymy lub otwieramy plik .py
↓
piszemy / modyfikujemy kod
↓
zapisujemy plik
↓
uruchamiamy go w terminalu
↓
obserwujemy wynik
↓
zmieniamy kod
↓
uruchamiamy ponownie
To ważne, ponieważ programowania nie uczymy się wyłącznie przez czytanie kodu. Trzeba go uruchamiać, zmieniać i czasami również psuć. Jeżeli usuniesz dwukropek, pomylisz nazwę zmiennej albo zrobisz nieprawidłowe wcięcie i Python wyświetli błąd – to również jest część nauki. Czytanie komunikatów o błędach i szukanie ich przyczyny jest jedną z podstawowych umiejętności w pracy z kodem.
Jak będzie wyglądał nasz projekt później?
W miarę wykonywania kolejnych ćwiczeń katalog może zacząć wyglądać na przykład tak:
analiza-zagrozen-python/
│
├── .venv/
│
├── cwiczenie1.py
├── cwiczenie2.py
├── cwiczenie3.py
├── cwiczenie4.py
├── cwiczenie5.py
│
└── ...
Każde ćwiczenie będzie więc osobnym małym programem, do którego można później wrócić. Dzięki temu nie tylko poznajemy składnię Pythona, ale również od początku uczymy się organizowania własnego projektu.
To właśnie wariant 2 wykorzystujemy dalej
Podsumowując: w dalszej części tego poradnika będziemy pracować przede wszystkim we własnym środowisku laboratoryjnym.
Zakładamy więc następujący model:
własny komputer / maszyna laboratoryjna
↓
Python
↓
katalog naszego projektu
↓
virtual environment
↓
pliki .py
↓
uruchamianie w terminalu
↓
analiza
Jeżeli korzystasz z Online Python, wiele pierwszych przykładów nadal możesz wykonywać w przeglądarce. Przy każdym ćwiczeniu warto jednak pamiętać, że główna ścieżka poradnika zakłada pracę na własnym środowisku.
To rozwiązanie wymaga na początku kilku dodatkowych kroków, ale pozwala nauczyć się nie tylko samej składni Pythona. Od początku pracujemy w sposób znacznie bardziej zbliżony do prawdziwej pracy z kodem i narzędziami wykorzystywanymi w cyberbezpieczeństwie.
Który wariant wybrać?
Na początku wybierz ten, który pozwala Ci przede wszystkim skupić się na kodzie. Jeżeli terminal, instalacja Pythona i virtual environment są na razie dodatkową trudnością, zacznij od Pythona w wersji online. Natomiast jeśli chcesz od razu pracować tak, jak pracuje się z normalnym projektem, przygotuj własną maszynę zgodnie z poradnikami na naszym blogu, wymienionymi na górze wpisu. Niezależnie od wybranego wariantu sam kod działa według tych samych zasad. I to jest najważniejsze: najpierw zrozumieć, co robi program, a dopiero później stopniowo poznawać coraz bardziej rozbudowane narzędzia.
Chcesz lepiej poznać Pythona?
Ten artykuł koncentruje się przede wszystkim na wykorzystaniu Pythona w ćwiczeniach związanych z cyberbezpieczeństwem. Nie zastępuje pełnego kursu programowania. Jeżeli jakieś pojęcie – na przykład zmienna, lista, pętla czy funkcja – nadal jest niejasne, polecam równolegle korzystać z Python Tutorial – W3Schools. Możesz uczyć się dokładnie wtedy, kiedy dana konstrukcja pojawia się w ćwiczeniach. Przykładowo:
w ćwiczeniu pojawia się pętla for
↓
nie wiesz jeszcze, jak działa
↓
otwierasz lekcję o pętlach w W3Schools
↓
wykonujesz kilka prostych przykładów
↓
wracasz do naszego ćwiczenia
↓
modyfikujesz kod i sprawdzasz wynik
W ten sposób uczysz się Pythona na konkretnych przykładach związanych z cyberbezpieczeństwem, zamiast próbować wcześniej zapamiętać całą składnię języka.
Analiza zagrożeń przy użyciu Pythona – przygotowanie środowiska
Do większości ćwiczeń wystarczy Python 3. Na Ubuntu możesz sprawdzić wersję:
python3 --version
Rekomenduję utworzenie najpierw katalogu na wszystkie projekty, np. o nazwie workspace, Ja na swojej maszynie już taki mam, więc nie muszę znowu go tworzyć:
mkdir workspace
cd workspace
Następnie utwórz katalog projektu w katalogu workspace:
mkdir analiza-zagrozen-python
cd analiza-zagrozen-python
Potem przygotuj środowisko wirtualne:
python3 -m venv .analiza-python
Aktywuj je:
source .analiza-python/bin/activate
Na końcu sprawdź interpreter:
which python
python --version
Jeśli potrzebujesz pełnej instrukcji, zobacz: Virtual environment na Ubuntu – poradnik ZALNET. W tym wpisie pokazuję między innymi tworzenie .venv, aktywację środowiska, pracę z pip, requirements.txt oraz poprawną dezaktywację przed przejściem do innego projektu.
Jak sprawdzić, czy jesteśmy w dobrym katalogu?
Jeżeli Python wyświetli komunikat, że nie może znaleźć pliku, bardzo możliwe, że terminal znajduje się w innym katalogu. Na Ubuntu możesz sprawdzić aktualny katalog pwd. Następnie wyświetlić jego zawartość za pomocą polecenia ls. Powinna pojawić się między innymi nazwa: cwiczenie1.py. Jeżeli jej nie ma, przejdź do właściwego katalogu poleceniem cd analiza-zagrozen-python i spróbuj ponownie uruchomić polecenie: python cwiczenie1.py.
Jak dodawać kod ćwiczeniowy do naszego projektu?
Jeżeli wykonujesz ćwiczenia lokalnie na maszynie Ubuntu przygotowanej wcześniej zgodnie z wymienionym wyżej poradnikiem, kod z ćwiczenia warto zapisać jako osobny plik .py w katalogu projektu. Zakładam, że utworzył_ś już katalog analiza-zagrozen-python w katalogu workspace i jesteś już w nim:
cd analiza-zagrozen-python
Jeżeli korzystasz ze środowiska wirtualnego, najpierw je aktywuj:
source .analiza-python/bin/activate
Po aktywacji możesz utworzyć pierwszy plik z kodem. Nazwijmy go cwiczenie1.py. Rozszerzenie .py informuje system i edytor, że jest to plik z kodem Pythona.
Jak utworzyć plik?
Możesz zrobić to na kilka sposobów. Będąc wciąż w katalogu projektu, utwórz nowy plik za pomocą polecenia:
touch cwiczenie1.py
Jeżeli korzystasz z edytora Visual Studio Code, otwórz plik w VS Code przy użyciu polecenia
code .

Jeżeli nigdy wcześniej nie programowałaś albo nie programowałeś, ten fragment może na początku wyglądać trochę jak język przybyszów z obcej planety. To normalne. Programowanie rzeczywiście ma własne słowa, zasady i znaki, ale bardzo szybko można zacząć dostrzegać w nim logiczny porządek. Na początek nie musimy jeszcze wiedzieć, czym są klasy, biblioteki, obiekty albo API. Wystarczy zrozumieć kilka bardzo podstawowych rzeczy. Będziemy pracować z programem, który analizuje liczbę nieudanych logowań i dzieli je na trzy grupy:
OK,WARNING,ALERT.
Na końcu policzy również, ile alertów wystąpiło.
Wklej do wcześniej utworzonego pliku kod poniżej:
print("***** Failed logins *****")
print()
failed_logins = [2, 5, 12, 3, 18, 7, 10, 1]
alert_count = 0
for value in failed_logins:
if value >= 10:
print(f"ALERT: {value} nieudanych logowań")
alert_count += 1
elif value >= 5:
print(f"WARNING: {value} nieudanych logowań")
else:
print(f"OK: {value} nieudanych logowań")
print()
print(f"Liczba alertów: {alert_count}")
Następnie zapisz zmiany w pliku. W katalogu projektu powinna pojawić się mniej więcej taka struktura:
analiza-zagrozen-python/
│
├── .analiza-python/
└── cwiczenie1.py

W kolejnych ćwiczeniach możemy dodawać następne pliki. Dzięki temu każde ćwiczenie pozostaje niezależne i możemy wracać do niego później.
analiza-zagrozen-python/
│
├── .venv/
├── cwiczenie1.py
├── cwiczenie2.py
├── cwiczenie3.py
└── cwiczenie4.py
Jak uruchomić program napisany w Pythonie?
Kod zapisany w pliku nie wykona się sam. Musimy poprosić interpreter Pythona o jego uruchomienie. Będąc w katalogu projektu, wpisz w terminalu python cwiczenie1.py. Jeżeli w Twoim systemie Python jest dostępny jako python3, możesz użyć python3 cwiczenie1.py. Po naciśnięciu Enter interpreter odczyta plik od góry do dołu i wykona wszystkie instrukcje. W terminalu powinien pojawić się wynik:
***** Failed logins *****
OK: 2 nieudanych logowań
WARNING: 5 nieudanych logowań
ALERT: 12 nieudanych logowań
OK: 3 nieudanych logowań
ALERT: 18 nieudanych logowań
WARNING: 7 nieudanych logowań
ALERT: 10 nieudanych logowań
OK: 1 nieudanych logowań
Liczba alertów: 3
To oznacza, że program został poprawnie wykonany.

Na pierwszy rzut oka jest tu sporo nowych elementów. Rozłóżmy więc cały program na bardzo małe kawałki. Program komputerowy to po prostu lista instrukcji. Najprościej wyobrazić sobie program jak przepis kulinarny. Jeżeli mamy przepis: 1. Weź miskę.
2. Wsyp mąkę.
3. Dodaj wodę.
4. Wymieszaj. to wykonujemy instrukcje po kolei. Program działa podobnie. Komputer czyta kod od góry do dołu i wykonuje kolejne polecenia. Kod jest więc tylko bardziej precyzyjnym sposobem zapisania instrukcji.
Co się dzieje po wpisaniu polecenia python cwiczenie1.py?
Można to sobie wyobrazić w następujący sposób:
cwiczenie1.py
↓
interpreter Python
↓
odczyt pierwszej linii
↓
wykonanie instrukcji
↓
odczyt kolejnej linii
↓
...
↓
wynik w terminalu
Interpreter to program, który „rozumie” kod Pythona i wykonuje zapisane w nim instrukcje. Kiedy wpisujemy python cwiczenie1.py mówimy w uproszczeniu: Pythonie, otwórz plik cwiczenie1.py i wykonaj znajdujący się w nim kod.
Teraz przeanalizujmy nasz pierwszy program bardzo dokładnie. Zakładamy przy tym, że jest to Twój pierwszy kontakt z programowaniem. Dlatego zanim przejdziemy dalej, wyjaśnimy nie tylko poszczególne instrukcje, ale również takie pojęcia jak zmienna, lista, indeks, pętla czy instrukcja warunkowa.
Kod wygląda następująco:
print("***** Failed logins *****")
print()
failed_logins = [2, 5, 12, 3, 18, 7, 10, 1]
alert_count = 0
for value in failed_logins:
if value >= 10:
print(f"ALERT: {value} nieudanych logowań")
alert_count += 1
elif value >= 5:
print(f"WARNING: {value} nieudanych logowań")
else:
print(f"OK: {value} nieudanych logowań")
print()
print(
f"Liczba alertów: {alert_count}"
)
Program analizuje liczby reprezentujące liczbę nieudanych logowań. Każdą wartość przypisuje do jednej z trzech kategorii: OK, WARNING albo ALERT. Dodatkowo liczy, ile razy pojawił się poziom ALERT. Prześledzimy teraz działanie programu i poznamy kilka podstawowych pojęć.
Linie 1 i 2. Wyświetlenie nagłówka i pustej linii
Pierwsza linia print("***** Failed logins *****") wyświetla na ekranie tekst/nagłówek: ***** Failed logins *****. Dzięki temu od razu wiadomo, czego dotyczy program. Druga linia z poleceniem print() dodaje pustą linię, żeby wynik był bardziej czytelny. Na tym etapie zobaczymy:
***** Failed logins *****
print() oznacza:
wyświetl coś na ekranie.
Możemy potraktować print() jak osobę stojącą przy tablicy i zapisującą to, co jej podamy. Jeżeli powiemy print("Dzień dobry") na tablicy zobaczymy Dzień dobry. Jeżeli wpiszemy print("ALERT") pojawi się ALERT

Co to jest funkcja?
print() jest funkcją. To jedno z podstawowych pojęć programowania. Funkcję można porównać do małego urządzenia albo usługi, która wykonuje określone zadanie. Wyobraź sobie ekspres do kawy. Wkładasz do niego kawę i wodę, a urządzenie wykonuje konkretną czynność i zwraca wynik: gotową kawę. Funkcja działa podobnie. Dostaje jakieś dane, wykonuje zadanie, a czasem zwraca wynik. print() ma bardzo proste zadanie:Wyświetl na ekranie to, co zostało do ciebie przekazane.
Co oznaczają nawiasy w funkcji?
W zapisie print("ALERT") mamy nazwę funkcji print oraz nawiasy okrągłe (). W nawiasach przekazujemy informację, z którą funkcja ma pracować. Można to czytać jako:
funkcjo
W naszym przypadku przekazujemy tekst do wyświetlenia: "***** Failed logins *****"
Dlaczego tekst jest zapisany w cudzysłowie?
W poleceniu print("***** Failed logins *****") tekst znajduje się między cudzysłowami: " ". Dzięki temu Python wie:
to jest tekst, a nie nazwa polecenia albo zmiennej.
Na przykład print("ALERT") oznacza:
wyświetl słowo ALERT.
Natomiast bez cudzysłowu print(ALERT) Python potraktowałby ALERT jako nazwę czegoś, co powinno już istnieć. Jeżeli nie istnieje, pojawiłby się błąd.
Dlatego na początku warto zapamiętać prostą zasadę:
zwykły tekst zapisujemy w cudzysłowie.

Drugi print() – dlaczego jest pusty?
Druga linia to wywołanie print(). Nie ma niczego w nawiasach. W tym przypadku print() po prostu wyświetla nową, pustą linię. Można to potraktować jak naciśnięcie Entera w edytorze tekstu.
Dzięki temu wynik będzie wyglądał czytelniej:
***** Failed logins *****
OK: 2 nieudanych logowań
zamiast:
***** Failed logins *****
OK: 2 nieudanych logowań
To nie zmienia działania programu. Poprawia tylko wygląd wyniku.
Linia 3. Utworzenie listy z danymi
Poniższa linia tworzy listę liczb. Możemy ją potraktować jak zestaw danych wejściowych, które chcemy przeanalizować. Każdy element w takiej liście ma swój własny indeks. Czyli pierwszy element z listy 2 ma indeks=0, drugi element z listy 5 ma indeks=1itd. Zakładając, że lista ma in elementów to indeksy mają zakres od 0 do n-1. Nasza 8-elementowa lista ma indeksy od 0-7.
failed_logins = [2, 5, 12, 3, 18, 7, 10, 1]
W uproszczeniu:
2
5
12
3
18
7
10
1
Każda liczba oznacza liczbę nieudanych prób logowania dla jakiegoś analizowanego przypadku. Na razie nie interesuje nas jeszcze, jaki użytkownik albo adres IP stoi za danym wynikiem. Ćwiczymy samą logikę klasyfikowania wartości. Najpierw jednak musimy wyjaśnić, jak działa ta linia kodu.
Czym jest zmienna?
Jednym z najważniejszych pojęć w programowaniu jest zmienna. W naszym kodzie zmienną jest między innymi failed_loginsoraz alert_count, a nawet value. Wyobraź sobie telefon z kontaktami. Jeden kontakt ma podpis: Mama, drugi Szef, a trzeci Kosmetyczka. Nie musisz za każdym razem pamiętać wszystkich numerów telefonów. Wystarczy, że znasz nazwę kontaktu. Zmienne działają bardzo podobnie. W Pythonie zapisujemy zmienne w ten sposób: alert_count = 0. Możemy przeczytać tę instrukcję jako:
Utwórz zmienną o nazwie
alert_counti zapisz w niej (przypisz do niej) wartość0.
Dla telefonu taka instrukcja brzmiałaby:
Utwórz mi kontakt o nazwie
Ulubiony mechaniki przypisz do niego numer telefonu+48 000 000 000.
Od tej chwili nie musimy pamiętać samej wartości zmiennej ani numeru mechanika. Możemy odwoływać się do niej zmiennej i kontaktu w telefonie poprzez nazwę i pobierać aktualną wartość zmiennej lub numer telefonu mechanika.
Po co używamy zmiennych?
Wyobraź sobie, że musimy zapamiętać:
-
nazwę użytkownika,
-
jego adres IP,
-
liczbę nieudanych logowań,
-
liczbę wykrytych alertów.
Moglibyśmy utworzyć zmienne:
username = "admin"
ip_address = "192.168.1.10"
failed_logins = 12
alert_count = 3
Nazwy pomagają nam zrozumieć, co oznaczają dane. Dlatego zamiast pracować po prostu z liczbą 12 możemy pracować ze zmienną failed_logins i od razu wiemy, co ta liczba oznacza.
Zmienna może zmieniać wartość
Nazwa zmienna nie jest przypadkowa. Wartość przechowywana w zmiennej może zmienić się podczas działania programu. Na przykład zaczynamy od alert_count = 0. Potem wykrywamy pierwszy alert i wartość zmienia się na 1. Następnie 2 , a później 3. Możemy więc wyobrazić sobie:
alert_count
0 → 1 → 2 → 3
Nazwa pozostaje taka sama, ale zawartość naszego pudełka się zmienia.
Co oznacza znak = w zmiennej?
W naszym kodzie pojawia się alert_count = 0. Warto zwrócić uwagę na znak = . W programowaniu alert_count = 0 oznacza:
zapisz wartość
0w zmiennejalert_count.
Można to czytać alert_count otrzymuje wartość 0 . To ważne, ponieważ w matematyce x = 5 oznacza równość.
W Pythonie znak = często oznacza przypisanie wartości do zmiennej. Czyli alert_count = 0 możemy przeczytać:
Do zmiennej
alert_countprzypisz wartość0.
Podobnie username = "admin"o oznacza:
Do zmiennej
usernameprzypisz tekst"admin".
Co to są typy danych?
Komputer musi wiedzieć, z jakim rodzajem informacji pracuje. Inaczej traktuje tekst, inaczej liczbę, a jeszcze inaczej listę wielu wartości. Te różne rodzaje informacji nazywamy typami danych. W naszym programie występują trzy bardzo ważne typy: tekst, liczba i lista.
Typ str – tekst
Na przykład "ALERT" albo "nieudanych logowań" to tekst. W Pythonie taki typ nazywa się str od angielskiego słowa string, czyli ciąg znaków.
Typ int – liczba całkowita
Wartości 2, 5, 12 czy 18 to liczby całkowite. W Pythonie ich typ to int od angielskiego słowa integer.
Typ list – lista wielu wartości
Fragment polecenia [2, 5, 12, 3, 18, 7, 10, 1] to lista. W Pythonie ma typ list. Można ją porównać do kartki z wypisanymi wartościami, tyle że wszystkie są przechowywane razem:
2
5
12
3
18
7
10
1
Jak sprawdzić typ danych?
Python ma funkcję type(). Możemy za jej pomocą sprawdzić typ danych dowolnej wartości albo zmiennej, np: print(type(12)) zwróci nam wynik <class 'int'>. Albo print(type("ALERT"))zwróci wynik: <class 'str'>. A dla listy print(type(failed_logins)) wynik będzie <class 'list'>.

Czym jest lista?
W naszym programie znajduje się zmienna i nazwie failed_logins: failed_logins = [2, 5, 12, 3, 18, 7, 10, 1]. Tym razem do zmiennej nie przypisaliśmy jednej liczby. Przypisaliśmy całą listę liczb. Lista to jeden z podstawowych sposobów przechowywania wielu wartości w Pythonie. Rozpoznajemy ją po nawiasach kwadratowych[ ]. Elementy listy rozdzielamy przecinkami[2, 5, 12, 3, 18, 7, 10, 1]. Możemy wyobrazić sobie listę jak rząd ponumerowanych szuflad, gdzie każda szuflada posiada jedną wartość:
┌───┬───┬────┬───┬────┬───┬────┬───┐
│ 2 │ 5 │ 12 │ 3 │ 18 │ 7 │ 10 │ 1 │
└───┴───┴────┴───┴────┴───┴────┴───┘
Czym jest indeks na liście?
Każdy element listy ma swoją pozycję. Ta pozycja nazywana jest indeksem. Tutaj pojawia się jedna rzecz, która na początku może być zaskakująca: Python zaczyna numerowanie elementów listy od zera, a nie od jedynki. Nasza lista failed_logins = [2, 5, 12, 3, 18, 7, 10, 1] ma więc indeksy:
wartość: 2 5 12 3 18 7 10 1
│ │ │ │ │ │ │ │
indeks: 0 1 2 3 4 5 6 7
Pierwszy element ma indeks 0. Drugi element ma indeks 1. Trzeci element ma indeks 2. I tak dalej.
Jak pobrać element za pomocą indeksu?
Jeżeli chcemy pobrać pierwszy element print(failed_logins[0])otrzymamy 2. Jeżeli napiszemy print(failed_logins[1]) otrzymamy 5. Natomiast print(failed_logins[2]) wyświetli 12. Dlatego zapis failed_logins[2] nie oznacza „drugi element”. Oznacza:
element znajdujący się pod indeksem 2.
A ponieważ liczymy od zera, jest to trzeci element listy. Na początku nie trzeba jednak zagłębiać się w szczegóły techniczne. Najważniejsze jest zapamiętanie:
W Pythonie pierwszy element listy ma indeks 0.

Ile elementów ma nasza lista?
Możemy to sprawdzić funkcją len(failed_logins). len pochodzi od angielskiego length, czyli długość. Jeżeli napiszemy print(len(failed_logins))otrzymamy 8. Lista ma osiem elementów. Co ciekawe, ostatni indeks to jednak 7. Mamy więc:
liczba elementów: 8
indeksy:
0 1 2 3 4 5 6 7
To bardzo ważna zależność, z którą jeszcze wielokrotnie spotkamy się w Pythonie.

Możemy również zmienić wartości elementów listy
Indeks pozwala nie tylko odczytać wartość, ale także ją zmienić. Załóżmy, że mamy failed_logins = [2, 5, 12] i chcemy zmienić pierwszą wartość z 2 na 4. Możemy napisać failed_logins[0] = 4. Lista zmieni się z [2, 5, 12] na [4, 5, 12]. Indeks pozwala więc wskazać konkretny element listy.

Indeksy ujemne – mała ciekawostka
Python pozwala również liczyć elementy od końca. Na przykład failed_logins[-1] oznacza:
ostatni element listy.
Dla naszej listy [2, 5, 12, 3, 18, 7, 10, 1] instrukcja print(failed_logins[-1]) wyświetli 1. Z kolei failed_logins[-2] oznacza przedostatni element, czyli 10. Na razie nie będziemy tego potrzebować w naszym programie, ale warto wiedzieć, że Python daje taką możliwość.
Podsumowując: Python tworzy zmienną failed_logins, a następnie przypisuje do niej listę ośmiu liczb. Na tym etapie możemy powiedzieć:
zmienna: failed_logins
typ danych: list
liczba elementów: 8
pierwszy indeks: 0
ostatni indeks: 7
Dlaczego lista jest wygodna?
Bez listy musielibyśmy tworzyć:
failed_login_1 = 2
failed_login_2 = 5
failed_login_3 = 12
failed_login_4 = 3
i tak dalej. To szybko stałoby się niewygodne. Lista pozwala przechowywać wiele danych pod jedną nazwą, np. failed_logins.
Linia 5. Ustawienie licznika alertów
Potem pojawia się zapis alert_count = 0. Tworzona jest zmienna przechowująca liczbę wykrytych alertów. Możemy myśleć o nim jak o liczniku na bramce wejściowej. Na początku mamy 0 alertów. Za każdym razem, kiedy program znajdzie wartość spełniającą warunek ALERT, licznik zostanie zwiększony o jeden. ponieważ program jeszcze niczego nie przeanalizował.
Linie 7 – 17. Działanie pętli
Linia 7 to rozpoczęcie pętli. for value in failed_logins: oznacza: weź po kolei każdą pojedynczą wartość value z listy failed_logins i wykonaj dla niej poniższe instrukcje. Program będzie więc kolejno analizował liczby z naszej listy:
2
5
12
3
18
7
10
1
Co to jest pętla?
W naszym pliku jest bardzo ważny kod. To pętla for. Pętla pozwala wielokrotnie wykonać ten sam fragment kodu.
for value in failed_logins:
if value >= 10:
print(f"ALERT: {value} nieudanych logowań")
alert_count += 1
elif value >= 5:
print(f"WARNING: {value} nieudanych logowań")
else:
print(f"OK: {value} nieudanych logowań")
Analogia do sprawdzania listy obecności
Wyobraź sobie listę:
Anna
Beata
Kasia
Ola
Chcesz sprawdzić obecność. Nie tworzysz czterech różnych procedur. Wykonujesz powtarzające się czynności:
weź pierwszą osobę
sprawdź obecność
weź drugą osobę
sprawdź obecność
weź trzecią osobę
sprawdź obecność
Pętla robi dokładnie to samo.
Po co nam pętla?
Załóżmy, że nie mamy pętli. Musielibyśmy sprawdzić każdą wartość oddzielnie. Dla pierwszej liczby:
value = 2
if value >= 10:
print("ALERT")
elif value >= 5:
print("WARNING")
else:
print("OK")
Potem dla kolejnej:
value = 5
if value >= 10:
print("ALERT")
elif value >= 5:
print("WARNING")
else:
print("OK")
Następnie dla 12, 3, 18, 7, 10 i 1. Pisanie tego samego kodu osiem razy nie miałoby sensu. A co, gdybyśmy mieli 10 000 zdarzeń? Nie napiszemy przecież 10 000 razy tej samej instrukcji. Dlatego używamy pętli.
Pętla jak pracownik przy taśmie produkcyjnej
Wyobraź sobie taśmę produkcyjną. Na taśmie jadą kolejne pudełka: 2 → 5 → 12 → 3 → 18 → 7 → 10 → 1. Pracownik wykonuje dla każdego pudełka dokładnie tę samą kontrolę. Bierze pierwsze pudełko z wartością 2, sprawdza je i odkłada. Potem bierze 5, sprawdza i odkłada. Następnie 12 i robi dokładnie to samo. Pętla for jest takim automatycznym pracownikiem. Mówimy jej:
Weź każdy element z listy i wykonaj dla niego te same instrukcje.
Jak czytać instrukcję: for value in failed_logins?
Spójrzmy na for value in failed_logins:. Możemy przeczytać to prawie jak zdanie:
Dla każdej wartości
valueznajdującej się wfailed_loginswykonaj kod znajdujący się poniżej.
Słowo for rozpoczyna pętlę. Nazwa value jest zmienną pomocniczą. Słowo in możemy rozumieć jako:
znajdujące się w.
Natomiast failed_logins to lista, przez którą chcemy przejść.
Czym jest value?
To bardzo ważne. value również jest zmienną. Nie wpisaliśmy wcześniej value = .... Python tworzy i aktualizuje ją podczas działania pętli. W pierwszym przebiegu value = 2, w drugim value = 5, w trzecimvalue = 12, potem value = 3 i tak dalej. Możemy przedstawić to tak:
failed_logins = [2, 5, 12, 3, 18, 7, 10, 1]
↓
1. obrót pętli → value = 2
2. obrót pętli → value = 5
3. obrót pętli → value = 12
4. obrót pętli → value = 3
5. obrót pętli → value = 18
6. obrót pętli → value = 7
7. obrót pętli → value = 10
8. obrót pętli → value = 1
Każde takie przejście przez pętlę nazywamy iteracją. Nasza pętla wykona więc osiem iteracji, dopóki są elementy w liście. Pętla uruchomi się także po raz dziewiąty, ale ponieważ nie będzie już żadnej liczby w naszej liście, to pętla zakończy działanie i program przejdzie do wykonywania kodu z linii 19.
Czy value musi nazywać się value?
Nie. To nazwa wybrana przez programistę. Moglibyśmy napisać: for number in failed_logins: albo for login_count in failed_logins:. Wtedy wewnątrz pętli musielibyśmy używać tej samej nazwy:
for login_count in failed_logins:
if login_count >= 10:
print(f"ALERT: {login_count}")
W praktyce druga wersja może być nawet bardziej czytelna, ponieważ nazwa login_count od razu mówi nam, co znajduje się w zmiennej.
Co pętla ma wspólnego z indeksem?
W naszym programie for value in failed_logins: nie korzystamy bezpośrednio z indeksów. Python sam pobiera kolejne elementy listy. Możemy myśleć o tym tak:
indeks 0 → wartość 2
indeks 1 → wartość 5
indeks 2 → wartość 12
indeks 3 → wartość 3
indeks 4 → wartość 18
indeks 5 → wartość 7
indeks 6 → wartość 10
indeks 7 → wartość 1
Pętla przechodzi przez wszystkie te elementy po kolei. Na tym etapie nie musimy więc samodzielnie pilnować indeksu. To jedna z zalet pętli:
Python wykonuje powtarzalną pracę za nas.
Linie 9, 10 i 11. Python sprawdza pierwszy warunek – czy mamy do czynienia z ALERTEM
Program dochodzi do if value >= 10:. Ponieważ value = 2 Python tak naprawdę sprawdza, if 2 >= 10? (czy 2 >= 10?). Odpowiedź brzmi FALSE, 2 nie jest większe lub równe 10. Dlatego kod z linii 10 i 11 nie zostaje wykonany. Nie będzie wyświetlonej informacji ALERT z liczbą nieudanych logowań i nie będzie zwiększenia licznika alertów o 1.
Co to znaczy „prawda” i „fałsz” w programowaniu?
Komputer bardzo często podejmuje decyzje na zasadzie: TAK / NIE albo PRAWDA (TRUE) / FAŁSZ (FALSE). Na przykład 12 >= 10 to PRAWDA, natomiast 2 >= 10 to FAŁSZ. Na tej podstawie program decyduje, co zrobić dalej.
Linie 13 i 14. Python sprawdza elif – jeśli to nie ALERT, to sprawdź WARNING
Jeżeli liczba jest mniejsza niż 10, Python przechodzi do:
elif value >= 5:
Warunek elif value >= 5: oznacza:
jeżeli wcześniejszy warunek nie był prawdziwy, sprawdź, czy wartość jest większa lub równa 5.
Dla naszej liczby Python sprawdza if 2 >= 5?. Odpowiedź nadal brzmi FALSE, 2 nie jest większe lub równe 5. Dlatego kod z linii 14 nie zostaje wykonany.
Linie 16 i 17. Python sprawdza else – jeśli żaden warunek nie pasuje, wyświetl OK
Jeżeli wartość jest mniejsza niż 5, program dochodzi do instrukcji else:. Skoro żaden wcześniejszy warunek nie został spełniony, Python dochodzi do else: i wykonuje kod print(f"OK: {value} nieudanych logowań"). else oznacza:
w każdym pozostałym przypadku.
Czyli dla:
2
3
1
program wyświetli OK: 2 nieudanych logowań albo OK: 3 nieudanych logowań. Pierwsza iteracja jest zakończona. Na ekranie pojawia się:
OK: 2 nieudanych logowań
Do pętli trafia drugi element
Python automatycznie wraca do początku pętli. Teraz pobiera element o indeksie 1, czyli 5. Zmienia się więc wartość zmiennej na value = 5. Program ponownie wykonuje te same testy i szuka pierwszego warunku, który zwraca TRUE:
5 >= 10 FALSE
5 >= 5 TRUE
Dlatego wyświetla:
WARNING: 5 nieudanych logowań

Pętla dochodzi do wartości 12
Kolejny element ma indeks = 2, wartość = 12. Czyli value = 12. Program sprawdza warunek 12 >= 10?. Odpowiedź to oczywiście TRUE. Dlatego wykonywana jest 10 linia kodu print(f"ALERT: {value} nieudanych logowań") i widzimy w wyniku: ALERT: 12 nieudanych logowań
Linia 11. Licznik zostaje zwiększony
Ponieważ wykryliśmy ALERT, program wykonuje kod z linii 11: alert_count += 1. To skrócony zapis od: alert_count = alert_count + 1. Przed wykonaniem tej instrukcji alert_count = 0. Po jej wykonaniu alert_count = 1. Można to porównać do ręcznego licznika osób wchodzących na wydarzenie. Każdy wykryty alert to jedno kliknięcie licznika.

Proces powtarzamy dla całej listy
Program analizuje wszystkie elementy:
INDEKS WARTOŚĆ WYNIK
0 2 OK
1 5 WARNING
2 12 ALERT
3 3 OK
4 18 ALERT
5 7 WARNING
6 10 ALERT
7 1 OK
Alerty wystąpiły dla:
12
18
10
Dlatego alert_count zmieniał się następująco:
START
alert_count = 0
12 → ALERT
alert_count = 1
18 → ALERT
alert_count = 2
10 → ALERT
alert_count = 3
Skąd Python wie, co znajduje się w pętli?
Dzięki wcięciom. Spójrz na nasz kod:
for value in failed_logins:
if value >= 10:
print(f"ALERT: {value} nieudanych logowań")
alert_count += 1
elif value >= 5:
print(f"WARNING: {value} nieudanych logowań")
else:
print(f"OK: {value} nieudanych logowań")
print()
Kod przesunięty w prawo znajduje się wewnątrz pętli. Natomiast ostatnie polecenie print()wróciło do lewego marginesu. Dla Pythona oznacza to:
pętla się skończyła, teraz wykonuj dalszą część programu.
Wcięcia w Pythonie nie są więc tylko kwestią estetyki. Są częścią składni języka.
Pętla zakończyła działanie
Kiedy Python wykorzysta ostatni element listy indeks 7 → wartość 1 nie ma już kolejnego elementu. Pętla: for value in failed_logins: kończy więc działanie. Python przechodzi do pierwszej instrukcji znajdującej się poza pętlą, czyli linii 19.
Linia 19. Dodanie pustej linii
Po zakończeniu pętli wywoływana jest funkcja print(). To ponownie tylko kwestia czytelności. Dzięki temu końcowe podsumowanie nie będzie przyklejone do ostatniego komunikatu.
Linia 20. Wyświetlenie liczby alertów
Na końcu mamy kod:
print(f"Liczba alertów: {alert_count}")
Program wyświetla aktualną wartość zmiennej alert_count. Ponieważ alert pojawia się dla:
12
18
10
licznik wyniesie: 3. Dlatego końcowy wynik programu będzie wyglądał tak:
***** Failed logins *****
OK: 2 nieudanych logowań
WARNING: 5 nieudanych logowań
ALERT: 12 nieudanych logowań
OK: 3 nieudanych logowań
ALERT: 18 nieudanych logowań
WARNING: 7 nieudanych logowań
ALERT: 10 nieudanych logowań
OK: 1 nieudanych logowań
Liczba alertów: 3
Co oznacza f przed tekstem?
Spójrzmy jeszcze na print(f". Litera Liczba alertów: {alert_count}")f pozwala umieszczać wartości zmiennych bezpośrednio w tekście. Na przykład:
name = "Anna"
print(f"Cześć {name}")
wyświetli Cześć Anna. Python widzi zmienną{name} i zastępuje ten fragment aktualną wartością zmiennej name. Natomiast zapis bez f w funkcji print()
name = "Anna"
print("Cześć {name}")
wyświetli Cześć {name}, ponieważ potraktuje {name} jako czysty tekst, a nie zmienną.

Dlaczego kolejność if i elif ma znaczenie?
Zwróć uwagę, że najpierw sprawdzamy if value >= 10:, a dopiero potem elif value >= 5:. To ważne. Liczba 12 spełnia przecież także warunek: 12 >= 5. Jednak Python sprawdza warunki od góry. Pierwszy pasujący warunek wygrywa. Dla 12 wygląda to tak:
if 12 >= 10:
TRUE
Python w rezultacie wyświetla ALERT i nie sprawdza już elif. Dlatego kolejność warunków została ustawiona od najbardziej wymagającego do najmniej wymagającego.
Podsumowanie działania całego programu
Wyobraź sobie sortowanie paczek. Liczby pojawiają się kolejno na taśmie 2 → 5 → 12 → 3 → 18 → 7 → 10 → 1. Każda trafia do jednego z trzech koszy z etykietami:
0–4 → OK
5–9 → WARNING
10+ → ALERT
Dodatkowo przy każdym elemencie wpadającym do kosza ALERT naciskamy licznik/alarm. Na końcu odczytujemy licznik i wiemy, ile alertów wystąpiło. Całość można zapisać tak:
START
↓
wyświetl nagłówek
↓
utwórz listę wartości
↓
ustaw licznik alertów na 0
↓
weź pierwszą wartość
↓
czy wartość >= 10?
├── TAK → ALERT + zwiększ licznik
└── NIE
↓
czy wartość >= 5?
├── TAK → WARNING
└── NIE → OK
↓
weź kolejną wartość
↓
powtarzaj do końca listy
↓
wyświetl liczbę alertów
↓
KONIEC
To właśnie robi nasz program.
Jak wyglądają dane podczas wykonywania programu?
Możemy przedstawić cały proces w tabeli. Znak — oznacza tutaj, że Python nie musi już sprawdzać kolejnego warunku. Jeśli value >= 10 jest prawdziwe, odpowiedni blok został wybrany.
| Iteracja | Indeks | value |
value >= 10 |
value >= 5 |
Wynik | alert_count |
|---|---|---|---|---|---|---|
| 1 | 0 | 2 | FALSE | FALSE | OK | 0 |
| 2 | 1 | 5 | FALSE | TRUE | WARNING | 0 |
| 3 | 2 | 12 | TRUE | — | ALERT | 1 |
| 4 | 3 | 3 | FALSE | FALSE | OK | 1 |
| 5 | 4 | 18 | TRUE | — | ALERT | 2 |
| 6 | 5 | 7 | FALSE | TRUE | WARNING | 2 |
| 7 | 6 | 10 | TRUE | — | ALERT | 3 |
| 8 | 7 | 1 | FALSE | FALSE | OK | 3 |
Co trzeba zapamiętać z tego ćwiczenia?
Jeżeli to Twój pierwszy kontakt z programowaniem, nie próbuj zapamiętać całego kodu. Na tym etapie znacznie ważniejsze jest zrozumienie kilku idei. Zmienna jest nazwą, pod którą przechowujemy dane, np. alert_count = 0
Lista pozwala przechowywać wiele elementów, np. failed_logins = [2, 5, 12, 3, 18, 7, 10, 1]
Indeks określa pozycję elementu na liście. Numerowanie zaczynamy od 0:
failed_logins[0] → 2
failed_logins[1] → 5
failed_logins[2] → 12
Pętla for pozwala automatycznie powtarzać czynności dla różnych parametrówfor value in failed_logins:. Zmienna value przechowuje aktualnie analizowany element listy. Warunek pozwala programowi podjąć decyzję: if value >= 10:, elif oznacza „jeżeli poprzedni warunek nie był spełniony, sprawdź kolejny”. else obsługuje wszystkie pozostałe przypadki. Natomiast alert_count += 1 zwiększa licznik o jeden.
Co z tego wynika dla analizy zagrożeń?
Na razie pracujemy na bardzo uproszczonych danych. To celowe. Najpierw chcemy zrozumieć mechanizm:
LISTA DANYCH
↓
PĘTLA
↓
POBIERZ ELEMENT
↓
SPRAWDŹ WARUNEK
↓
┌─────────┬─────────┬─────────┐
│ OK │ WARNING │ ALERT │
└─────────┴─────────┴─────────┘
↓
ZWIĘKSZ LICZNIK
W kolejnych ćwiczeniach będziemy rozwijać dokładnie tę samą koncepcję. Zamiast samej liczby 12 możemy później analizować zdarzenie zawierające:
użytkownik: admin
adres IP: 203.0.113.42
nieudane logowania: 12
czas: 03:15
Następnie dojdą logi, adresy IP, IOC, hashe, porty i inne informacje związane z cyberbezpieczeństwem. Dlatego warto dobrze zrozumieć właśnie ten pierwszy przykład. Lista, zmienna, pętla i warunek to podstawowe klocki, z których będziemy budować znacznie bardziej interesujące programy.
Co takie ćwiczenie ma wspólnego z cyberbezpieczeństwem?
Bardzo dużo. Często to właśnie próg decyduje o tym, czy system potraktuje zdarzenie jako normalne, ostrzegawcze czy podejrzane. Jednak pojawia się ważne pytanie: dlaczego próg wynosi akurat 10? Python tego nie wie. Taką decyzję musi podjąć osoba projektująca detekcję.
Na razie nie budujemy prawdziwego systemu wykrywania brute force. Nie mamy informacji o:
-
czasie,
-
adresie IP,
-
nazwie użytkownika,
-
komputerze,
-
sposobie logowania.
Uczymy się jednak fundamentalnej zasady:
DANE
↓
REGUŁA
↓
KLASYFIKACJA
↓
ALERT
To właśnie ten mechanizm będziemy rozwijać w kolejnych ćwiczeniach. Po zrozumieniu podstaw, zamiast prostych liczb pojawią się prawdziwsze dane, takie jak logi, adresy IP, IoC, hashe czy informacje o użytkownikach.
Ćwiczenie 2. Analiza logów malware w Pythonie
Cel: Parsowanie tekstu, filtrowanie zdarzeń, poznanie słowników oraz obsługa uszkodzonych rekordów.
W pierwszym ćwiczeniu pracowaliśmy z prostą listą liczb:
failed_logins = [2, 5, 12, 3, 18, 7, 10, 1]
Każdy element był tylko liczbą. Teraz zrobimy kolejny krok. W prawdziwej analizie bezpieczeństwa pojedyncze zdarzenie zawiera zazwyczaj znacznie więcej informacji. Możemy chcieć wiedzieć na przykład:
-
jaki jest typ zdarzenia,
-
z jakiego adresu IP pochodzi,
-
jaki hash pliku został wykryty,
-
jaka akcja została wykonana.
Dlatego zaczniemy pracować z logami.
Przygotowanie pliku malware.log
Utwórz plik o nazwie malware.log i wklej do niego następujący kod:
type=MALWARE src=203.0.113.42 hash=44d88612fea8a8f36de82e1278abb02f action=blocked
type=CLEAN src=10.0.0.15 hash=abc123 action=allowed
type=MALWARE src=198.51.100.7 hash=e99a18c428cb38d5f260853678922e03 action=quarantine
type=ERROR message=scan_failed
type=MALWARE src=192.0.2.44 hash=5d41402abc4b2a76b9719d911017c592 action=blocked
type=MALWARE src=203.0.113.15 action=blocked
type=CLEAN src=10.0.0.8 hash=def456 action=allowed

Zwróć uwagę na linię type=MALWARE src=203.0.113.15 action=blocked. Jest to celowo przygotowany niekompletny rekord. Nie zawiera pola hash. Za chwilę zobaczymy, jak zachowa się nasz program, kiedy na niego trafi.
Utwórz drugi plik i nazwij go cwiczenie2.py, a potem wklej do niego poniższy kod lub skopiuj kod z Github – kod do ćwiczenia 2:
processed = 0
skipped = 0
with open("malware.log", "r", encoding="utf-8") as file:
for line in file:
line = line.strip()
if not line:
continue
try:
fields = {}
for token in line.split():
if "=" in token:
key, value = token.split("=", 1)
fields[key] = value
if fields.get("type") != "MALWARE":
continue
src = fields["src"]
file_hash = fields["hash"]
print(
f"MALWARE | IP: {src} | "
f"HASH: {file_hash}"
)
processed += 1
except KeyError as error:
print(
f"Pominięto uszkodzony rekord. "
f"Brak pola: {error}"
)
skipped += 1
print()
print(f"Poprawnie przetworzono: {processed}")
print(f"Pominięto: {skipped}")

Uruchom program za pomocą polecenia:
python cwiczenie2.py

Co nowego pojawia się w tym ćwiczeniu?
W poprzednim ćwiczeniu poznaliśmy między innymi:
-
zmienne,
-
listy,
-
indeksy,
-
pętlę
for, -
instrukcje
if,elifielse.
Tutaj pojawia się bardzo ważny nowy typ danych o nazwie dict, która pochodzi od angielskiego słowa dictionary, czyli słownik.
W naszym programie słownik tworzymy tutaj:
fields = {}
Żeby zrozumieć, dlaczego jest potrzebny, wróćmy najpierw do list.
Przypomnienie – jak działała lista?
W pierwszym ćwiczeniu mieliśmy do czynienia z tą listą: failed_logins = [2, 5, 12, 3, 18, 7, 10, 1]. Lista przechowuje elementy w określonej kolejności. Każdy element ma swoją pozycję, czyli indeks:
indeks: 0 1 2 3 4 5 6 7
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
wartość: 2 5 12 3 18 7 10 1
Jeżeli chcemy pobrać pierwszą wartość z listy, używamy do tego celu indeksu failed_logins[0] i otrzymujemy wartość 2 itd. W przypadku listy pytamy:
Co znajduje się na pozycji numer 2?
To jest bardzo wygodne, kiedy istotna jest kolejność danych. Jednak w przypadku naszego logu pojawia się inny problem.
Dlaczego sama lista nie jest tutaj najlepszym rozwiązaniem?
Spójrz na pojedynczy rekord:
type=MALWARE src=203.0.113.42 hash=44d88612fea8a8f36de82e1278abb02f action=blocked
Mamy tutaj kilka różnych informacji:
typ zdarzenia → MALWARE
adres IP → 203.0.113.42
hash → 44d88612fea8a8f36de82e1278abb02f
akcja → blocked
Moglibyśmy oczywiście zapisać te dane jako listę:
event = [
"MALWARE",
"203.0.113.42",
"44d88612fea8a8f36de82e1278abb02f",
"blocked"
]
Ale wtedy musielibyśmy pamiętać:
indeks 0 → typ
indeks 1 → adres IP
indeks 2 → hash
indeks 3 → akcja
Aby pobrać adres IP, pisalibyśmy event[1]. Tylko czy patrząc na event[1] od razu wiemy, że 1 oznacza adres IP? Niekoniecznie. Przy czterech polach można to jeszcze zapamiętać. Przy logach mających kilkanaście lub kilkadziesiąt pól szybko stałoby się to niewygodne i podatne na pomyłki. I właśnie tutaj przydaje się słownik.
Co to jest słownik w Pythonie?
Słownik, czyli dict, przechowuje dane w postaci:
KLUCZ → WARTOŚĆ
Zamiast pytać: Co znajduje się na pozycji numer 1? możemy zapytać: Jaka wartość jest przypisana do klucza src?
Przykładowy słownik opisujący nasze zdarzenie może wyglądać tak:
event = {
"type": "MALWARE",
"src": "203.0.113.42",
"hash": "44d88612fea8a8f36de82e1278abb02f",
"action": "blocked"
}

Na pierwszy rzut oka jest dłuższy niż lista. Jest jednak znacznie bardziej opisowy. Nie musimy pamiętać, że adres IP znajduje się na pozycji 1. Możemy po prostu napisać event["src"] i otrzymamy 203.0.113.42.

Słownik jak formularz
Dobrym sposobem na zrozumienie słownika jest wyobrażenie sobie formularza. Na formularzu mamy pola:
Imię: Anna
Nazwisko: Kowalska
Miasto: Kraków
Wiek: 30
Po lewej stronie mamy nazwę pola:
Imię
Nazwisko
Miasto
Wiek
Po prawej jego wartość:
Anna
Kowalska
Kraków
30
Słownik działa podobnie:
KLUCZ WARTOŚĆ
type → MALWARE
src → 203.0.113.42
hash → 44d88612fea8a8f36de82e1278abb02f
action → blocked
Do czego służy klucz w słowniku?
Klucz identyfikuje konkretną informację znajdującą się w słowniku. Za pomocą klucza możemy pobierać wartości ze słownika. W przykładzie:
event = {
"type": "MALWARE",
"src": "203.0.113.42",
"action": "blocked"
}
kluczami są:
type
src
action
Co to jest wartość?
Wartość to właściwa informacja przypisana do klucza. W parze type → MALWARE słowo type jest kluczem, a MALWARE jest wartością. W src → 203.0.113.42 natomiast src jest kluczem, a 203.0.113.42 jest wartością. Możemy więc przedstawić słownik jako zestaw par:
KLUCZ WARTOŚĆ
↓ ↓
type → MALWARE
src → 203.0.113.42
hash → 444d88612fea8a8f36de82e1278abb02f
action → blocked
Taką parę często nazywamy parą klucz–wartość.
Klucz a indeks – jaka jest różnica?
To bardzo ważne rozróżnienie. W poprzednim ćwiczeniu korzystaliśmy z listy. Lista failed_logins = [2, 5, 12, 3] ma indeksy:
INDEKS WARTOŚĆ
0 → 2
1 → 5
2 → 12
3 → 3
Żeby dostać się do 12, używamy failed_logins[2]. Tutaj 2 jest indeksem, czyli pozycją elementu na liście. W słowniku:
event = {
"type": "MALWARE",
"src": "203.0.113.42"
}
nie musimy pytać o pozycję. Możemy napisać event["src"]. Tutaj "src" jest kluczem.
Czy klucz może się powtarzać?
W jednym słowniku klucze powinny być unikalne. Jeżeli mamy:
event = {
"type": "MALWARE",
"src": "203.0.113.42",
"action": "blocked"
}
to mamy jeden klucz src z jedną przypisaną do niego wartością. To przypomina formularz – nie chcemy mieć dwóch różnych pól o dokładnie tej samej nazwie, jeśli mają oznaczać tę samą informację.
Jak tworzymy pusty słownik?
W naszym programie znajduje się zapis fields = {}. Nawiasy klamrowe {} oznaczają tutaj pusty słownik. Na początku możemy go sobie wyobrazić jako pustą tabelę:
KLUCZ WARTOŚĆ
\--- ---
Nie ma jeszcze żadnych danych. Dopiero podczas analizy linii będziemy go wypełniać.
Jak dane trafiają do słownika?
Spójrzmy na ten fragment:
for token in line.split():
if "=" in token:
key, value = token.split("=", 1)
fields[key] = value
To jeden z najważniejszych fragmentów całego ćwiczenia. Rozłóżmy go na części. Mamy w logu linię:
type=MALWARE src=203.0.113.42 hash=44d88612fea8a8f36de82e1278abb02f action=blocked
Najpierw line.split() dzieli ją w miejscach występowania spacji. Otrzymujemy elementy:
type=MALWARE
src=203.0.113.42
hash=44d88612fea8a8f36de82e1278abb02f
action=blocked
Każdy taki element trafia kolejno do zmiennej token. Czyli w pierwszej iteracji token = "type=MALWARE" , w drugiej token = "src=203.0.113.42" i tak dalej.
Co robi token.split(„=”, 1) ?
Weźmy pod uwagę poniższy klucz i wartość: src=203.0.113.42. Program wykonuje token.split("=", 1) i dzieli tekst przy znaku =. Otrzymujemy dwie części:
src
203.0.113.42
Następnie key, value = token.split("=", 1) przypisuje pierwszą część do key, a drugą do value. Czyli:
key = "src"
value = "203.0.113.42"
Key i Value też są zmiennymi
Warto to podkreślić. W instrukcji key, value = token.split("=", 1) – key oraz value są zwykłymi zmiennymi. Ich wartości zmieniają się podczas kolejnych iteracji pętli. Najpierw:
key = "type"
value = "MALWARE"
potem:
key = "src"
value = "203.0.113.42"
następnie:
key = "hash"
value = "444d88612fea8a8f36de82e1278abb02f"
a później:
key = "action"
value = "blocked"
Jak zapisujemy parę klucz – wartość w słowniku?
Kiedy mamy już:
key = "src"
value = "203.0.113.42"
wykonujemy polecenie: fields[key] = value. Ponieważ aktualnie key = "src", możemy sobie wyobrazić, że Python wykonuje operację fields["src"] = "203.0.113.42" , co można przetłumaczyć jako: w słowniku fields zapisz pod kluczem "src" wartość "203.0.113.42". Po przetworzeniu całej linii słownik będzie wyglądał mniej więcej tak:
fields = {
"type": "MALWARE",
"src": "203.0.113.42",
"hash": "44d88612fea8a8f36de82e1278abb02f",
"action": "blocked"
}
Właśnie zamieniliśmy surowy tekst (raw data) na uporządkowaną strukturę danych.
Zobaczmy cały proces
Na początku mamy type=MALWARE src=203.0.113.42 hash=44d88612... action=blocked. Pierwsze dzielenie:
line.split()
↓
┌────────────────────┐
│ type=MALWARE │
│ src=203.0.113.42 │
│ hash=44d88612... │
│ action=blocked │
└────────────────────┘
Potem każdy element dzielimy przy =:
type=MALWARE
↓
type → MALWARE
src=203.0.113.42
↓
src → 203.0.113.42
hash=44d88612...
↓
hash → 44d88612...
action=blocked
↓
action → blocked
Na końcu otrzymujemy:
SŁOWNIK fields{}
type → MALWARE
src → 203.0.113.42
hash → 44d88612...
action → blocked
Po co zamieniamy tekst na słownik?
Ponieważ teraz możemy zadawać znacznie wygodniejsze pytania.
Na przykład:
Jaki jest typ zdarzenia?
fields["type"]
Jaki jest adres IP?
fields["src"]
Jaki jest hash?
fields["hash"]
Zamiast przeszukiwać za każdym razem całą linię tekstu, mamy uporządkowane dane.
fields.get(„type”) – jeszcze jeden sposób pobierania wartości
W naszym programie znajduje się następujący kod:
if fields.get("type") != "MALWARE":
continue
Tutaj używamy fields.get("type"). Funkcja .get() próbuje pobrać wartość przypisaną do klucza "type". Jeżeli słownik wygląda tak:
fields = {
"type": "MALWARE",
"src": "203.0.113.42"
}
to fields.get("type") zwróci MALWARE
fields[„type”] a fields.get(„type”)
To ważna różnica. Możemy pobierać dane ze słownika za pomocą fields["type"] albo fields.get("type"). Jeżeli klucz istnieje, oba sposoby zwrócą jego wartość. Różnica pojawia się wtedy, kiedy klucza nie ma. Załóżmy, że mamy taki słownik jak poniżej:
fields = {
"type": "MALWARE",
"src": "203.0.113.15"
}
Nie ma tutaj klucza hash. Jeżeli wykonamy polecenie fields.get("hash"). Python nie przerwie programu. Domyślnie przejdzie do kolejnej linii (otrzymamy wartość None).
Natomiast fields["hash"] oznacza: Oczekuję, że klucz "hash" istnieje. Pobierz jego wartość. Jeżeli go nie ma, Python zgłosi KeyError. I właśnie to wykorzystujemy w naszym ćwiczeniu.

Co dzieje się z uszkodzonym rekordem?
Mamy w logu rekord bez hasha: type=MALWARE src=203.0.113.15 action=blocked
Po przetworzeniu otrzymamy słownik:
fields = {
"type": "MALWARE",
"src": "203.0.113.15",
"action": "blocked"
}
Program wykonuje polecenie src = fields["src"]. Klucz "src" istnieje, więc wszystko działa. Następnie file_hash = fields["hash"]. Ale w słowniku nie ma klucza hash. Python zgłasza więc KeyError.
Co to jest KeyError?
KeyError oznacza w uproszczeniu: Próbujesz pobrać ze słownika wartość przy użyciu klucza, którego w tym słowniku nie ma.
To trochę tak, jakbyśmy mieli szafkę z podpisanymi szufladami:
type
src
action
i poprosili: Otwórz szufladę hash. Problem polega na tym, że taka szuflada nie istnieje. Python zgłasza więc błąd KeyError.
Dlaczego program się wtedy nie zatrzymuje?
Ponieważ kod znajduje się wewnątrz try:, a później mamy except KeyError as error:. Można to przeczytać jako: spróbuj wykonać ten kod. Jeżeli podczas jego wykonywania pojawi się KeyError, nie zatrzymuj całego programu – wykonaj instrukcje z sekcji except. Dlatego uszkodzony rekord powoduje wyświetlenie: Pominięto uszkodzony rekord. Brak pola: 'hash', a następnie: skipped += 1 zwiększa licznik pominiętych rekordów. Program może przejść do kolejnej linii logu.

processed i skipped – dwa liczniki
Na początku programu mamy:
processed = 0
skipped = 0
To podobny mechanizm jak w ćwiczeniu 1 w przypadku alert_count = 0 z pierwszego ćwiczenia. Tym razem liczymy dwie rzeczy. processed oznacza ile poprawnych rekordów MALWARE udało się przetworzyć. skipped oznacza: ile rekordów MALWARE musieliśmy pominąć z powodu brakujących danych. Po poprawnym przetworzeniu wykonujemy processed += 1. Po błędzie skipped += 1. Na końcu wyświetlamy podsumowanie:
print(f"Poprawnie przetworzono: {processed}")
print(f"Pominięto: {skipped}")
Co oznacza continue?
W tym ćwiczeniu pojawia się również nowe polecenie: continue, które oznacza w uproszczeniu: zakończ tę iterację pętli i przejdź od razu do następnej. Na przykład:
if fields.get("type") != "MALWARE":
continue
oznacza: jeżeli zdarzenie nie jest typu MALWARE, nie analizuj go dalej. Przejdź do kolejnej linii pliku. Jeżeli więc mamy type=CLEAN src=10.0.0.15 hash=abc123 action=allowed, to wtedy program stwierdza:
type = CLEAN
CLEAN != MALWARE
→ continue
i przechodzi do kolejnego rekordu. To pozwala nam filtrować dane.
Co robi with (open)?
Na początku głównej części programu znajduje się with open("malware.log", "r", encoding="utf-8") as file:. Ta instrukcja otwiera plik malware.log. Tryb "r" oznacza read, czyli otwórz plik do odczytu. Z kolei encoding="utf-8" określa sposób kodowania tekstu. Po as file możemy odwoływać się do otwartego pliku za pomocą zmiennej file.
Jak Python czyta plik?
Następnie for line in file: oznacza: pobieraj kolejno każdą linię z pliku. To kolejna pętla for. Tym razem nie przechodzimy jednak przez listę liczb. Przechodzimy przez linie pliku. Możemy to sobie wyobrazić tak:
malware.log
↓
linia 1
↓
analiza
↓
linia 2
↓
analiza
↓
linia 3
↓
analiza
↓
...
Każda aktualnie analizowana linia trafia do zmiennej line.
Co robi line.strip()?
Następnie przechodzimy do linii line = line.strip(), gdzie .strip() usuwa niepotrzebne białe znaki znajdujące się na początku i końcu tekstu, między innymi znak końca linii. Możemy to potraktować jako proste „oczyszczenie” tekstu przed dalszą analizą.
Co oznacza if not line?
W liniach
if not line:
continue
sprawdzamy, czy po oczyszczeniu linia jest pusta.
Jeżeli linia jest pusta:
pusta linia
↓
continue
↓
przejdź do następnej
Nie ma sensu próbować analizować rekordu, który nie zawiera żadnych danych.
Ważna uwaga – w tym kodzie nie używamy splitlines()
W tym konkretnym przykładzie plik jest czytany za pomocą for line in file:. Dlatego Python przekazuje nam kolejne linie pliku bez potrzeby wcześniejszego używania splitlines(). Dopiero dla każdej pojedynczej linii wykonujemy line.split() i dzielimy ją na poszczególne elementy. Czyli rzeczywisty proces w tym programie wygląda tak:
PLIK malware.log
↓
for line in file
↓
POJEDYNCZA LINIA
↓
line.strip()
↓
line.split()
↓
TOKENY
↓
token.split("=", 1)
↓
KLUCZ + WARTOŚĆ
↓
SŁOWNIK fields
Co warto zauważyć w tym kodzie?
Program nie zatrzymuje się po napotkaniu niekompletnego rekordu. To bardzo ważny wzorzec w analizie bezpieczeństwa:
BŁĘDNY REKORD
≠
ZATRZYMANIE CAŁEGO PROGRAMU
Jeżeli analizujemy tysiące albo miliony zdarzeń, pojedynczy uszkodzony wpis nie powinien automatycznie uniemożliwiać przetworzenia wszystkich pozostałych danych. Jednocześnie nie chcemy takiego problemu całkowicie ignorować. Dlatego skipped += 1 pozwala nam policzyć pominięte rekordy. Na końcu wiemy więc zarówno, ile danych udało się poprawnie przetworzyć, jak i ile rekordów wymagało pominięcia.
To jest pierwszy krok do normalizacji danych
Cały proces możemy podsumować tak:
SUROWY LOG
↓
ODCZYT LINII
↓
PODZIAŁ NA ELEMENTY
↓
ROZDZIELENIE KLUCZA I WARTOŚCI
↓
SŁOWNIK
↓
FILTROWANIE
↓
WALIDACJA WYMAGANYCH PÓL
↓
PRZETWARZANIE
↓
WYNIK
Właśnie dlatego to ćwiczenie jest ważnym krokiem dalej względem pierwszego. W ćwiczeniu 1 nauczyliśmy się procesu:
LISTA
↓
PĘTLA
↓
WARUNEK
↓
DECYZJA
Teraz dokładamy:
PLIK
↓
LINIA
↓
TEKST
↓
PARSOWANIE
↓
KLUCZ + WARTOŚĆ
↓
SŁOWNIK
↓
FILTROWANIE
↓
OBSŁUGA BŁĘDU
To już znacznie bardziej przypomina sposób, w jaki przygotowuje się dane do dalszej automatycznej analizy logów.
Najważniejsza różnica do zapamiętania
Jeżeli po tym ćwiczeniu masz zapamiętać jedną rzecz dotyczącą list i słowników, niech będzie to:
LISTA
element odnajdujemy za pomocą POZYCJI – indeksu
failed_logins[2]
↓
12
Natomiast:
SŁOWNIK
wartość odnajdujemy za pomocą NAZWY – klucza
fields["src"]
↓
203.0.113.42
Czyli indeks mówi „gdzie?”, a klucz mówi „czego szukam?”. Ta różnica będzie bardzo ważna w kolejnych ćwiczeniach, ponieważ dane dotyczące bezpieczeństwa często składają się z wielu nazwanych pól: adresu IP, użytkownika, czasu, typu zdarzenia, portu, hasha, akcji czy poziomu zagrożenia.
Analiza zagrożeń przy użyciu Pythona – czego naprawdę się uczymy?
Na poziomie Pythona poznajemy:
- zmienne,
- listy,
- słowniki,
- zbiory,
- pętle,
- warunki,
- funkcje,
- wyjątki,
- regex,
Counter,datetime.
Jednocześnie pracujemy z:
- logami,
- IoC,
- malware,
- brute force,
- password spraying,
- skanowaniem portów,
- Threat Intelligence,
- PowerShell,
- DNS,
- korelacją,
- risk scoringiem.
Dzięki temu kod przestaje być celem samym w sobie. Staje się narzędziem do rozwiązania konkretnego problemu.
Analiza zagrożeń przy użyciu Pythona a automatyzacja SOC
Spójrzmy teraz szerzej.
Firewall ─────┐
IDS/IPS ──────┤
EDR ──────────┼──→ ZBIERANIE DANYCH
Windows ──────┤
Linux ────────┘
↓
NORMALIZACJA
↓
KORELACJA
↓
DETEKCJA
↓
WZBOGACANIE
↓
OCENA RYZYKA
↓
ALERT
↓
ANALITYK
Nasze przykłady implementują niewielkie fragmenty tego procesu. Oczywiście nie budujemy pełnego SIEM. Mimo to możemy dobrze zrozumieć, jak dane przechodzą od surowego logu do alertu.
Gdzie w tym wszystkim pojawia się NIS2?
Automatyzacja może wspierać większy proces monitorowania i reagowania na incydenty. Nie oznacza to jednak, że pojedynczy skrypt zapewnia zgodność z NIS2. Znacznie trafniej traktować go jako element technicznych możliwości organizacji:
monitorowanie
→ wykrywanie
→ analiza
→ klasyfikacja
→ reakcja
→ raportowanie
Oficjalny tekst dyrektywy: Directive (EU) 2022/2555 – NIS2, EUR-Lex. Materiały ENISA: NIS2 – materiały ENISA.
False positive i false negative
Załóżmy, że analiza logu zwróciła nam następujące informacje:
admin
12 nieudanych logowań
03:15
nowy IP
Brzmi podejrzanie. Jednak administrator może mieć nocny dyżur, zmienione hasło i nowe połączenie VPN. Wtedy alert może być false positive, czyli fałszywie pozytywny. Z drugiej strony atakujący może działać powoli, tak aby nigdy nie przekroczyć ustawionego progu. W takim przypadku możemy mieć false negative, czyli zwrócenie informacji: spokojnie, nic złego w systemie się nie dzieje. Dlatego każda reguła jest kompromisem. Analiza zagrożeńprzy użyciu Pythona pozwala na pisanie skryptów w taki sposób, aby taka praca była jak najbardziej efektywna.
Python nie zastępuje analityka
Python wykonuje to, co mu zaprogramujemy. Jeżeli reguła jest zła, program bardzo szybko i konsekwentnie będzie generował błędne wyniki. Dlatego automatyzacja powinna wspierać analizę, a nie zastępować kontekst i ocenę sytuacji. To szczególnie ważne przy prostych ćwiczeniach, w których celowo upraszczamy rzeczywistość.
Gdzie znaleźć kod do ćwiczeń?
Wszystkie przykłady znajdują się tutaj: Repozytorium GitHub z ćwiczeniami. Jeżeli chcesz wykonywać przykłady na własnej maszynie Ubuntu, zacznij od: Virtual environment na Ubuntu – poradnik ZALNET.
Analiza zagrożeń przy użyciu Pythona – podsumowanie
Ten materiał przygotowałam jako uzupełnienie prowadzonych przeze mnie szkoleń z cyberbezpieczeństwa w programie SupermOcNA w IT. Jednocześnie zależy mi, żeby wpis był przydatny również dla innych osób uczących się Pythona i podstaw pracy z logami. Analiza zagrożeń przy użyciu Pythona nie musi zaczynać się od skomplikowanych frameworków. Najpierw wystarczy prosty if. Następnie można dodać logi, IOC, agregację, korelację, scoring i raportowanie. Co najważniejsze, nie warto ograniczać się do uruchamiania gotowego kodu.
Zmieniaj przykłady. Dodawaj własne dane. Modyfikuj progi. Wprowadzaj błędy. Sprawdzaj false positives. Rozbudowuj reguły. W efekcie zaczniesz rozumieć nie tylko sam Python, lecz także sposób myślenia potrzebny podczas analizy zagrożeń i pracy z automatyzacją bezpieczeństwa.
A jeśli szukasz bardziej rozbudowanego szkolenia z Pythona, to napisz do mnie.
