Skip to content

Lab 01 — Build a Security Log Analyzer

Difficulty: Beginner → Intermediate
Estimated Time: 90–120 minutes
Primary Language: Python
Security Domain: SOC / Detection / Incident Investigation
Environment: Local lab only
Automation Type: Defensive Security Analytics

Your task is to build a Python-based security log analyzer that converts raw authentication logs into an analyst-ready security report.

You will take data that looks like this:

2026-08-29T09:00:11Z user=admin01 source_ip=10.10.10.50 action=login status=failed
2026-08-29T09:00:34Z user=admin01 source_ip=10.10.10.50 action=login status=failed
2026-08-29T09:01:08Z user=admin01 source_ip=10.10.10.50 action=login status=failed
2026-08-29T09:05:42Z user=admin01 source_ip=10.10.10.50 action=login status=success

and turn it into:

TOTAL EVENTS
FAILED LOGINS
USERS WITH REPEATED FAILURES
TOP SOURCE IPs
FAILURE → SUCCESS SEQUENCES
ANALYST REVIEW ITEMS

The final tool should generate:

CSV REPORTS
JSON SUMMARY
MARKDOWN INVESTIGATION REPORT

Security teams rarely work with perfectly formatted information.

Instead, analysts receive:

RAW LOGS
LARGE DATASETS
INCONSISTENT VALUES
DUPLICATE EVENTS
MISSING FIELDS
MULTIPLE USERS
MULTIPLE SOURCE IPs

Your job is to transform:

RAW DATA

into:

SECURITY CONTEXT

This is one of the most important skills in:

SOC ANALYSIS
THREAT HUNTING
INCIDENT RESPONSE
DETECTION ENGINEERING
SECURITY AUTOMATION

By completing this lab, you should be able to:

READ SECURITY LOG FILES
PARSE STRUCTURED FIELDS
VALIDATE SECURITY DATA
NORMALIZE VALUES
COUNT FAILED AUTHENTICATIONS
GROUP EVENTS BY USER
GROUP EVENTS BY SOURCE IP
BUILD A TIMELINE
IDENTIFY FAILURE → SUCCESS PATTERNS
EXPORT CSV
EXPORT JSON
GENERATE MARKDOWN REPORTS
AUTHENTICATION LOG
PYTHON SCRIPT
LINE PARSER
VALIDATION
NORMALIZATION
EVENT OBJECTS
ANALYSIS ENGINE
┌────┼────────┐
↓ ↓ ↓
USERS IPs TIMELINE
↓ ↓ ↓
└────┼────────┘
SECURITY FINDINGS
REPORT GENERATOR
┌────┼──────┐
↓ ↓ ↓
CSV JSON MD

You are working as a junior SOC analyst.

Your team receives an authentication log from a Linux-hosted application.

An alert indicates:

MULTIPLE FAILED LOGIN ATTEMPTS

Your manager asks you to determine:

How many authentication events occurred?
How many failed logins occurred?
Which users had the most failures?
Which IP addresses generated the most failures?
Did any successful login occur after repeated failures?
Which events should be reviewed by an analyst?

Instead of manually reviewing the file, you will build a reusable Python analyzer.

This lab uses:

SYNTHETIC AUTHENTICATION DATA

Do not collect or analyze authentication records from systems unless you are:

THE OWNER
AN AUTHORIZED ADMINISTRATOR
OR
EXPLICITLY AUTHORIZED TO PERFORM THE ANALYSIS

Create:

security-log-analyzer/
|
+-- data/
|
| +-- authentication.log
|
+-- reports/
|
+-- src/
|
| +-- analyzer.py
|
+-- README.md

Open your terminal.

Create the main directory:

Terminal window
mkdir security-log-analyzer

Enter:

Terminal window
cd security-log-analyzer

Create subdirectories:

Terminal window
mkdir data
mkdir reports
mkdir src

Your structure should now look like:

security-log-analyzer/
|
+-- data/
+-- reports/
+-- src/

Run:

Terminal window
python --version

or:

Terminal window
python3 --version

Recommended:

Python 3.10+

Create:

data/authentication.log

Add the following synthetic events:

2026-08-29T09:00:11Z user=admin01 source_ip=10.10.10.50 action=login status=failed
2026-08-29T09:00:34Z user=admin01 source_ip=10.10.10.50 action=login status=failed
2026-08-29T09:01:08Z user=admin01 source_ip=10.10.10.50 action=login status=failed
2026-08-29T09:02:16Z user=analyst01 source_ip=10.10.10.40 action=login status=success
2026-08-29T09:03:02Z user=user01 source_ip=10.10.10.60 action=login status=failed
2026-08-29T09:03:18Z user=user02 source_ip=10.10.10.60 action=login status=failed
2026-08-29T09:03:44Z user=user03 source_ip=10.10.10.60 action=login status=failed
2026-08-29T09:04:10Z user=user04 source_ip=10.10.10.60 action=login status=failed
2026-08-29T09:04:22Z user=user05 source_ip=10.10.10.60 action=login status=failed
2026-08-29T09:05:42Z user=admin01 source_ip=10.10.10.50 action=login status=success
2026-08-29T09:06:03Z user=user01 source_ip=10.10.10.70 action=login status=failed
2026-08-29T09:06:19Z user=user01 source_ip=10.10.10.70 action=login status=failed
2026-08-29T09:06:47Z user=user01 source_ip=10.10.10.70 action=login status=failed
2026-08-29T09:07:15Z user=analyst01 source_ip=10.10.10.40 action=login status=success
2026-08-29T09:08:00Z user=user02 source_ip=invalid-ip action=login status=failed
2026-08-29T09:08:25Z user=admin02 source_ip=10.10.10.80 action=logout status=success

This dataset intentionally contains:

REPEATED FAILURES
MULTIPLE USERS FROM ONE SOURCE
SUCCESS AFTER FAILURE
INVALID IP DATA
NON-LOGIN ACTIVITY

Each log line contains:

TIMESTAMP
USER
SOURCE_IP
ACTION
STATUS

Example:

2026-08-29T09:00:11Z
user=admin01
source_ip=10.10.10.50
action=login
status=failed

Step 05 — Define the Internal Event Structure

Section titled “Step 05 — Define the Internal Event Structure”

Your script should convert each log line into:

{
"timestamp": "...",
"user": "...",
"source_ip": "...",
"action": "...",
"status": "..."
}

Conceptually:

RAW TEXT LINE
PARSE
PYTHON DICTIONARY

Create:

src/analyzer.py

Start with:

from pathlib import Path
from collections import Counter, defaultdict
import csv
import json
import ipaddress

Add:

BASE_DIR = Path(__file__).resolve().parent.parent
LOG_FILE = BASE_DIR / "data" / "authentication.log"
REPORT_DIR = BASE_DIR / "reports"

Create the report directory automatically:

REPORT_DIR.mkdir(
parents=True,
exist_ok=True
)

Add:

def parse_log_line(line):
parts = line.strip().split()
if len(parts) < 5:
return None
event = {
"timestamp": parts[0]
}
for item in parts[1:]:
if "=" not in item:
continue
key, value = item.split("=", 1)
event[key] = value
return event

Input:

2026-08-29T09:00:11Z user=admin01 source_ip=10.10.10.50 action=login status=failed

Output:

{
"timestamp": "2026-08-29T09:00:11Z",
"user": "admin01",
"source_ip": "10.10.10.50",
"action": "login",
"status": "failed"
}

Add:

REQUIRED_FIELDS = {
"timestamp",
"user",
"source_ip",
"action",
"status"
}

Create:

def has_required_fields(event):
return REQUIRED_FIELDS.issubset(
event.keys()
)

Never assume logs are complete.

A malformed event could look like:

user=admin01 status=failed

Your analyzer should recognize incomplete data.

Create:

def is_valid_ip(value):
try:
ipaddress.ip_address(value)
return True
except ValueError:
return False

Create:

def normalize_event(event):
return {
"timestamp": event["timestamp"].strip(),
"user": event["user"].strip().lower(),
"source_ip": event["source_ip"].strip(),
"action": event["action"].strip().lower(),
"status": event["status"].strip().lower()
}

Normalization ensures:

Admin01
ADMIN01
admin01

become:

admin01

Create:

def load_events():
valid_events = []
invalid_events = []
with LOG_FILE.open(
"r",
encoding="utf-8"
) as file:
for line_number, line in enumerate(
file,
start=1
):
if not line.strip():
continue
event = parse_log_line(line)
if not event:
invalid_events.append({
"line": line_number,
"reason": "Unable to parse",
"raw": line.strip()
})
continue
if not has_required_fields(event):
invalid_events.append({
"line": line_number,
"reason": "Missing required field",
"raw": line.strip()
})
continue
event = normalize_event(event)
if not is_valid_ip(
event["source_ip"]
):
invalid_events.append({
"line": line_number,
"reason": "Invalid IP address",
"raw": line.strip()
})
continue
valid_events.append(event)
return valid_events, invalid_events

At the bottom temporarily add:

events, invalid = load_events()
print(
f"Valid events: {len(events)}"
)
print(
f"Invalid events: {len(invalid)}"
)

Run:

Terminal window
python src/analyzer.py

You should see approximately:

Valid events: 15
Invalid events: 1

Step 14 — Focus on Authentication Events

Section titled “Step 14 — Focus on Authentication Events”

The dataset also contains:

logout

Create:

def get_login_events(events):
return [
event
for event in events
if event["action"] == "login"
]

Create:

def split_login_events(events):
failed = []
successful = []
for event in events:
if event["status"] == "failed":
failed.append(event)
elif event["status"] == "success":
successful.append(event)
return failed, successful

Create:

def count_failures_by_user(events):
return Counter(
event["user"]
for event in events
)

Example result:

admin01 3
user01 4
user02 1

Create:

def count_failures_by_ip(events):
return Counter(
event["source_ip"]
for event in events
)

Step 18 — Identify Repeated User Failures

Section titled “Step 18 — Identify Repeated User Failures”

Choose a training threshold:

FAILED_USER_THRESHOLD = 3

Create:

def find_repeated_user_failures(
failure_counts
):
return {
user: count
for user, count
in failure_counts.items()
if count >= FAILED_USER_THRESHOLD
}

The value:

3

is a lab threshold.

It is not a universal detection rule.

Real detection thresholds should consider:

BASELINE
TIME WINDOW
USER TYPE
ENVIRONMENT
APPLICATION
FALSE POSITIVES

Step 19 — Identify High-Volume Source IPs

Section titled “Step 19 — Identify High-Volume Source IPs”

Set:

FAILED_IP_THRESHOLD = 3

Create:

def find_high_volume_ips(
failure_counts
):
return {
ip: count
for ip, count
in failure_counts.items()
if count >= FAILED_IP_THRESHOLD
}

Step 20 — Count Unique Users per Source IP

Section titled “Step 20 — Count Unique Users per Source IP”

This can help identify:

ONE SOURCE
MANY ACCOUNTS

Create:

def users_by_source_ip(events):
result = defaultdict(set)
for event in events:
result[
event["source_ip"]
].add(
event["user"]
)
return result

Create:

def find_multi_user_sources(
source_users
):
return {
source_ip: sorted(users)
for source_ip, users
in source_users.items()
if len(users) >= 3
}

Again:

MULTIPLE USERS
FROM ONE SOURCE

does not automatically mean an attack.

Possible legitimate causes include:

NAT
VPN
PROXY
SHARED SYSTEM
AUTOMATION

Sort events:

def build_timeline(events):
return sorted(
events,
key=lambda event:
event["timestamp"]
)

Because the synthetic timestamps use ISO format, lexical sorting works correctly here.

Step 23 — Identify Success After Failures

Section titled “Step 23 — Identify Success After Failures”

This is an important investigation pattern.

You want to identify:

USER
FAILED LOGIN
FAILED LOGIN
FAILED LOGIN
SUCCESS

Create:

def find_success_after_failures(
login_events
):
failure_state = defaultdict(int)
findings = []
for event in build_timeline(
login_events
):
key = (
event["user"],
event["source_ip"]
)
if event["status"] == "failed":
failure_state[key] += 1
elif event["status"] == "success":
failed_count = failure_state[key]
if failed_count >= 3:
findings.append({
"user": event["user"],
"source_ip":
event["source_ip"],
"previous_failures":
failed_count,
"success_time":
event["timestamp"]
})
failure_state[key] = 0
return findings

For:

admin01

you should see:

3 FAILURES
1 SUCCESS

This should create:

ANALYST REVIEW ITEM

It does not automatically prove compromise.

Possible explanations include:

USER FORGOT PASSWORD
KEYBOARD ERROR
STALE SAVED CREDENTIAL
APPLICATION ISSUE
AUTHORIZED PASSWORD TESTING
ACCOUNT ATTACK

Automation identifies:

PATTERN

The analyst determines:

MEANING

Create:

def analyze_events(events):
login_events = get_login_events(
events
)
failed, successful = \
split_login_events(
login_events
)
user_failures = \
count_failures_by_user(
failed
)
ip_failures = \
count_failures_by_ip(
failed
)
source_users = \
users_by_source_ip(
failed
)
repeated_users = \
find_repeated_user_failures(
user_failures
)
high_volume_ips = \
find_high_volume_ips(
ip_failures
)
multi_user_sources = \
find_multi_user_sources(
source_users
)
success_after_failures = \
find_success_after_failures(
login_events
)
return {
"total_events":
len(events),
"login_events":
len(login_events),
"failed_logins":
len(failed),
"successful_logins":
len(successful),
"failure_counts_by_user":
dict(user_failures),
"failure_counts_by_ip":
dict(ip_failures),
"repeated_failure_users":
repeated_users,
"high_volume_sources":
high_volume_ips,
"multi_user_sources":
multi_user_sources,
"success_after_failures":
success_after_failures
}

Add:

events, invalid_events = load_events()
analysis = analyze_events(events)
print(
json.dumps(
analysis,
indent=2
)
)

Run:

Terminal window
python src/analyzer.py

Review the output.

Step 28 — Expected Security Observations

Section titled “Step 28 — Expected Security Observations”

Your results should identify patterns similar to:

admin01
→ repeated failures
user01
→ repeated failures
10.10.10.50
→ repeated failed attempts
10.10.10.60
→ multiple user accounts
10.10.10.70
→ repeated attempts against user01
admin01
→ successful login after repeated failures

Create:

def export_failure_users(
analysis
):
output_file = (
REPORT_DIR /
"failed-users.csv"
)
rows = sorted(
analysis[
"failure_counts_by_user"
].items(),
key=lambda item: item[1],
reverse=True
)
with output_file.open(
"w",
newline="",
encoding="utf-8"
) as file:
writer = csv.writer(file)
writer.writerow([
"user",
"failed_logins"
])
for user, count in rows:
writer.writerow([
user,
count
])

Create:

def export_source_ips(
analysis
):
output_file = (
REPORT_DIR /
"source-ip-summary.csv"
)
rows = sorted(
analysis[
"failure_counts_by_ip"
].items(),
key=lambda item: item[1],
reverse=True
)
with output_file.open(
"w",
newline="",
encoding="utf-8"
) as file:
writer = csv.writer(file)
writer.writerow([
"source_ip",
"failed_logins"
])
for source_ip, count in rows:
writer.writerow([
source_ip,
count
])

Create:

def export_invalid_events(
invalid_events
):
output_file = (
REPORT_DIR /
"invalid-events.csv"
)
with output_file.open(
"w",
newline="",
encoding="utf-8"
) as file:
writer = csv.DictWriter(
file,
fieldnames=[
"line",
"reason",
"raw"
]
)
writer.writeheader()
writer.writerows(
invalid_events
)

Do not silently discard bad security data.

You should know:

WHAT WAS REJECTED?
WHY?
HOW MUCH DATA WAS LOST?

Create:

def export_json_summary(
analysis
):
output_file = (
REPORT_DIR /
"security-summary.json"
)
with output_file.open(
"w",
encoding="utf-8"
) as file:
json.dump(
analysis,
file,
indent=2
)

Create:

def generate_markdown_report(
analysis,
invalid_events
):
report = []
report.append(
"# Security Log Investigation Report"
)
report.append("")
report.append(
"## Executive Summary"
)
report.append("")
report.append(
f"- Total valid events: "
f"{analysis['total_events']}"
)
report.append(
f"- Login events: "
f"{analysis['login_events']}"
)
report.append(
f"- Failed logins: "
f"{analysis['failed_logins']}"
)
report.append(
f"- Successful logins: "
f"{analysis['successful_logins']}"
)
report.append(
f"- Invalid events: "
f"{len(invalid_events)}"
)
report.append("")
report.append(
"## Repeated Failure Users"
)
report.append("")
for user, count in analysis[
"repeated_failure_users"
].items():
report.append(
f"- {user}: {count}"
)
report.append("")
report.append(
"## High-Volume Source IPs"
)
report.append("")
for source_ip, count in analysis[
"high_volume_sources"
].items():
report.append(
f"- {source_ip}: {count}"
)
report.append("")
report.append(
"## Multi-User Sources"
)
report.append("")
for source_ip, users in analysis[
"multi_user_sources"
].items():
report.append(
f"- {source_ip}: "
f"{', '.join(users)}"
)
report.append("")
report.append(
"## Successful Login After Failures"
)
report.append("")
for item in analysis[
"success_after_failures"
]:
report.append(
"- "
f"{item['user']} from "
f"{item['source_ip']} "
f"succeeded after "
f"{item['previous_failures']} "
f"failed attempts at "
f"{item['success_time']}"
)
report.append("")
report.append(
"## Analyst Recommendation"
)
report.append("")
report.append(
"Review repeated authentication "
"failures and any successful login "
"following repeated failures. "
"Correlate these events with identity, "
"asset, endpoint, VPN, and application "
"telemetry before determining whether "
"the activity is malicious."
)
output_file = (
REPORT_DIR /
"investigation-report.md"
)
output_file.write_text(
"\n".join(report),
encoding="utf-8"
)

Create:

def main():
events, invalid_events = \
load_events()
analysis = analyze_events(
events
)
export_failure_users(
analysis
)
export_source_ips(
analysis
)
export_invalid_events(
invalid_events
)
export_json_summary(
analysis
)
generate_markdown_report(
analysis,
invalid_events
)
print(
"Security log analysis complete."
)
print(
f"Reports saved to: "
f"{REPORT_DIR}"
)

At the bottom:

if __name__ == "__main__":
main()

Run:

Terminal window
python src/analyzer.py

Expected:

Security log analysis complete.
Reports saved to: ...

You should now have:

reports/
|
+-- failed-users.csv
|
+-- source-ip-summary.csv
|
+-- invalid-events.csv
|
+-- security-summary.json
|
+-- investigation-report.md

Expected structure:

user,failed_logins
user01,4
admin01,3
user02,1
user03,1
user04,1
user05,1

Expected structure:

source_ip,failed_logins
10.10.10.60,4
10.10.10.50,3
10.10.10.70,3

Your exact ordering can differ when counts are tied.

Step 40 — Review the Invalid Event Report

Section titled “Step 40 — Review the Invalid Event Report”

You should identify:

invalid-ip

as:

INVALID IP ADDRESS

This verifies your validation logic.

Step 41 — Review the Success-After-Failure Finding

Section titled “Step 41 — Review the Success-After-Failure Finding”

Your report should identify:

USER
admin01
SOURCE
10.10.10.50
PREVIOUS FAILURES
3
THEN
SUCCESSFUL LOGIN

Do not immediately conclude:

ACCOUNT COMPROMISED

Instead ask:

Was the source expected?
Was the user working at that time?
Was MFA involved?
Was the device known?
Did activity continue after login?
Was the source VPN infrastructure?
Were there endpoint alerts?

Step 42 — Create an Analyst Severity Model

Section titled “Step 42 — Create an Analyst Severity Model”

For this lab, use a simple review classification:

LOW
No notable pattern
MEDIUM
Repeated failures
HIGH
Multiple users targeted from one source
HIGH
Successful login following repeated failures

This is a training classification.

Do not treat it as a universal SOC policy.

Step 43 — Add a Risk Recommendation Function

Section titled “Step 43 — Add a Risk Recommendation Function”

Optional:

def calculate_review_level(
failures,
unique_users,
success_after_failures
):
if success_after_failures:
return "high"
if unique_users >= 3:
return "high"
if failures >= 3:
return "medium"
return "low"

Step 44 — Understand Detection Logic vs Investigation Logic

Section titled “Step 44 — Understand Detection Logic vs Investigation Logic”

Detection logic asks:

DID THE PATTERN OCCUR?

Investigation asks:

WHY DID THE PATTERN OCCUR?

Automation is strong at the first.

Human analysts are essential for the second.

Extend the analyzer so that you can generate:

TIMESTAMP
USER
SOURCE IP
STATUS

for selected users.

Example:

09:00:11 admin01 10.10.10.50 FAILED
09:00:34 admin01 10.10.10.50 FAILED
09:01:08 admin01 10.10.10.50 FAILED
09:05:42 admin01 10.10.10.50 SUCCESS

Optional:

def get_user_timeline(
events,
username
):
return [
event
for event in build_timeline(events)
if event["user"] == username
]

For a more professional tool:

import argparse

Create:

def get_arguments():
parser = argparse.ArgumentParser(
description=(
"Analyze authentication "
"security logs."
)
)
parser.add_argument(
"--input",
required=False,
default=str(LOG_FILE),
help="Authentication log file"
)
return parser.parse_args()

This allows future use such as:

Terminal window
python src/analyzer.py \
--input data/authentication.log

Professional tools should log execution.

Add:

import logging

Configure:

logging.basicConfig(
level=logging.INFO,
format=(
"%(asctime)s "
"%(levelname)s "
"%(message)s"
)
)

Use:

logging.info(
"Starting security log analysis"
)

and:

logging.info(
"Analysis complete"
)

In real environments, logs may contain sensitive information.

Avoid unnecessarily recording:

PASSWORDS
TOKENS
SESSION COOKIES
PRIVATE KEYS
FULL SENSITIVE PAYLOADS

Wrap file access:

try:
events, invalid_events = load_events()
except FileNotFoundError:
print(
"Authentication log not found."
)
raise SystemExit(1)

Temporarily rename:

authentication.log

Run the tool.

Verify:

THE TOOL FAILS CLEANLY

rather than producing a confusing traceback for the end user.

Restore the file afterward.

Create an empty temporary log.

Your tool should handle:

ZERO EVENTS

without crashing.

Add:

broken authentication record

Verify that the event is:

REJECTED
RECORDED AS INVALID
NOT SILENTLY ACCEPTED

Add:

2026-08-29T10:00:00Z user=ADMIN01 source_ip=10.10.10.50 action=LOGIN status=FAILED

Verify that normalization produces:

admin01
login
failed

Add the same event twice.

Ask:

SHOULD DUPLICATES COUNT?

For the first lab version:

YES

But document that production pipelines may need:

DEDUPLICATION

depending on ingestion architecture.

A future log format could contain:

event_id=EVT-1001

Stable event IDs can help with:

DEDUPLICATION
CORRELATION
CASE TRACKING

Create a table in your report:

Detection Logic Result
Repeated failures User failures ≥ 3 Review
High-volume source IP failures ≥ 3 Review
Multi-user source ≥ 3 unique users Review
Failure → Success ≥ 3 failures then success High review

Before trusting the script, manually count several events.

For example:

How many failed logins
came from 10.10.10.50?

Compare the answer with your script.

This is important.

Never assume:

NO ERROR
=
CORRECT ANALYSIS

Step 59 — Build a Manual Verification Checklist

Section titled “Step 59 — Build a Manual Verification Checklist”

Check:

TOTAL EVENTS
FAILED EVENTS
SUCCESS EVENTS
TOP USER
TOP SOURCE
INVALID EVENT COUNT
FAILURE → SUCCESS FINDING

Optional advanced step.

Create:

tests/

Then:

tests/test_analyzer.py

Test normalization:

def test_normalize_event():
event = {
"timestamp":
"2026-08-29T09:00:00Z",
"user": " ADMIN01 ",
"source_ip": "10.10.10.50",
"action": "LOGIN",
"status": "FAILED"
}
result = normalize_event(
event
)
assert result["user"] == "admin01"
assert result["action"] == "login"
assert result["status"] == "failed"

Example:

def test_valid_ip():
assert is_valid_ip(
"10.10.10.50"
)
def test_invalid_ip():
assert not is_valid_ip(
"invalid-ip"
)

Step 62 — Test Success-After-Failure Logic

Section titled “Step 62 — Test Success-After-Failure Logic”

Use a tiny synthetic event set and confirm:

3 FAILURES + SUCCESS

creates exactly:

1 FINDING

Create:

README.md

Include:

PROJECT PURPOSE
ARCHITECTURE
REQUIREMENTS
DATA FORMAT
HOW TO RUN
OUTPUT FILES
DETECTION LOGIC
LIMITATIONS
SECURITY CONSIDERATIONS
This project demonstrates defensive security log
processing using Python. It parses synthetic
authentication events, validates and normalizes
records, identifies notable authentication patterns,
and generates analyst-ready reports.

Include:

Synthetic dataset only
No identity enrichment
No asset enrichment
No GeoIP enrichment
No MFA telemetry
No endpoint telemetry
Thresholds are training values
Timestamp analysis is simplified

This shows professional judgment.

Do not write:

THIS TOOL DETECTS
ACCOUNT COMPROMISE

Instead write:

THIS TOOL IDENTIFIES
AUTHENTICATION PATTERNS
THAT MAY REQUIRE
ANALYST REVIEW

Step 67 — Optional Enhancement: Time Windows

Section titled “Step 67 — Optional Enhancement: Time Windows”

The current tool counts all events.

Production detection usually considers:

TIME WINDOWS

Example:

5 failed logins
within 10 minutes

Future enhancement:

TIMESTAMP
WINDOW
COUNT

Step 68 — Optional Enhancement: Identity Context

Section titled “Step 68 — Optional Enhancement: Identity Context”

Add synthetic:

users.csv

with:

username
department
privileged
mfa_enabled

Then enrich the event.

AUTH EVENT
USERNAME
USER INVENTORY
ROLE
MFA
PRIVILEGE

This could turn:

3 failed logins

into a stronger contextual finding when the user is:

PRIVILEGED

Step 70 — Optional Enhancement: Asset Context

Section titled “Step 70 — Optional Enhancement: Asset Context”

Add:

assets.csv

containing:

hostname
ip_address
criticality
owner

A mature pipeline could combine:

AUTHENTICATION EVENT
+
USER PRIVILEGE
+
ASSET CRITICALITY
+
SOURCE CONTEXT
=
BETTER TRIAGE

Store normalized events in a small SQLite database.

Conceptually:

PYTHON
NORMALIZED EVENTS
SQLITE
SQL ANALYSIS

Then ask:

Which users had the most failures?
Which source targeted the most users?
Which accounts had failures then success?

Step 73 — Optional Enhancement: Dashboard

Section titled “Step 73 — Optional Enhancement: Dashboard”

A later project could expose sanitized results through:

HTML
JAVASCRIPT

Example:

FAILED LOGINS
JSON REPORT
JAVASCRIPT
DASHBOARD

Your tool should support—not replace—the analyst.

RAW LOGS
AUTOMATED ANALYSIS
NOTABLE PATTERNS
ANALYST INVESTIGATION
CASE DECISION

For a repeated-failure finding ask:

Is this user privileged?
Is the source IP known?
Is the source internal or external?
Is the device managed?
Was MFA challenged?
Was a successful login observed?
Did activity continue afterward?
Are there endpoint alerts?
Are other users affected?

A SOC may eventually classify activity as:

BENIGN
EXPECTED ACTIVITY
FALSE POSITIVE
SUSPICIOUS
CONFIRMED INCIDENT

Your script should not make those final decisions automatically.

If this were part of an incident:

PRESERVE ORIGINAL LOG
WORK ON A COPY
RECORD COLLECTION TIME
HASH IMPORTANT EVIDENCE
DOCUMENT ANALYSIS

Optional:

import hashlib

Create a SHA-256 value for:

authentication.log

This can support evidence integrity documentation.

You should finish with:

security-log-analyzer/
|
+-- data/
| +-- authentication.log
|
+-- reports/
| +-- failed-users.csv
| +-- source-ip-summary.csv
| +-- invalid-events.csv
| +-- security-summary.json
| +-- investigation-report.md
|
+-- src/
| +-- analyzer.py
|
+-- README.md

Confirm:

  • Python runs successfully
  • Log file loads
  • Invalid IP is rejected
  • Login events are separated from logout events
  • Failed logins are counted
  • Users are grouped
  • Source IPs are grouped
  • Multi-user sources are identified
  • Success-after-failure sequence is identified
  • CSV reports are created
  • JSON summary is created
  • Markdown report is created
  • Tool handles missing files safely
  • Detection thresholds are documented
  • No sensitive credentials are stored

At the start of the lab you had:

RAW AUTHENTICATION LOGS

You now have:

RAW LOGS
PYTHON
PARSING
VALIDATION
NORMALIZATION
CORRELATION
SECURITY PATTERNS
REPORTS

You built a security tool capable of identifying:

REPEATED LOGIN FAILURES
HIGH-VOLUME SOURCE IPs
ONE SOURCE TARGETING MULTIPLE USERS
SUCCESSFUL LOGIN AFTER FAILURES
INVALID SECURITY DATA

and converting those observations into:

ANALYST-READY REPORTS

The most important lesson from this lab is:

DETECTION
IS NOT
CONCLUSION

Your script can say:

THREE FAILED LOGINS
FOLLOWED BY SUCCESS

But it cannot automatically tell you:

WHY

That requires:

IDENTITY CONTEXT
ASSET CONTEXT
ENDPOINT DATA
NETWORK DATA
BUSINESS CONTEXT
ANALYST JUDGMENT
LOG
PARSE
VALIDATE
NORMALIZE
COUNT
GROUP
CORRELATE
DETECT PATTERN
GENERATE REPORT
ANALYST REVIEW

When working with security logs, always ask:

WHAT HAPPENED?
WHO WAS INVOLVED?
WHERE DID IT COME FROM?
WHEN DID IT HAPPEN?
HOW OFTEN?
WHAT HAPPENED NEXT?
WHAT ADDITIONAL CONTEXT
DO I NEED?

That is the foundation of security log analysis.

➡️ Lab 02 — IOC Processing and Enrichment Pipeline

In the next lab, you will move from analyzing authentication events to processing security indicators.

You will build:

RAW IOC DATA
VALIDATION
NORMALIZATION
DEDUPLICATION
CLASSIFICATION
AUTHORIZED ENRICHMENT
ANALYST REPORT

You will work with:

IP ADDRESSES
DOMAINS
FILE HASHES

and build a reusable defensive pipeline for converting raw IOC lists into clean, structured, analyst-ready security intelligence.