HackBlue

Tools · Python and Bash for security automation · 02 / 20

Build a Python command-line tool that summarizes SSH failures from auth logs

Write a script that reads Linux auth logs, including rotated gzip files, extracts failed logins with a regex, and prints top sources and usernames with optional CSV output.

11 min read · practice only with authorization

Goal

Answer quickly, without a SIEM: which IPs failed SSH logins most, which usernames they tried, and when it started. The script should handle auth.log and rotated auth.log.2.gz files, so it opens files based on their extension.

The script

The regex captures the user and source IP from both “Failed password for” and “Invalid user” lines. collections.Counter does the counting. Output can go to the terminal or a CSV.

Snippet
#!/usr/bin/env python3
"""Summarize failed SSH logins from auth logs."""
import argparse, collections, csv, gzip, re, sys

PAT = re.compile(r'^(?P<ts>\w{3}\s+\d+ [\d:]+).*sshd\[\d+\]: (?:Failed password for (?:invalid user )?|Invalid user )(?P<user>\S+) from (?P<ip>[\d.]+)')

def lines(path):
    opener = gzip.open if path.endswith('.gz') else open
    with opener(path, 'rt', errors='replace') as f:
        yield from f

def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('logs', nargs='+')
    p.add_argument('--top', type=int, default=10)
    p.add_argument('--csv')
    a = p.parse_args()
    by_ip, by_user, first = collections.Counter(), collections.Counter(), {}
    for path in a.logs:
        for line in lines(path):
            m = PAT.search(line)
            if m:
                by_ip[m['ip']] += 1; by_user[m['user']] += 1
                first.setdefault(m['ip'], m['ts'])
    print(f'{"ip":16} {"fails":>6}  first seen')
    for ip, n in by_ip.most_common(a.top):
        print(f'{ip:16} {n:6}  {first[ip]}')
    print('\ntop usernames:', ', '.join(f'{u}({n})' for u, n in by_user.most_common(a.top)))
    if a.csv:
        with open(a.csv, 'w', newline='') as f:
            w = csv.writer(f); w.writerow(['ip', 'fails', 'first_seen'])
            w.writerows((ip, n, first[ip]) for ip, n in by_ip.most_common())
    return 0

if __name__ == '__main__':
    sys.exit(main())

Lab: run it on your own server logs

Run the script against /var/log/auth.log* on a lab VM that has SSH exposed to your lab network, or against a sample auth log. Generate a few failures yourself with ssh wronguser@localhost. Check that the counts match grep -c "Failed password" on the same files. Then add a --since option that skips lines before a given date.

Snippet
sudo python3 sshfails.py /var/log/auth.log* --top 5 --csv fails.csv
sudo zgrep -c 'Failed password' /var/log/auth.log*

Extending it

On systemd systems without auth.log, read from journalctl -u ssh -o json and parse the MESSAGE field. Add a successful-login pass with “Accepted” lines and flag IPs that had many failures followed by a success, which is the case that matters most.

Diagram

Input lines and what the regex extracts

Two line shapes, one pattern.

useripts

Failed password

Nov  2 10:14:03 web1 sshd[2211]: Failed password for root from 203.0.113.45 port 51122 ssh2
user=root ip=203.0.113.45

Invalid user

Nov  2 10:14:05 web1 sshd[2213]: Invalid user deploy from 203.0.113.45 port 51130
user=deploy ip=203.0.113.45

Accepted

Nov  2 10:20:00 web1 sshd[2300]: Accepted publickey for alice from 198.51.100.7
Not matched by this pattern

Check yourself

1. How does the script read rotated .gz logs?

2. Which Python class makes counting occurrences simple?

3. Which pattern deserves the most attention?