Lab 01 — Build a Security Log Analyzer
Mission Information
Section titled “Mission Information”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
Mission
Section titled “Mission”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=failed2026-08-29T09:00:34Z user=admin01 source_ip=10.10.10.50 action=login status=failed2026-08-29T09:01:08Z user=admin01 source_ip=10.10.10.50 action=login status=failed2026-08-29T09:05:42Z user=admin01 source_ip=10.10.10.50 action=login status=successand turn it into:
TOTAL EVENTS ↓FAILED LOGINS ↓USERS WITH REPEATED FAILURES ↓TOP SOURCE IPs ↓FAILURE → SUCCESS SEQUENCES ↓ANALYST REVIEW ITEMSThe final tool should generate:
CSV REPORTS
JSON SUMMARY
MARKDOWN INVESTIGATION REPORTWhy This Lab Matters
Section titled “Why This Lab Matters”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 IPsYour job is to transform:
RAW DATAinto:
SECURITY CONTEXTThis is one of the most important skills in:
SOC ANALYSIS
THREAT HUNTING
INCIDENT RESPONSE
DETECTION ENGINEERING
SECURITY AUTOMATIONLearning Objectives
Section titled “Learning Objectives”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 REPORTSFinal Architecture
Section titled “Final Architecture”AUTHENTICATION LOG ↓PYTHON SCRIPT ↓LINE PARSER ↓VALIDATION ↓NORMALIZATION ↓EVENT OBJECTS ↓ANALYSIS ENGINE ┌────┼────────┐ ↓ ↓ ↓ USERS IPs TIMELINE ↓ ↓ ↓ └────┼────────┘ ↓SECURITY FINDINGS ↓REPORT GENERATOR ┌────┼──────┐ ↓ ↓ ↓ CSV JSON MDLab Scenario
Section titled “Lab Scenario”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 ATTEMPTSYour 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.
Authorization and Safety
Section titled “Authorization and Safety”This lab uses:
SYNTHETIC AUTHENTICATION DATADo not collect or analyze authentication records from systems unless you are:
THE OWNER
AN AUTHORIZED ADMINISTRATOR
OR
EXPLICITLY AUTHORIZED TO PERFORM THE ANALYSISLab Folder Structure
Section titled “Lab Folder Structure”Create:
security-log-analyzer/|+-- data/|| +-- authentication.log|+-- reports/|+-- src/|| +-- analyzer.py|+-- README.mdStep 01 — Create the Lab Workspace
Section titled “Step 01 — Create the Lab Workspace”Open your terminal.
Create the main directory:
mkdir security-log-analyzerEnter:
cd security-log-analyzerCreate subdirectories:
mkdir datamkdir reportsmkdir srcYour structure should now look like:
security-log-analyzer/|+-- data/+-- reports/+-- src/Step 02 — Verify Python
Section titled “Step 02 — Verify Python”Run:
python --versionor:
python3 --versionRecommended:
Python 3.10+Step 03 — Create the Authentication Log
Section titled “Step 03 — Create the Authentication Log”Create:
data/authentication.logAdd the following synthetic events:
2026-08-29T09:00:11Z user=admin01 source_ip=10.10.10.50 action=login status=failed2026-08-29T09:00:34Z user=admin01 source_ip=10.10.10.50 action=login status=failed2026-08-29T09:01:08Z user=admin01 source_ip=10.10.10.50 action=login status=failed2026-08-29T09:02:16Z user=analyst01 source_ip=10.10.10.40 action=login status=success2026-08-29T09:03:02Z user=user01 source_ip=10.10.10.60 action=login status=failed2026-08-29T09:03:18Z user=user02 source_ip=10.10.10.60 action=login status=failed2026-08-29T09:03:44Z user=user03 source_ip=10.10.10.60 action=login status=failed2026-08-29T09:04:10Z user=user04 source_ip=10.10.10.60 action=login status=failed2026-08-29T09:04:22Z user=user05 source_ip=10.10.10.60 action=login status=failed2026-08-29T09:05:42Z user=admin01 source_ip=10.10.10.50 action=login status=success2026-08-29T09:06:03Z user=user01 source_ip=10.10.10.70 action=login status=failed2026-08-29T09:06:19Z user=user01 source_ip=10.10.10.70 action=login status=failed2026-08-29T09:06:47Z user=user01 source_ip=10.10.10.70 action=login status=failed2026-08-29T09:07:15Z user=analyst01 source_ip=10.10.10.40 action=login status=success2026-08-29T09:08:00Z user=user02 source_ip=invalid-ip action=login status=failed2026-08-29T09:08:25Z user=admin02 source_ip=10.10.10.80 action=logout status=successThis dataset intentionally contains:
REPEATED FAILURES
MULTIPLE USERS FROM ONE SOURCE
SUCCESS AFTER FAILURE
INVALID IP DATA
NON-LOGIN ACTIVITYStep 04 — Understand the Log Format
Section titled “Step 04 — Understand the Log Format”Each log line contains:
TIMESTAMP
USER
SOURCE_IP
ACTION
STATUSExample:
2026-08-29T09:00:11Zuser=admin01source_ip=10.10.10.50action=loginstatus=failedStep 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 DICTIONARYStep 06 — Create the Python Script
Section titled “Step 06 — Create the Python Script”Create:
src/analyzer.pyStart with:
from pathlib import Pathfrom collections import Counter, defaultdictimport csvimport jsonimport ipaddressStep 07 — Define File Paths
Section titled “Step 07 — Define File Paths”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)Step 08 — Build the Log Parser
Section titled “Step 08 — Build the Log Parser”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 eventWhat This Function Does
Section titled “What This Function Does”Input:
2026-08-29T09:00:11Z user=admin01 source_ip=10.10.10.50 action=login status=failedOutput:
{ "timestamp": "2026-08-29T09:00:11Z", "user": "admin01", "source_ip": "10.10.10.50", "action": "login", "status": "failed"}Step 09 — Validate Required Fields
Section titled “Step 09 — Validate Required Fields”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=failedYour analyzer should recognize incomplete data.
Step 10 — Validate IP Addresses
Section titled “Step 10 — Validate IP Addresses”Create:
def is_valid_ip(value): try: ipaddress.ip_address(value) return True except ValueError: return FalseStep 11 — Normalize Events
Section titled “Step 11 — Normalize Events”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
admin01become:
admin01Step 12 — Load the Log File
Section titled “Step 12 — Load the Log File”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_eventsStep 13 — Test Event Loading
Section titled “Step 13 — Test Event Loading”At the bottom temporarily add:
events, invalid = load_events()
print( f"Valid events: {len(events)}")
print( f"Invalid events: {len(invalid)}")Run:
python src/analyzer.pyYou should see approximately:
Valid events: 15Invalid events: 1Step 14 — Focus on Authentication Events
Section titled “Step 14 — Focus on Authentication Events”The dataset also contains:
logoutCreate:
def get_login_events(events): return [ event for event in events if event["action"] == "login" ]Step 15 — Separate Success and Failure
Section titled “Step 15 — Separate Success and Failure”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, successfulStep 16 — Count Failures by User
Section titled “Step 16 — Count Failures by User”Create:
def count_failures_by_user(events): return Counter( event["user"] for event in events )Example result:
admin01 3
user01 4
user02 1Step 17 — Count Failures by Source IP
Section titled “Step 17 — Count Failures by Source IP”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 = 3Create:
def find_repeated_user_failures( failure_counts): return { user: count for user, count in failure_counts.items() if count >= FAILED_USER_THRESHOLD }Important Lesson
Section titled “Important Lesson”The value:
3is a lab threshold.
It is not a universal detection rule.
Real detection thresholds should consider:
BASELINE
TIME WINDOW
USER TYPE
ENVIRONMENT
APPLICATION
FALSE POSITIVESStep 19 — Identify High-Volume Source IPs
Section titled “Step 19 — Identify High-Volume Source IPs”Set:
FAILED_IP_THRESHOLD = 3Create:
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 ACCOUNTSCreate:
def users_by_source_ip(events): result = defaultdict(set)
for event in events: result[ event["source_ip"] ].add( event["user"] )
return resultStep 21 — Identify Multi-User Sources
Section titled “Step 21 — Identify Multi-User Sources”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 USERSFROM ONE SOURCEdoes not automatically mean an attack.
Possible legitimate causes include:
NAT
VPN
PROXY
SHARED SYSTEM
AUTOMATIONStep 22 — Build a Timeline
Section titled “Step 22 — Build a Timeline”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 ↓SUCCESSCreate:
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 findingsStep 24 — Understand the Detection
Section titled “Step 24 — Understand the Detection”For:
admin01you should see:
3 FAILURES ↓1 SUCCESSThis should create:
ANALYST REVIEW ITEMIt does not automatically prove compromise.
Step 25 — Why Context Matters
Section titled “Step 25 — Why Context Matters”Possible explanations include:
USER FORGOT PASSWORD
KEYBOARD ERROR
STALE SAVED CREDENTIAL
APPLICATION ISSUE
AUTHORIZED PASSWORD TESTING
ACCOUNT ATTACKAutomation identifies:
PATTERNThe analyst determines:
MEANINGStep 26 — Build the Analysis Function
Section titled “Step 26 — Build the Analysis Function”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 }Step 27 — Run the Analysis
Section titled “Step 27 — Run the Analysis”Add:
events, invalid_events = load_events()
analysis = analyze_events(events)
print( json.dumps( analysis, indent=2 ))Run:
python src/analyzer.pyReview 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 failuresStep 29 — Export User Failure CSV
Section titled “Step 29 — Export User Failure CSV”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 ])Step 30 — Export Source IP CSV
Section titled “Step 30 — Export Source IP CSV”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 ])Step 31 — Export Invalid Events
Section titled “Step 31 — Export Invalid Events”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 )Why Invalid Data Matters
Section titled “Why Invalid Data Matters”Do not silently discard bad security data.
You should know:
WHAT WAS REJECTED?
WHY?
HOW MUCH DATA WAS LOST?Step 32 — Export JSON Summary
Section titled “Step 32 — Export JSON Summary”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 )Step 33 — Generate the Markdown Report
Section titled “Step 33 — Generate the Markdown Report”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" )Step 34 — Create main()
Section titled “Step 34 — Create main()”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}" )Step 35 — Add the Entry Point
Section titled “Step 35 — Add the Entry Point”At the bottom:
if __name__ == "__main__": main()Step 36 — Run the Complete Tool
Section titled “Step 36 — Run the Complete Tool”Run:
python src/analyzer.pyExpected:
Security log analysis complete.Reports saved to: ...Step 37 — Review the Reports Directory
Section titled “Step 37 — Review the Reports Directory”You should now have:
reports/|+-- failed-users.csv|+-- source-ip-summary.csv|+-- invalid-events.csv|+-- security-summary.json|+-- investigation-report.mdStep 38 — Review failed-users.csv
Section titled “Step 38 — Review failed-users.csv”Expected structure:
user,failed_loginsuser01,4admin01,3user02,1user03,1user04,1user05,1Step 39 — Review source-ip-summary.csv
Section titled “Step 39 — Review source-ip-summary.csv”Expected structure:
source_ip,failed_logins10.10.10.60,410.10.10.50,310.10.10.70,3Your 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-ipas:
INVALID IP ADDRESSThis 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:
USERadmin01
SOURCE10.10.10.50
PREVIOUS FAILURES3
THENSUCCESSFUL LOGINInvestigation Thought Process
Section titled “Investigation Thought Process”Do not immediately conclude:
ACCOUNT COMPROMISEDInstead 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:
LOWNo notable pattern
MEDIUMRepeated failures
HIGHMultiple users targeted from one source
HIGHSuccessful login following repeated failuresThis 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.
Step 45 — Improve the Timeline
Section titled “Step 45 — Improve the Timeline”Extend the analyzer so that you can generate:
TIMESTAMPUSERSOURCE IPSTATUSfor 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 SUCCESSStep 46 — Add a User Timeline Function
Section titled “Step 46 — Add a User Timeline Function”Optional:
def get_user_timeline( events, username): return [ event for event in build_timeline(events) if event["user"] == username ]Step 47 — Add Command-Line Arguments
Section titled “Step 47 — Add Command-Line Arguments”For a more professional tool:
import argparseCreate:
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:
python src/analyzer.py \ --input data/authentication.logStep 48 — Add Logging
Section titled “Step 48 — Add Logging”Professional tools should log execution.
Add:
import loggingConfigure:
logging.basicConfig( level=logging.INFO, format=( "%(asctime)s " "%(levelname)s " "%(message)s" ))Use:
logging.info( "Starting security log analysis")and:
logging.info( "Analysis complete")Step 49 — Do Not Log Sensitive Data
Section titled “Step 49 — Do Not Log Sensitive Data”In real environments, logs may contain sensitive information.
Avoid unnecessarily recording:
PASSWORDS
TOKENS
SESSION COOKIES
PRIVATE KEYS
FULL SENSITIVE PAYLOADSStep 50 — Add Exception Handling
Section titled “Step 50 — Add Exception Handling”Wrap file access:
try: events, invalid_events = load_events()
except FileNotFoundError: print( "Authentication log not found." ) raise SystemExit(1)Step 51 — Test Missing File Behavior
Section titled “Step 51 — Test Missing File Behavior”Temporarily rename:
authentication.logRun the tool.
Verify:
THE TOOL FAILS CLEANLYrather than producing a confusing traceback for the end user.
Restore the file afterward.
Step 52 — Test Empty File Behavior
Section titled “Step 52 — Test Empty File Behavior”Create an empty temporary log.
Your tool should handle:
ZERO EVENTSwithout crashing.
Step 53 — Test Malformed Event Behavior
Section titled “Step 53 — Test Malformed Event Behavior”Add:
broken authentication recordVerify that the event is:
REJECTED
RECORDED AS INVALID
NOT SILENTLY ACCEPTEDStep 54 — Test Case Normalization
Section titled “Step 54 — Test Case Normalization”Add:
2026-08-29T10:00:00Z user=ADMIN01 source_ip=10.10.10.50 action=LOGIN status=FAILEDVerify that normalization produces:
admin01
login
failedStep 55 — Test Duplicate Events
Section titled “Step 55 — Test Duplicate Events”Add the same event twice.
Ask:
SHOULD DUPLICATES COUNT?For the first lab version:
YESBut document that production pipelines may need:
DEDUPLICATIONdepending on ingestion architecture.
Step 56 — Add Optional Event IDs
Section titled “Step 56 — Add Optional Event IDs”A future log format could contain:
event_id=EVT-1001Stable event IDs can help with:
DEDUPLICATION
CORRELATION
CASE TRACKINGStep 57 — Build a Detection Matrix
Section titled “Step 57 — Build a Detection Matrix”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 |
Step 58 — Validate Results Manually
Section titled “Step 58 — Validate Results Manually”Before trusting the script, manually count several events.
For example:
How many failed loginscame from 10.10.10.50?Compare the answer with your script.
This is important.
Never assume:
NO ERROR=CORRECT ANALYSISStep 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 FINDINGStep 60 — Add Unit Tests
Section titled “Step 60 — Add Unit Tests”Optional advanced step.
Create:
tests/Then:
tests/test_analyzer.pyTest 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"Step 61 — Test IP Validation
Section titled “Step 61 — Test IP Validation”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 + SUCCESScreates exactly:
1 FINDINGStep 63 — Add a README
Section titled “Step 63 — Add a README”Create:
README.mdInclude:
PROJECT PURPOSE
ARCHITECTURE
REQUIREMENTS
DATA FORMAT
HOW TO RUN
OUTPUT FILES
DETECTION LOGIC
LIMITATIONS
SECURITY CONSIDERATIONSStep 64 — README Purpose Example
Section titled “Step 64 — README Purpose Example”This project demonstrates defensive security logprocessing using Python. It parses syntheticauthentication events, validates and normalizesrecords, identifies notable authentication patterns,and generates analyst-ready reports.Step 65 — Document Limitations
Section titled “Step 65 — Document Limitations”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 simplifiedThis shows professional judgment.
Step 66 — Do Not Overclaim
Section titled “Step 66 — Do Not Overclaim”Do not write:
THIS TOOL DETECTSACCOUNT COMPROMISEInstead write:
THIS TOOL IDENTIFIESAUTHENTICATION PATTERNSTHAT MAY REQUIREANALYST REVIEWStep 67 — Optional Enhancement: Time Windows
Section titled “Step 67 — Optional Enhancement: Time Windows”The current tool counts all events.
Production detection usually considers:
TIME WINDOWSExample:
5 failed loginswithin 10 minutesFuture enhancement:
TIMESTAMP ↓WINDOW ↓COUNTStep 68 — Optional Enhancement: Identity Context
Section titled “Step 68 — Optional Enhancement: Identity Context”Add synthetic:
users.csvwith:
username
department
privileged
mfa_enabledThen enrich the event.
Step 69 — Identity Enrichment Model
Section titled “Step 69 — Identity Enrichment Model”AUTH EVENT ↓USERNAME ↓USER INVENTORY ↓ROLE ↓MFA ↓PRIVILEGEThis could turn:
3 failed loginsinto a stronger contextual finding when the user is:
PRIVILEGEDStep 70 — Optional Enhancement: Asset Context
Section titled “Step 70 — Optional Enhancement: Asset Context”Add:
assets.csvcontaining:
hostname
ip_address
criticality
ownerStep 71 — Contextual Security Analysis
Section titled “Step 71 — Contextual Security Analysis”A mature pipeline could combine:
AUTHENTICATION EVENT +USER PRIVILEGE +ASSET CRITICALITY +SOURCE CONTEXT =BETTER TRIAGEStep 72 — Optional Enhancement: SQL
Section titled “Step 72 — Optional Enhancement: SQL”Store normalized events in a small SQLite database.
Conceptually:
PYTHON ↓NORMALIZED EVENTS ↓SQLITE ↓SQL ANALYSISThen 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
JAVASCRIPTExample:
FAILED LOGINS ↓JSON REPORT ↓JAVASCRIPT ↓DASHBOARDStep 74 — SOC Analyst Workflow
Section titled “Step 74 — SOC Analyst Workflow”Your tool should support—not replace—the analyst.
RAW LOGS ↓AUTOMATED ANALYSIS ↓NOTABLE PATTERNS ↓ANALYST INVESTIGATION ↓CASE DECISIONStep 75 — Investigation Questions
Section titled “Step 75 — Investigation Questions”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?Step 76 — Analyst Decision Categories
Section titled “Step 76 — Analyst Decision Categories”A SOC may eventually classify activity as:
BENIGN
EXPECTED ACTIVITY
FALSE POSITIVE
SUSPICIOUS
CONFIRMED INCIDENTYour script should not make those final decisions automatically.
Step 77 — Evidence Handling
Section titled “Step 77 — Evidence Handling”If this were part of an incident:
PRESERVE ORIGINAL LOG
WORK ON A COPY
RECORD COLLECTION TIME
HASH IMPORTANT EVIDENCE
DOCUMENT ANALYSISStep 78 — Hash the Original Log
Section titled “Step 78 — Hash the Original Log”Optional:
import hashlibCreate a SHA-256 value for:
authentication.logThis can support evidence integrity documentation.
Step 79 — Lab Deliverables
Section titled “Step 79 — Lab Deliverables”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.mdStep 80 — Final Validation
Section titled “Step 80 — Final Validation”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
Mission Review
Section titled “Mission Review”At the start of the lab you had:
RAW AUTHENTICATION LOGSYou now have:
RAW LOGS ↓PYTHON ↓PARSING ↓VALIDATION ↓NORMALIZATION ↓CORRELATION ↓SECURITY PATTERNS ↓REPORTSWhat You Built
Section titled “What You Built”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 DATAand converting those observations into:
ANALYST-READY REPORTSKey Security Lesson
Section titled “Key Security Lesson”The most important lesson from this lab is:
DETECTIONIS NOTCONCLUSIONYour script can say:
THREE FAILED LOGINSFOLLOWED BY SUCCESSBut it cannot automatically tell you:
WHYThat requires:
IDENTITY CONTEXT
ASSET CONTEXT
ENDPOINT DATA
NETWORK DATA
BUSINESS CONTEXT
ANALYST JUDGMENTFinal Mental Model
Section titled “Final Mental Model”LOG ↓PARSE ↓VALIDATE ↓NORMALIZE ↓COUNT ↓GROUP ↓CORRELATE ↓DETECT PATTERN ↓GENERATE REPORT ↓ANALYST REVIEWWhen 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 CONTEXTDO I NEED?That is the foundation of security log analysis.
What’s Next?
Section titled “What’s Next?”➡️ 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 REPORTYou will work with:
IP ADDRESSES
DOMAINS
FILE HASHESand build a reusable defensive pipeline for converting raw IOC lists into clean, structured, analyst-ready security intelligence.