Project Overview
loganalyzer.py is a Python-based log anomaly detection tool that parses Linux authentication logs and flags suspicious activity — brute force attempts, privilege escalation, and off-hours logins. Findings are exported to a timestamped CSV report sorted by severity, ready to feed into a ticketing or incident response workflow.
Why I Built This
Log Analysis is Core SOC Work
Security analysts spend a significant portion of their time reviewing logs for indicators of compromise. Manual review of thousands of log lines is impractical — automation is essential. This tool replicates the core detection logic a SIEM performs, built from scratch to demonstrate a deep understanding of what's happening under the hood.
Aligned with Real Attack Patterns
The three detection rules map directly to MITRE ATT&CK techniques: brute force (T1110), valid account abuse during off-hours (T1078), and privilege escalation via sudo (T1548). Understanding these patterns — not just the tools that detect them — is what separates a strong analyst from someone who just reads alerts.
Feeds Directly into Incident Response
The CSV output is structured to integrate with ticketing systems like ServiceNow — each row is a discrete finding with severity, timestamp, source IP, affected user, and a human-readable description. In a real environment, this output could trigger automated ticket creation or feed a dashboard.
How It Works
1. Log Parsing
Each line is tested against a set of compiled regex patterns for SSH failures, successful logins, and sudo executions. Matched events are normalized into structured dicts with timestamp, IP, user, and event type.
2. Brute Force Detection
Failed logins are grouped by source IP. A sliding window check flags any IP that produces 5 or more failures within 60 seconds — a classic credential stuffing or brute force signature.
3. Off-Hours Login Detection
Successful authentications between midnight and 6am are flagged as MEDIUM severity. Legitimate users rarely log in during these hours; attackers using stolen credentials often do.
4. Privilege Escalation Detection
Any sudo execution is captured and flagged HIGH. The command itself is included in the finding detail, giving analysts immediate context without needing to re-query the log.
Detection Rules
5+ failed SSH logins from the same IP within 60 seconds
Any sudo command execution — user and full command captured
Successful authentication between 12:00am – 6:00am
Thresholds are defined as constants at the top of the script and can be tuned to match your environment's baseline behavior.
Usage
Analyze a single log file
python3 loganalyzer.py /var/log/auth.log
Analyze multiple logs
python3 loganalyzer.py /var/log/auth.log /var/log/syslog
Custom output file
python3 loganalyzer.py /var/log/auth.log -o incident_report.csv
Console summary output
Parsing /var/log/auth.log... Parsed 9 events. Running detection... === ANOMALY SUMMARY === CRITICAL: 1 HIGH: 1 MEDIUM: 1 TOTAL: 3 Report saved → anomalies_20260528_201500.csv
CSV Output Format
| severity | rule | timestamp | ip | user | detail |
|---|---|---|---|---|---|
| CRITICAL | Brute Force Attempt | 2026-05-28T02:15:01 | 10.0.0.5 | 6 failed logins in 60s | |
| HIGH | Privilege Escalation | 2026-05-28T14:05:00 | root | sudo command: /usr/bin/apt-get install nmap | |
| MEDIUM | Off-Hours Login | 2026-05-28T03:22:00 | 203.0.113.42 | glen | Successful login at 03:22 |
Full Source Code
~160 lines of pure Python, no external dependencies. Available on GitHub.
#!/usr/bin/env python3
"""Log anomaly detector: parses auth/syslog logs and flags suspicious events."""
import re, csv, sys
from collections import defaultdict
from datetime import datetime
from pathlib import Path
BRUTE_FORCE_THRESHOLD = 5
BRUTE_FORCE_WINDOW_SEC = 60
OFF_HOURS = range(0, 6)
PATTERNS = {
"ssh_fail": re.compile(
r"(?P<month>\w+)\s+(?P<day>\d+)\s+(?P<time>\S+).*Failed password for "
r"(?:invalid user )?(?P<user>\S+) from (?P<ip>\S+)"
),
"ssh_success": re.compile(
r"(?P<month>\w+)\s+(?P<day>\d+)\s+(?P<time>\S+).*Accepted \S+ for "
r"(?P<user>\S+) from (?P<ip>\S+)"
),
"sudo": re.compile(
r"(?P<month>\w+)\s+(?P<day>\d+)\s+(?P<time>\S+).*sudo.*USER=(?P<user>\S+)"
r".*COMMAND=(?P<cmd>.+)"
),
}
def parse_syslog_time(month, day, time_str):
try:
return datetime.strptime(f"{datetime.now().year} {month} {day} {time_str}",
"%Y %b %d %H:%M:%S")
except ValueError:
return datetime.now()
def parse_log(path):
events = []
with open(path, errors="replace") as f:
for line in f:
for etype, pat in PATTERNS.items():
m = pat.search(line)
if m:
d = m.groupdict()
events.append({
"type": etype,
"timestamp": parse_syslog_time(
d.get("month", "Jan"), d.get("day", "1"), d.get("time", "00:00:00")),
"ip": d.get("ip", ""),
"user": d.get("user", ""),
"cmd": d.get("cmd", ""),
})
break
return events
def detect_anomalies(events):
findings = []
fails_by_ip = defaultdict(list)
for e in events:
if e["type"] == "ssh_fail":
fails_by_ip[e["ip"]].append(e["timestamp"])
elif e["type"] == "ssh_success" and e["timestamp"].hour in OFF_HOURS:
findings.append({"severity": "MEDIUM", "rule": "Off-Hours Login",
"timestamp": e["timestamp"].isoformat(), "ip": e["ip"],
"user": e["user"], "detail": f"Login at {e['timestamp'].strftime('%H:%M')}"})
elif e["type"] == "sudo":
findings.append({"severity": "HIGH", "rule": "Privilege Escalation",
"timestamp": e["timestamp"].isoformat(), "ip": "",
"user": e["user"], "detail": f"sudo: {e['cmd'][:80]}"})
for ip, times in fails_by_ip.items():
times.sort()
for i in range(len(times)):
window = [t for t in times[i:]
if (t - times[i]).total_seconds() <= BRUTE_FORCE_WINDOW_SEC]
if len(window) >= BRUTE_FORCE_THRESHOLD:
findings.append({"severity": "CRITICAL", "rule": "Brute Force Attempt",
"timestamp": times[i].isoformat(), "ip": ip, "user": "",
"detail": f"{len(window)} failed logins in {BRUTE_FORCE_WINDOW_SEC}s"})
break
return sorted(findings, key=lambda x: x["severity"])
def main():
import argparse
parser = argparse.ArgumentParser(description="Log anomaly detector")
parser.add_argument("logs", nargs="+")
parser.add_argument("-o", "--output")
args = parser.parse_args()
output = args.output or f"anomalies_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
all_events = []
for path in args.logs:
print(f"Parsing {path}...")
all_events.extend(parse_log(path))
findings = detect_anomalies(all_events)
with open(output, "w", newline="") as f:
writer = csv.DictWriter(f,
fieldnames=["severity", "rule", "timestamp", "ip", "user", "detail"])
writer.writeheader()
writer.writerows(findings)
counts = defaultdict(int)
for f in findings:
counts[f["severity"]] += 1
print("\n=== ANOMALY SUMMARY ===")
for sev in ["CRITICAL", "HIGH", "MEDIUM"]:
if counts[sev]:
print(f" {sev}: {counts[sev]}")
print(f" TOTAL: {len(findings)}\nReport saved → {output}")
if __name__ == "__main__":
main()