Files
2026-06-19 15:55:22 +02:00

130 lines
3.3 KiB
Python

#!/usr/bin/env python3
import sqlite3, subprocess, re, os, requests, time
DB = "sshd_failures.db"
STATE = "cursor.txt"
def get_cursor():
return open(STATE).read().strip() if os.path.exists(STATE) else None
def save_cursor(c):
with open(STATE, "w") as f:
f.write(c)
def init_db(db):
db.execute("""
CREATE TABLE IF NOT EXISTS events (
ts INTEGER,
ip TEXT,
port INTEGER,
user TEXT,
type TEXT
)
""")
def classify(line):
if not line or not line[0].isdigit():
return None
try:
ts_str, msg = line.split(" ", 1)
ts = int(float(ts_str))
except:
return None
m = re.search(r"Invalid user ([a-zA-Z0-9_-]+) from ([0-9a-fA-F:.]+) port (\d+)", msg)
if m:
return ts, m.group(2), int(m.group(3)), m.group(1), "invalid_user"
m = re.search(r"PAM: Authentication failure.*from ([0-9a-fA-F:.]+)", msg)
if m:
return ts, m.group(1), None, None, "pam_failure"
m = re.search(r"Received disconnect from ([0-9a-fA-F:.]+) port (\d+)", msg)
if m:
return ts, m.group(1), int(m.group(2)), None, "disconnect"
m = re.search(r"Connection closed by .* from ([0-9a-fA-F:.]+) port (\d+)", msg)
if m:
return ts, m.group(1), int(m.group(2)), None, "connection_closed"
m = re.search(r"Failed .* for (?:invalid user )?([a-zA-Z0-9_-]+) from ([0-9a-fA-F:.]+) port (\d+)", msg)
if m:
return ts, m.group(2), int(m.group(3)), m.group(1), "failed_auth"
return None
def geo(db, ip):
row = db.execute("SELECT country, lat, lon FROM ip_geo WHERE ip=?", (ip,)).fetchone()
if row:
return row
try:
r = requests.get(f"http://ip-api.com/json/{ip}", timeout=5).json()
if r.get("status") == "success":
data = (r["country"], r["lat"], r["lon"])
else:
data = (None, None, None)
except:
data = (None, None, None)
db.execute(
"INSERT OR REPLACE INTO ip_geo VALUES (?, ?, ?, ?)",
(ip, *data)
)
# time.sleep(0.5)
print(f"Get location for {ip} to {data[0]}")
return data
def main():
db = sqlite3.connect(DB)
init_db(db)
db.execute("""
CREATE TABLE IF NOT EXISTS ip_geo (
ip TEXT PRIMARY KEY,
country TEXT,
lat REAL,
lon REAL
)
""")
cmd = ["journalctl", "-u", "sshd", "-o", "short-unix", "--no-pager"]
cur = get_cursor()
if cur:
cmd += ["--after-cursor", cur]
out = subprocess.check_output(cmd, text=True)
lines = out.splitlines()
added = 0
for line in lines:
result = classify(line)
if not result:
continue
ts, ip, port, user, typ = result
db.execute(
"INSERT INTO events VALUES (?, ?, ?, ?, ?)",
(ts, ip, port, user, typ)
)
added += 1
if ip:
geo(db, ip)
db.commit()
# save cursor
out = subprocess.check_output(
["journalctl", "-u", "sshd", "-n", "1", "-o", "export"],
text=True
)
for l in out.splitlines():
if l.startswith("__CURSOR="):
save_cursor(l.split("=", 1)[1])
break
db.close()
print(f"Parsed {len(lines) - 1} lines and added {added} events")
if __name__ == "__main__":
main()