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.
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.
#!/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.
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.
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?