Build a Security Log Analyzer in Python
Parse SSH auth logs with regex to detect brute-force attacks.
Introduction
30 Second Summary
Every server connected to the internet faces thousands of login attempts per day. Security teams rely on log analysis to separate real attacks from background noise and respond before damage is done.
In this project, you will build a Python command-line tool that reads authentication logs, detects brute-force login attacks, and generates a professional threat report in JSON.
What You'll Build
A security log analyzer that parses Linux-style SSH authentication logs, identifies brute-force attack patterns using regular expressions, and outputs a structured JSON threat report with severity ratings.
By the end of this project, you'll have:
- A sample SSH authentication log file with realistic attack data simulating brute-force login attempts.
- A Python log analyzer script that parses logs, detects suspicious IPs, and classifies threats by severity.
- A JSON threat report containing flagged IP addresses, attempt counts, targeted usernames, and severity levels.
- Secret Mission: Add privilege escalation detection that catches suspicious sudo abuse patterns.
Are there any prerequisites?
You should be comfortable with basic Python concepts like variables, loops, functions, and if/else statements. No cybersecurity experience is required.
Set Up Your Project
Before you can analyze any security logs, you need a workspace with two key ingredients: the Python script that does the analysis, and a log file full of attack data to analyze.
In this step, you will create your project folder, verify Python is ready, and populate a sample log file with realistic SSH brute-force attack data.
In this step, get ready to:
- Create a project folder with your script and log files.
- Verify Python is working in your terminal.
- Build a sample authentication log simulating a real attack.
Create the project folder and files
Your project needs a dedicated folder to keep the script and log files together. You will create a folder called log-analyzer on your Desktop, then open it in VS Code.
- Press the Windows key to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the terminal inside VS Code by pressing Ctrl+` (that is the backtick key, above Tab).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder by running this command:
mkdir log-analyzer
- Now open that folder in VS Code. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the log-analyzer folder, and click Select Folder.
VS Code reloads with log-analyzer as your workspace. You should see the folder name in the Explorer sidebar on the left.
- In the Explorer sidebar, hover over the folder name and click the New File icon (a page with a plus sign).
- Type analyzer.py and press Enter.
- Click the New File icon again.
- Type sample_auth.log and press Enter.
- Confirm both files exist by checking the Explorer sidebar. You should see analyzer.py and sample_auth.log listed under the log-analyzer folder.
Don't see the Explorer sidebar?
If the sidebar is hidden, press Ctrl+Shift+E to toggle it back on. Make sure you selected the correct folder when opening. If VS Code shows an empty workspace, repeat the File > Open Folder step and navigate to Desktop > log-analyzer.
Help me open my project folder in VS Code
Verify Python is working
Your analyzer script runs on Python, so you need to confirm it is accessible from your VS Code terminal before writing any code.
- Open the VS Code terminal by pressing Ctrl+` (if it is not already open).
- Check your Python version by running this command:
python --version
You should see a version number like Python 3.13.5 (or any 3.x version). This confirms Python is installed and accessible from your terminal.
Seeing an error instead of a version number?
If you see a message that the command is not recognized, Python may not be added to your system PATH. Reinstall Python from python.org and make sure to check the Add Python to PATH checkbox during installation.
After reinstalling, close and reopen VS Code, then try the command again.
Help me fix Python not being recognized in my terminal
Create your sample log data
Your analyzer needs log data to work with. In a real environment, these logs come from a Linux server's authentication log file. For this project, you will create a sample log that simulates a brute-force attack in progress.
What is an authentication log?
Linux servers record every login attempt in a file called auth.log. Each line follows a standard format: Month Day Time Hostname sshd[PID]: Message.
When someone fails an SSH login, the log records which username they tried and what IP address they connected from. Security analysts look for patterns in these failures to catch attackers.
- Click on sample_auth.log in the Explorer sidebar to open it in the editor.
- Paste the following log data into the file:
Jun 4 22:14:15 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:16 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:18 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:20 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:22 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:25 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:28 server1 sshd[41460]: Accepted password for developer from 10.0.0.5 port 44892 ssh2
Jun 4 22:14:30 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:32 server1 sshd[41458]: Failed password for ubuntu from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:35 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:38 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:40 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:42 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:45 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:48 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:50 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:52 server1 sshd[41458]: Failed password for deploy from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:55 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:58 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:00 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:02 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:05 server1 sshd[41458]: Failed password for test from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:08 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:10 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:12 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:15 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:18 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:20 server1 sshd[41458]: Failed password for ubuntu from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:22 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:25 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:28 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:30 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:20:01 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:03 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:05 server1 sshd[41470]: Failed password for admin from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:08 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:10 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:12 server1 sshd[41470]: Failed password for ubuntu from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:15 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:18 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:20 server1 sshd[41470]: Failed password for admin from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:22 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:25 server1 sshd[41470]: Failed password for deploy from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:28 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:30 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:32 server1 sshd[41470]: Failed password for admin from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:35 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:38 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:40 server1 sshd[41471]: Accepted password for developer from 10.0.0.5 port 44900 ssh2
Jun 4 22:30:01 server1 sshd[41480]: Failed password for root from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:03 server1 sshd[41480]: Failed password for admin from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:05 server1 sshd[41480]: Failed password for test from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:08 server1 sshd[41480]: Failed password for root from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:10 server1 sshd[41480]: Failed password for root from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:12 server1 sshd[41480]: Failed password for user from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:15 server1 sshd[41480]: Failed password for root from 203.0.113.42 port 61234 ssh2
Jun 4 22:35:00 server1 sshd[41485]: Accepted password for admin from 10.0.0.10 port 55123 ssh2
Jun 4 22:40:01 server1 sshd[41490]: Failed password for root from 172.16.0.99 port 40001 ssh2
Jun 4 22:40:03 server1 sshd[41490]: Failed password for root from 172.16.0.99 port 40001 ssh2
Jun 4 22:40:05 server1 sshd[41490]: Failed password for admin from 172.16.0.99 port 40001 ssh2
What is in this log data?
This sample simulates a realistic brute-force attack scenario with four different attacker IPs:
- 192.168.1.100 hammers the server with 32 failed attempts across multiple usernames. This is the most aggressive attacker.
- 10.0.0.55 launches 17 failed attempts in a separate burst.
- 203.0.113.42 tries 7 times, a more cautious probe.
- 172.16.0.99 only tries 3 times, which may fall below detection thresholds.
The log also includes a few successful logins ("Accepted password") to mimic normal traffic mixed in with the attack.
- Save sample_auth.log by pressing Ctrl+S.
- Check the bottom-left corner of VS Code. You should see the file has 60 lines of log data.
File shows fewer lines than expected?
Make sure you copied all the log data above, starting from the first Jun 4 22:14:15 line through the last Jun 4 22:40:05 line. Also check that the file was empty before you pasted. If there is extra whitespace at the top, delete any blank lines above the first log entry.
Help me verify my sample log file has the right content
✔️ Awesome, I've got everything!
Great. Double check you have saved sample_auth.log and that analyzer.py exists (it should be empty for now).
ⓧ I'd like to double check the full code
Here is the complete content of sample_auth.log. Your file should match this exactly:
Jun 4 22:14:15 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:16 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:18 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:20 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:22 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:25 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:28 server1 sshd[41460]: Accepted password for developer from 10.0.0.5 port 44892 ssh2
Jun 4 22:14:30 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:32 server1 sshd[41458]: Failed password for ubuntu from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:35 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:38 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:40 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:42 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:45 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:48 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:50 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:52 server1 sshd[41458]: Failed password for deploy from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:55 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:14:58 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:00 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:02 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:05 server1 sshd[41458]: Failed password for test from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:08 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:10 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:12 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:15 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:18 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:20 server1 sshd[41458]: Failed password for ubuntu from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:22 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:25 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:28 server1 sshd[41458]: Failed password for admin from 192.168.1.100 port 52413 ssh2
Jun 4 22:15:30 server1 sshd[41458]: Failed password for root from 192.168.1.100 port 52413 ssh2
Jun 4 22:20:01 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:03 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:05 server1 sshd[41470]: Failed password for admin from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:08 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:10 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:12 server1 sshd[41470]: Failed password for ubuntu from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:15 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:18 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:20 server1 sshd[41470]: Failed password for admin from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:22 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:25 server1 sshd[41470]: Failed password for deploy from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:28 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:30 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:32 server1 sshd[41470]: Failed password for admin from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:35 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:38 server1 sshd[41470]: Failed password for root from 10.0.0.55 port 33201 ssh2
Jun 4 22:20:40 server1 sshd[41471]: Accepted password for developer from 10.0.0.5 port 44900 ssh2
Jun 4 22:30:01 server1 sshd[41480]: Failed password for root from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:03 server1 sshd[41480]: Failed password for admin from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:05 server1 sshd[41480]: Failed password for test from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:08 server1 sshd[41480]: Failed password for root from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:10 server1 sshd[41480]: Failed password for root from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:12 server1 sshd[41480]: Failed password for user from 203.0.113.42 port 61234 ssh2
Jun 4 22:30:15 server1 sshd[41480]: Failed password for root from 203.0.113.42 port 61234 ssh2
Jun 4 22:35:00 server1 sshd[41485]: Accepted password for admin from 10.0.0.10 port 55123 ssh2
Jun 4 22:40:01 server1 sshd[41490]: Failed password for root from 172.16.0.99 port 40001 ssh2
Jun 4 22:40:03 server1 sshd[41490]: Failed password for root from 172.16.0.99 port 40001 ssh2
Jun 4 22:40:05 server1 sshd[41490]: Failed password for admin from 172.16.0.99 port 40001 ssh2
Your analyzer.py file should be empty at this point. You will add code to it in the next step.
Your workspace is ready with real attack data to analyze. Next up, you will write Python code that uses regular expressions to extract the important details from each log entry.
Parse Log Entries with Regex
Your log file is ready with realistic attack data. Now you need a way to extract the useful information from each line.
Each log entry contains a timestamp, username, and source IP buried inside unstructured text. Regular expressions (regex) let you define a pattern that pulls out exactly the fields you need from each line.
In this step, get ready to:
- Import modules and define a compiled regex pattern.
- Write a log parsing function that extracts structured data.
- Test the parser against your sample log file.
Import modules and define the regex pattern
Before you can parse anything, you need a pattern that tells Python what to look for in each log line. The re module provides regex support. You will also import json, Counter from collections, and datetime for later steps.
- In VS Code, click on analyzer.py in the Explorer sidebar to open it.
- Add the imports and compiled regex pattern by pasting this code:
import re
import json
from collections import Counter
from datetime import datetime
# Regex pattern to extract failed SSH login attempts
# Matches: "Month Day Time Hostname sshd[PID]: Failed password for USER from IP port PORT ssh2"
FAILED_LOGIN_PATTERN = re.compile(
r"(\w+\s+\d+\s+\d+:\d+:\d+)\s+\S+\s+sshd\[\d+\]:\s+Failed password for\s+(\S+)\s+from\s+(\d+\.\d+\.\d+\.\d+)"
)
What does this code do?
The re.compile() function turns your regex string into a reusable pattern object. Compiling it once up front is faster than re-parsing the pattern on every line of the log.
The pattern has three capture groups (the parts inside parentheses):
- Group 1: the timestamp (e.g., Jun 4 22:14:15).
- Group 2: the username that failed to log in (e.g., root).
- Group 3: the source IP address (e.g., 192.168.1.100).
Why compile the pattern as a constant?
Real security logs can contain millions of lines. By compiling the regex once and storing it in FAILED_LOGIN_PATTERN, Python does not rebuild the pattern engine for every single line. This is the same approach professional log parsers use.
- Save analyzer.py.
Write the log parsing function
Now you need a function that opens the log file, scans every line against your pattern, and collects the matches into a structured list.
- In analyzer.py, add the parse_log_file() function below the FAILED_LOGIN_PATTERN definition:
def parse_log_file(filepath):
"""Parse the log file and extract failed login attempts."""
events = []
with open(filepath, "r") as f:
for line in f:
match = FAILED_LOGIN_PATTERN.search(line)
if match:
events.append({
"timestamp": match.group(1),
"username": match.group(2),
"source_ip": match.group(3),
})
return events
What does this function do?
- It opens the file using with open(), which automatically closes the file when done.
- For each line, FAILED_LOGIN_PATTERN.search(line) checks if the line matches a failed login entry.
- If there is a match, it pulls out the three capture groups (timestamp, username, source_ip) and stores them as a dictionary.
- Lines that do not match (like successful logins or system messages) are silently skipped.
- Save analyzer.py.
Test the parser
Before building more features, confirm that the regex actually extracts data from your log file.
- At the bottom of analyzer.py, add a quick test block:
# Quick test
if __name__ == "__main__":
events = parse_log_file("sample_auth.log")
print(f"Found {len(events)} failed login attempts")
What does this code do?
The if __name__ == "__main__": guard means this block only runs when you execute the file directly. It will not run when other scripts import your functions later.
It calls parse_log_file() on your sample log and prints how many failed attempts the regex found.
- Save analyzer.py.
- Run the script in your VS Code terminal by running this command:
python analyzer.py
- You should see: Found 57 failed login attempts.
Seeing 0 failed login attempts?
- Make sure sample_auth.log is in the same folder as analyzer.py. The script looks for the file in the current directory.
- Check that your log file entries contain Failed password for (with a capital F). The regex is case-sensitive.
- Verify your terminal is in the log-analyzer folder. Run cd to check your current location.
Why is my parser returning 0 results from the sample log?
✔️ Awesome, I've got everything!
Great. Double check you have saved analyzer.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete analyzer.py so far:
import re
import json
from collections import Counter
from datetime import datetime
# Regex pattern to extract failed SSH login attempts
# Matches: "Month Day Time Hostname sshd[PID]: Failed password for USER from IP port PORT ssh2"
FAILED_LOGIN_PATTERN = re.compile(
r"(\w+\s+\d+\s+\d+:\d+:\d+)\s+\S+\s+sshd\[\d+\]:\s+Failed password for\s+(\S+)\s+from\s+(\d+\.\d+\.\d+\.\d+)"
)
def parse_log_file(filepath):
"""Parse the log file and extract failed login attempts."""
events = []
with open(filepath, "r") as f:
for line in f:
match = FAILED_LOGIN_PATTERN.search(line)
if match:
events.append({
"timestamp": match.group(1),
"username": match.group(2),
"source_ip": match.group(3),
})
return events
# Quick test
if __name__ == "__main__":
events = parse_log_file("sample_auth.log")
print(f"Found {len(events)} failed login attempts")
Your parser is extracting structured data from raw log text. Next, you will build detection logic that flags suspicious IPs based on how many times they failed to log in.
Detect Brute-Force Attacks
Your parser can now extract structured data from raw log entries. The next challenge is identifying which of those failed attempts are actually dangerous.
A single failed login is harmless. But 30 failed attempts from the same IP address in one minute is a brute-force attack. In this step, you will build detection logic that counts failed attempts per IP and flags anything above a threshold.
In this step, get ready to:
- Build a detection function that counts failed attempts per IP using Counter.
- Classify flagged IPs into MEDIUM, HIGH, and CRITICAL severity levels.
- Test the detector and verify it produces real alerts from your sample data.
Build the detection function
The core idea behind brute-force detection is simple: count how many times each IP address fails, then flag anything above a threshold. collections.Counter handles the counting in a single line.
- In analyzer.py, find the parse_log_file() function. Add the following function below it (and above the # Quick test block):
def detect_brute_force(events, threshold=5):
"""Detect brute-force attacks based on failed login threshold."""
# Count attempts per IP
ip_counts = Counter(event["source_ip"] for event in events)
alerts = []
for ip, count in ip_counts.items():
if count >= threshold:
# Classify severity based on attempt count
if count >= 30:
severity = "CRITICAL"
elif count >= 15:
severity = "HIGH"
else:
severity = "MEDIUM"
# Collect which usernames the attacker targeted
targeted_users = list(set(
event["username"] for event in events if event["source_ip"] == ip
))
# Get the time window of the attack
ip_events = [event for event in events if event["source_ip"] == ip]
first_seen = ip_events[0]["timestamp"]
last_seen = ip_events[-1]["timestamp"]
alerts.append({
"source_ip": ip,
"attempt_count": count,
"severity": severity,
"targeted_users": targeted_users,
"first_seen": first_seen,
"last_seen": last_seen,
})
# Sort by attempt count (most dangerous first)
alerts.sort(key=lambda x: x["attempt_count"], reverse=True)
return alerts
What does this code do?
- Counter(event["source_ip"] for event in events) counts how many failed login events came from each IP address. One line does all the grouping and tallying.
- threshold=5 is the minimum number of failed attempts before an IP gets flagged. Anything below 5 is ignored as normal typo-level noise.
- The function returns a list of alert dictionaries sorted by attempt_count (most dangerous IPs first).
Classify severity levels
The function you just wrote assigns a severity rating to each flagged IP. This is the same tiered approach that commercial SIEM tools use to prioritize which alerts a security analyst investigates first.
How do the severity levels work?
- MEDIUM (5-14 attempts): Suspicious activity. Could be a misconfigured script or a slow scan.
- HIGH (15-29 attempts): Active attack in progress. An automated tool is cycling through credentials.
- CRITICAL (30+ attempts): Sustained brute-force assault. Immediate response needed.
For each flagged IP, the function also collects targeted_users (which accounts the attacker tried) and first_seen/last_seen timestamps (the attack window). These details help analysts decide how to respond.
Test the detector
Now you need to call your detection function and print the results to confirm it correctly identifies the attacks hiding in your sample data.
- In analyzer.py, find the # Quick test block at the bottom of your file.
- Find this line:
print(f"Found {len(events)} failed login attempts")
- Add the following lines directly below it:
alerts = detect_brute_force(events)
for alert in alerts:
print(f"[{alert['severity']}] {alert['source_ip']} - {alert['attempt_count']} attempts")
What does this test do?
This calls detect_brute_force(events) with the default threshold of 5, then loops through the returned alerts and prints each one with its severity, IP address, and attempt count.
- Save analyzer.py.
- Run the script by entering this command in your terminal:
python analyzer.py
- You should see output showing at least one [CRITICAL] alert for 192.168.1.100 (32 attempts) and one [HIGH] alert for 10.0.0.55 (17 attempts).
Not seeing any alerts?
- Make sure the detect_brute_force() function is placed ABOVE the # Quick test block. Python reads top to bottom, so the function must be defined before it is called.
- Check that your indentation inside the function uses 4 spaces consistently. Mixed tabs and spaces cause silent failures in Python.
- Verify that sample_auth.log is in the same folder as analyzer.py. If the parser finds 0 events, the detector has nothing to flag.
Help me debug my brute-force detection function
✔️ Awesome, I've got everything!
Great. Double check you have saved analyzer.py before moving on.
ⓧ I'd like to double check the full code
Here is the full analyzer.py file as it should look at the end of this step:
import re
import json
from collections import Counter
from datetime import datetime
# Regex pattern to extract failed SSH login attempts
FAILED_LOGIN_PATTERN = re.compile(
r"(\w+\s+\d+\s+\d+:\d+:\d+)\s+\S+\s+sshd\[\d+\]:\s+Failed password for\s+(\S+)\s+from\s+(\d+\.\d+\.\d+\.\d+)"
)
def parse_log_file(filepath):
"""Parse the log file and extract failed login attempts."""
events = []
with open(filepath, "r") as f:
for line in f:
match = FAILED_LOGIN_PATTERN.search(line)
if match:
events.append({
"timestamp": match.group(1),
"username": match.group(2),
"source_ip": match.group(3),
})
return events
def detect_brute_force(events, threshold=5):
"""Detect brute-force attacks based on failed login threshold."""
ip_counts = Counter(event["source_ip"] for event in events)
alerts = []
for ip, count in ip_counts.items():
if count >= threshold:
if count >= 30:
severity = "CRITICAL"
elif count >= 15:
severity = "HIGH"
else:
severity = "MEDIUM"
targeted_users = list(set(
event["username"] for event in events if event["source_ip"] == ip
))
ip_events = [event for event in events if event["source_ip"] == ip]
first_seen = ip_events[0]["timestamp"]
last_seen = ip_events[-1]["timestamp"]
alerts.append({
"source_ip": ip,
"attempt_count": count,
"severity": severity,
"targeted_users": targeted_users,
"first_seen": first_seen,
"last_seen": last_seen,
})
alerts.sort(key=lambda x: x["attempt_count"], reverse=True)
return alerts
# Quick test
if __name__ == "__main__":
events = parse_log_file("sample_auth.log")
print(f"Found {len(events)} failed login attempts")
alerts = detect_brute_force(events)
for alert in alerts:
print(f"[{alert['severity']}] {alert['source_ip']} - {alert['attempt_count']} attempts")
Your tool now detects brute-force attacks and classifies them by severity. Next up, you will turn these alerts into a professional JSON threat report that a security team can actually use.
Generate a JSON Threat Report
Your analyzer can now parse logs and flag suspicious IPs. The detection logic works, but the results only exist in memory while the script runs.
Professional security tools produce structured reports that other systems can consume. In this step, you will build a report generator that outputs a JSON threat report with metadata, severity summaries, and full alert details.
In this step, get ready to:
- Build a report generator function that structures detection results into a professional format.
- Write the report to a JSON file and print a console summary.
- Wire everything into a main function with command-line threshold support.
Build the report generator
Detection alerts are useful, but a structured report adds context that makes findings actionable. Real security reports include metadata about when and what was analyzed, a quick summary for triage, and the full alert details.
- In analyzer.py, add the following generate_report() function below your detect_brute_force() function:
def generate_report(alerts, total_events, log_file):
"""Generate a structured JSON threat report."""
severity_counts = Counter(alert["severity"] for alert in alerts)
report = {
"report_metadata": {
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"log_file_analyzed": log_file,
"total_failed_logins": total_events,
},
"summary": {
"total_alerts": len(alerts),
"critical": severity_counts.get("CRITICAL", 0),
"high": severity_counts.get("HIGH", 0),
"medium": severity_counts.get("MEDIUM", 0),
},
"alerts": alerts,
}
return report
What does this code do?
This function takes three inputs: the list of alerts from your detector, the total number of failed login events, and the log file name.
- severity_counts uses Counter again to tally how many alerts fall into each severity level.
- report_metadata records when the report was generated and what file was analyzed.
- summary gives a quick at-a-glance count of alerts by severity for fast triage.
- alerts includes the full details so an analyst can drill into each flagged IP.
- Save analyzer.py.
Seeing a NameError for Counter or datetime?
Make sure your imports at the top of the file still include from collections import Counter and from datetime import datetime. If you accidentally deleted them, add them back.
Help me fix a NameError in my generate_report function.
Write the report to a JSON file
The report function builds a dictionary, but you need to serialize it to a file that other tools can read. You also need to add a new import and clean up the old test code to make room for a proper entry point.
- In analyzer.py, scroll to the top and find your import lines:
import re
import json
from collections import Counter
from datetime import datetime
- Add import sys between import json and from collections import Counter, so the imports look like this:
import re
import json
import sys
from collections import Counter
from datetime import datetime
Why sys?
The sys module gives your script access to command-line arguments via sys.argv. This lets a user pass a custom detection threshold when running the tool.
- Now scroll to the bottom of the file and find the quick test block:
# Quick test
if __name__ == "__main__":
events = parse_log_file("sample_auth.log")
print(f"Found {len(events)} failed login attempts")
alerts = detect_brute_force(events)
for alert in alerts:
print(f"[{alert['severity']}] {alert['source_ip']} - {alert['attempt_count']} attempts")
- Select that entire block (from # Quick test to the last line) and delete it.
- Save analyzer.py.
Wire everything together with a main function
A main() function orchestrates the entire pipeline: parse, detect, report. It also handles writing the JSON file and printing a console summary so you get immediate feedback.
- At the bottom of analyzer.py (where the old test block was), add the following main() function and entry-point guard:
def main():
log_file = "sample_auth.log"
threshold = 5
# Allow custom threshold from command line
if len(sys.argv) > 1:
try:
threshold = int(sys.argv[1])
except ValueError:
print(f"Invalid threshold: {sys.argv[1]}. Using default: 5")
print(f"[*] Security Log Analyzer")
print(f"[*] Analyzing: {log_file}")
print(f"[*] Detection threshold: {threshold} failed attempts")
print("-" * 50)
# Step 1: Parse
events = parse_log_file(log_file)
print(f"[+] Parsed {len(events)} failed login attempts")
# Step 2: Detect
alerts = detect_brute_force(events, threshold)
print(f"[!] Detected {len(alerts)} suspicious IPs")
# Step 3: Report
report = generate_report(alerts, len(events), log_file)
# Write JSON report
output_file = "threat_report.json"
with open(output_file, "w") as f:
json.dump(report, f, indent=2)
print("-" * 50)
print(f"[+] Report saved to: {output_file}")
print()
# Print console summary
for alert in alerts:
print(f" [{alert['severity']}] {alert['source_ip']} - {alert['attempt_count']} attempts")
print(f" Targeted users: {', '.join(alert['targeted_users'])}")
print(f" Window: {alert['first_seen']} -> {alert['last_seen']}")
print()
if __name__ == "__main__":
main()
What does this code do?
- sys.argv[1] checks if the user passed a custom threshold number when running the script. If not, it defaults to 5.
- The pipeline calls parse_log_file(), detect_brute_force(), and generate_report() in sequence, printing progress at each stage.
- json.dump(report, f, indent=2) serializes the report dictionary to a nicely formatted JSON file.
- The console summary loop prints each alert so you get immediate feedback without opening the JSON file.
- if __name__ == "__main__" ensures main() only runs when you execute the script directly, not when importing it as a module.
- Save analyzer.py.
- Run the complete tool by entering this command in your terminal:
python analyzer.py
You should see output showing the parse count, detected IPs, and a console summary with severity levels for each flagged IP.
- Confirm that a new file called threat_report.json appeared in your log-analyzer folder by running:
ls
You should see analyzer.py, sample_auth.log, and threat_report.json listed.
Seeing a NameError for sys?
Make sure you added import sys to the top of your file. It should appear between import json and from collections import Counter.
If you see FileNotFoundError, make sure your terminal is inside the log-analyzer folder where sample_auth.log lives.
Help me troubleshoot my security log analyzer script.
✔️ Awesome, I've got everything!
Great. Double check you have saved analyzer.py and that threat_report.json exists in your project folder.
ⓧ I'd like to double check the full code
import re
import json
import sys
from collections import Counter
from datetime import datetime
# Regex pattern to extract failed SSH login attempts
FAILED_LOGIN_PATTERN = re.compile(
r"(\w+\s+\d+\s+\d+:\d+:\d+)\s+\S+\s+sshd\[\d+\]:\s+Failed password for\s+(\S+)\s+from\s+(\d+\.\d+\.\d+\.\d+)"
)
def parse_log_file(filepath):
"""Parse the log file and extract failed login attempts."""
events = []
with open(filepath, "r") as f:
for line in f:
match = FAILED_LOGIN_PATTERN.search(line)
if match:
events.append({
"timestamp": match.group(1),
"username": match.group(2),
"source_ip": match.group(3),
})
return events
def detect_brute_force(events, threshold=5):
"""Detect brute-force attacks based on failed login threshold."""
ip_counts = Counter(event["source_ip"] for event in events)
alerts = []
for ip, count in ip_counts.items():
if count >= threshold:
if count >= 30:
severity = "CRITICAL"
elif count >= 15:
severity = "HIGH"
else:
severity = "MEDIUM"
targeted_users = list(set(
event["username"] for event in events if event["source_ip"] == ip
))
ip_events = [event for event in events if event["source_ip"] == ip]
first_seen = ip_events[0]["timestamp"]
last_seen = ip_events[-1]["timestamp"]
alerts.append({
"source_ip": ip,
"attempt_count": count,
"severity": severity,
"targeted_users": targeted_users,
"first_seen": first_seen,
"last_seen": last_seen,
})
alerts.sort(key=lambda x: x["attempt_count"], reverse=True)
return alerts
def generate_report(alerts, total_events, log_file):
"""Generate a structured JSON threat report."""
severity_counts = Counter(alert["severity"] for alert in alerts)
report = {
"report_metadata": {
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"log_file_analyzed": log_file,
"total_failed_logins": total_events,
},
"summary": {
"total_alerts": len(alerts),
"critical": severity_counts.get("CRITICAL", 0),
"high": severity_counts.get("HIGH", 0),
"medium": severity_counts.get("MEDIUM", 0),
},
"alerts": alerts,
}
return report
def main():
log_file = "sample_auth.log"
threshold = 5
if len(sys.argv) > 1:
try:
threshold = int(sys.argv[1])
except ValueError:
print(f"Invalid threshold: {sys.argv[1]}. Using default: 5")
print(f"[*] Security Log Analyzer")
print(f"[*] Analyzing: {log_file}")
print(f"[*] Detection threshold: {threshold} failed attempts")
print("-" * 50)
events = parse_log_file(log_file)
print(f"[+] Parsed {len(events)} failed login attempts")
alerts = detect_brute_force(events, threshold)
print(f"[!] Detected {len(alerts)} suspicious IPs")
report = generate_report(alerts, len(events), log_file)
output_file = "threat_report.json"
with open(output_file, "w") as f:
json.dump(report, f, indent=2)
print("-" * 50)
print(f"[+] Report saved to: {output_file}")
print()
for alert in alerts:
print(f" [{alert['severity']}] {alert['source_ip']} - {alert['attempt_count']} attempts")
print(f" Targeted users: {', '.join(alert['targeted_users'])}")
print(f" Window: {alert['first_seen']} -> {alert['last_seen']}")
print()
if __name__ == "__main__":
main()
Secret mission
Detect Privilege Escalation
Your analyzer catches brute-force attacks from the outside. But what about threats that are already inside? Add a second detection capability that catches suspicious sudo abuse patterns, the signature of an attacker who has gained initial access and is now trying to become root.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally on your machine, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files, or delete them entirely.
Resources you used:
- The log-analyzer folder on your Desktop (contains analyzer.py, sample_auth.log, and threat_report.json).
Keep everything running
No action needed. Choose this if you want to keep experimenting with your security log analyzer, add new detection rules, or reference the code later.
- Your log-analyzer folder stays on your Desktop.
- You can run python analyzer.py anytime to regenerate reports with new log data.
Pause - I'll come back to this later
Since this is a local project with no running processes, there is nothing to pause. Your files sit quietly on disk until you need them again.
- Close VS Code to free up memory.
- Your project files remain in the log-analyzer folder on your Desktop, ready for next time.
Delete - I don't want to use this again
Remove all project files from your machine. This is permanent.
- Close VS Code.
- Delete the log-analyzer folder from your Desktop by running one of the following commands in your terminal.
macOS
rm -rf ~/Desktop/log-analyzer
Windows
Remove-Item -Recurse -Force ~\Desktop\log-analyzer
- Confirm the folder is gone by checking your Desktop.
Nice Work!
Nice Work!
You just built a real cybersecurity tool from scratch using Python. Your security log analyzer does exactly what a Tier 1 SOC analyst does every day: ingest logs, identify anomalies, and escalate findings.
You've learned how to:
- Built a regex-based log parser that extracts structured data from unstructured text.
- Implemented threshold-based detection logic using the same approach found in commercial SIEMs.
- Generated professional JSON threat reports with severity classification using only Python standard library modules.
- Added privilege escalation detection for sudo abuse monitoring.
Ready to quiz yourself?