Glen Larkin

Log Anomaly Detector — Python Project

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.

Python 3.10+ No External Dependencies auth.log / syslog Regex Pattern Matching CSV Severity Report

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

CRITICAL Brute Force Attempt

5+ failed SSH logins from the same IP within 60 seconds

HIGH Privilege Escalation

Any sudo command execution — user and full command captured

MEDIUM Off-Hours Login

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()
▲