More stats
This commit is contained in:
1 parent
bfd3213cc9
commit
fe2745e7f8
2 files changed
+182
-64
No files matched your search
@@ -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")
|
||||||
@@ -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)
|
||||||
Reference in new issue
Block a user