More stats

This commit is contained in:
jonas committed 2026-06-07 22:26:03 +02:00
1 parent bfd3213cc9
commit fe2745e7f8
2 files changed
+182 -64

No files matched your search

+77 -43
View File
@@ -4,25 +4,75 @@ import sqlite3, subprocess, re, os, requests, time
DB = "sshd_failures.db" DB = "sshd_failures.db"
STATE = "cursor.txt" STATE = "cursor.txt"
PATTERN = re.compile(r"PAM: Authentication failure for root from ([0-9a-fA-F:.]+)")
def get_cursor(): def get_cursor():
return open(STATE).read().strip() if os.path.exists(STATE) else None return open(STATE).read().strip() if os.path.exists(STATE) else None
def save_cursor(c): def save_cursor(c):
open(STATE, "w").write(c) with open(STATE, "w") as f:
f.write(c)
def geo(ip): def init_db(db):
print(f"Send geo request for {ip}") db.execute("""
r = requests.get(f"http://ip-api.com/json/{ip}", timeout=5).json() CREATE TABLE IF NOT EXISTS events (
if r["status"] == "success": ts INTEGER,
return r["country"], r["lat"], r["lon"] ip TEXT,
return None, None, None 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):
print(f"Get location for {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.1)
return data
def main(): def main():
db = sqlite3.connect(DB) db = sqlite3.connect(DB)
db.execute("CREATE TABLE IF NOT EXISTS failures (ts INTEGER, ip TEXT)") init_db(db)
db.execute(""" db.execute("""
CREATE TABLE IF NOT EXISTS ip_geo ( CREATE TABLE IF NOT EXISTS ip_geo (
@@ -34,6 +84,7 @@ def main():
""") """)
cmd = ["journalctl", "-u", "sshd", "-o", "short-unix", "--no-pager"] cmd = ["journalctl", "-u", "sshd", "-o", "short-unix", "--no-pager"]
cur = get_cursor() cur = get_cursor()
if cur: if cur:
cmd += ["--after-cursor", cur] cmd += ["--after-cursor", cur]
@@ -41,45 +92,25 @@ def main():
out = subprocess.check_output(cmd, text=True) out = subprocess.check_output(cmd, text=True)
lines = out.splitlines() lines = out.splitlines()
print(f"Must parse {len(lines)}") print(f"Parsing {len(lines)} lines")
for line in lines: for line in lines:
print(line) result = classify(line)
if not line or not line[0].isdigit(): if not result:
continue continue
ts_str, msg = line.split(" ", 1) ts, ip, port, user, typ = result
try: print(f"{typ:18} ip={ip} port={port} user={user}")
ts = int(float(ts_str))
except ValueError:
continue
m = PATTERN.search(msg) db.execute(
if not m: "INSERT INTO events VALUES (?, ?, ?, ?, ?)",
continue (ts, ip, port, user, typ)
)
ip = m.group(1) if ip:
geo(db, ip)
# store raw event
print(f"Inserting {ip}")
db.execute("INSERT INTO failures VALUES (?, ?)", (ts, ip))
# check cache
cached = db.execute(
"SELECT 1 FROM ip_geo WHERE ip = ?",
(ip,)
).fetchone()
if not cached:
country, lat, lon = geo(ip)
db.execute(
"INSERT OR REPLACE INTO ip_geo VALUES (?, ?, ?, ?)",
(ip, country, lat, lon)
)
time.sleep(0.2)
db.commit() db.commit()
db.close()
# save cursor # save cursor
out = subprocess.check_output( out = subprocess.check_output(
@@ -92,6 +123,9 @@ def main():
save_cursor(l.split("=", 1)[1]) save_cursor(l.split("=", 1)[1])
break break
db.close()
print("Done")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
print("Parsed everything") print("All done")
+105 -21
View File
@@ -4,30 +4,114 @@ from datetime import datetime
db = sqlite3.connect("sshd_failures.db") db = sqlite3.connect("sshd_failures.db")
for row in db.execute(""" def ts(x):
return datetime.fromtimestamp(x).strftime("%Y-%m-%d %H:%M:%S") if x else "N/A"
def bar(n, mx, width=40):
if not n or mx == 0:
return ""
return "█" * max(1, int((n / mx) * width))
print("\n" + "═"*100)
print("🔐 SSH SECURITY INTELLIGENCE DASHBOARD")
print("═"*100)
total = db.execute("SELECT COUNT(*) FROM events").fetchone()[0]
ips = db.execute("SELECT COUNT(DISTINCT ip) FROM events").fetchone()[0]
countries = db.execute("""
SELECT COUNT(DISTINCT COALESCE(country,'Unknown'))
FROM ip_geo
""").fetchone()[0]
print(f"\n📊 EVENTS: {total} 🌐 IPS: {ips} 🌍 COUNTRIES: {countries}")
print("\n" + "─"*100)
print("🔥 ATTACK TYPES")
print("─"*100)
types = list(db.execute("""
SELECT type, COUNT(*) c
FROM events
GROUP BY type
ORDER BY c DESC
"""))
mx = max([c for _, c in types] or [1])
for t, c in types:
print(f"{t:20} {c:6} {bar(c, mx)}")
print("\n" + "─"*100)
print("🚨 TOP ATTACKING IPS")
print("─"*100)
ips_top = list(db.execute("""
SELECT SELECT
g.country, e.ip,
COUNT(*) AS total, COUNT(*) c,
MAX(f.ts) AS last_seen, MAX(e.ts) last,
( COALESCE(g.country,'Unknown') country
SELECT f2.ip FROM events e
FROM failures f2 LEFT JOIN ip_geo g ON e.ip = g.ip
JOIN ip_geo g2 ON f2.ip = g2.ip GROUP BY e.ip
WHERE g2.country = g.country ORDER BY c DESC
GROUP BY f2.ip LIMIT 15
"""))
mx = max([c for _, c, _, _ in ips_top] or [1])
for ip, c, last, country in ips_top:
print(f"{ip:18} {c:6} {country:15} {bar(c, mx)} last={ts(last)}")
print("\n" + "─"*100)
print("🌍 COUNTRY HEATMAP")
print("─"*100)
countries_rows = list(db.execute("""
SELECT
COALESCE(g.country,'Unknown') country,
COUNT(*) total,
COUNT(DISTINCT e.ip) ips,
MAX(e.ts) last
FROM events e
JOIN ip_geo g ON e.ip = g.ip
GROUP BY country
ORDER BY total DESC
"""))
mx = max([c for _, c, _, _ in countries_rows] or [1])
for country, total_c, ip_count, last in countries_rows:
top_ip = db.execute("""
SELECT e.ip
FROM events e
JOIN ip_geo g ON e.ip = g.ip
WHERE COALESCE(g.country,'Unknown') = ?
GROUP BY e.ip
ORDER BY COUNT(*) DESC ORDER BY COUNT(*) DESC
LIMIT 1 LIMIT 1
) AS top_ip """, (country,)).fetchone()
FROM failures f
JOIN ip_geo g ON f.ip = g.ip
GROUP BY g.country
ORDER BY total DESC
"""):
country, total, last_seen, top_ip = row
if last_seen: top_ip = top_ip[0] if top_ip else "Unknown"
last_seen = datetime.fromtimestamp(last_seen)
print(f"{country:25} {total:6} top_ip={top_ip:18} last={last_seen}") print(f"{country:25} {total_c:6} ips={ip_count:4} top={top_ip:16} {bar(total_c, mx)} last={ts(last)}")
db.close() print("\n" + "─"*100)
print("🧠 TOP USERNAMES")
print("─"*100)
users = list(db.execute("""
SELECT COALESCE(user,'unknown') user, COUNT(*) c
FROM events
WHERE user IS NOT NULL
GROUP BY user
ORDER BY c DESC
LIMIT 15
"""))
mx = max([c for _, c in users] or [1])
for u, c in users:
print(f"{u:20} {c:6} {bar(c, mx)}")
print("\n" + "═"*100)
print("✅ DONE")
print("═"*100)