2 changed files with 114 additions and 1 deletions
@ -0,0 +1,113 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
|
||||||
|
from systemd import journal |
||||||
|
from datetime import datetime, timedelta |
||||||
|
import re # read lines |
||||||
|
# Import smtplib for the actual sending function |
||||||
|
import smtplib |
||||||
|
# Import the email modules we'll need |
||||||
|
from email.message import EmailMessage |
||||||
|
from tabulate import tabulate |
||||||
|
import GeoIP |
||||||
|
import os |
||||||
|
import socket |
||||||
|
import logging |
||||||
|
import logging.config |
||||||
|
|
||||||
|
class connection: |
||||||
|
def __init__(self,logger): |
||||||
|
self.logger = logger |
||||||
|
self.attempts = {} |
||||||
|
self.gi = GeoIP.open("/usr/share/GeoIP/GeoIP.dat", GeoIP.GEOIP_STANDARD) |
||||||
|
|
||||||
|
def __add(self,hostname: str,user: str, pattern: str): |
||||||
|
self.logger.debug("<<< attempts:{}".format(self.attempts)) |
||||||
|
self.logger.debug("--- add {} {} {}".format(hostname,user,pattern)) |
||||||
|
if hostname in self.attempts: |
||||||
|
if user in self.attempts[hostname]: |
||||||
|
if pattern in self.attempts[hostname][user]: |
||||||
|
self.attempts[hostname][user][pattern] += 1 |
||||||
|
else: |
||||||
|
self.attempts[hostname][user][pattern] = 1 |
||||||
|
else: |
||||||
|
self.attempts[hostname][user] = { pattern: 1 } |
||||||
|
else: |
||||||
|
self.attempts[hostname] = { user: { pattern: 1 } } |
||||||
|
self.logger.debug(">>> attempts:{}".format(self.attempts)) |
||||||
|
|
||||||
|
def __get_list(self): |
||||||
|
liste = [] |
||||||
|
for h,v1 in self.attempts.items(): |
||||||
|
self.logger.debug("h {}".format(h)) |
||||||
|
gcn = self.gi.country_name_by_addr(h) |
||||||
|
if gcn is not None: |
||||||
|
g = gcn |
||||||
|
else: |
||||||
|
g = "localhost" |
||||||
|
for u,v2 in v1.items(): |
||||||
|
self.logger.debug("u {}".format(u)) |
||||||
|
for s,n in v2.items(): |
||||||
|
self.logger.debug("s {}".format(s)) |
||||||
|
self.logger.debug("g {} h {} u {} s {} n {}".format(g,h,u,s,n)) |
||||||
|
liste.append([g,h,u,s,n]) |
||||||
|
self.logger.debug(liste) |
||||||
|
return liste |
||||||
|
|
||||||
|
def report(self): |
||||||
|
headers = { 'Country':'', 'Hostname':'', 'User':'', 'Source':'', 'Number':'' } |
||||||
|
header = headers.keys() |
||||||
|
msg = EmailMessage() |
||||||
|
msg.set_content(tabulate(self.__get_list(),headers)) |
||||||
|
msg['Subject'] = f'[ssh][attempts] Reporting' |
||||||
|
msg['From'] = "{}@{}".format(os.getlogin(),socket.gethostname()) |
||||||
|
msg['To'] = "{}@{}".format(os.getlogin(),'192.168.0.238') |
||||||
|
self.logger.debug(msg) |
||||||
|
s = smtplib.SMTP('localhost') |
||||||
|
s.send_message(msg) |
||||||
|
s.quit() |
||||||
|
|
||||||
|
def addmessage(self, message:str): |
||||||
|
patterns = { |
||||||
|
'pam_unix':r'''^pam_unix[^ ]*[ ](?P<status>[a-z ]+)[; ]+logname=[.]*[ ]uid=[0-9]+[ ]euid=[0-9]+[ ]tty=[sh]+[ ]ruser=[.]*[ ]rhost=(?P<hostname>[0-9.]+)[ ]+user=(?P<user>[a-zA-Z0-9]+)$''', |
||||||
|
'failed':r'''^Failed[ ]password[ ]for[ ]+(?P<user>[a-zA-Z0-9]+)[ ]from[ ](?P<hostname>[0-9.]+)[ ]port[ ][0-9]+[ ](?P<protocol>[a-z0-9]+)$''', |
||||||
|
'invalid':r'''^Failed[ ]password[ ]for[ ]invalid[ ]user[ ]+(?P<user>[a-zA-Z0-9]+)[ ]from[ ](?P<hostname>[0-9.]+)[ ]port[ ][0-9]+[ ](?P<protocol>[a-z0-9]+)$''', |
||||||
|
'closed':r'''^Connection[ ]closed[ ]by[ ]authenticating[ ]user[ ](?P<user>[a-zA-Z0-9]+)[ ](?P<hostname>[0-9.]+)[ ]port[ ][0-9]+[ ](?P<step>[a-z\[\]]+)$''', |
||||||
|
'reset':r'''^Connection[ ]reset[ ]by[ ]invalid[ ]user[ ](?P<user>[a-zA-Z0-9]+)[ ](?P<hostname>[0-9.]+)[ ]port[ ][0-9]+[ ](?P<step>[a-z\[\]]+)$''', |
||||||
|
'pam':r'''^PAM[ ][0-9][ ]more[ ]authentication[ ]failures;[ ]logname=[ ]uid=[0-9]+[ ]euid=[0-9]+[ ]tty=ssh[ ]ruser=[ ]rhost=(?P<hostname>[0-9.]+)[ ]+user=(?P<user>[a-zA-Z0-9]+)$''', |
||||||
|
'pamano':r'''^PAM[ ][0-9][ ]more[ ]authentication[ ]failures;[ ]logname=[ ]uid=[0-9]+[ ]euid=[0-9]+[ ]tty=ssh[ ]ruser=[ ]rhost=(?P<hostname>[0-9.]+)([ ]+user=(?P<user>[a-zA-Z0-9]+|)|)$''', |
||||||
|
'stopping':r'''^Received[ ]signal[ ]15;[ ]terminating.$''', |
||||||
|
'ignoring':r'''^PAM[ ]service[(]sshd[)][ ]ignoring[ ]max[ ]retries;[ ] [0-9]+ > [0-9]+$''', |
||||||
|
'listen':r'''^Server[ ]listening[ ]on[ ][0-9:.]+[ ]port[ ][0-9]+[.]$''' |
||||||
|
} |
||||||
|
compiled = { r:re.compile(patterns[r]) for r in iter(patterns) } |
||||||
|
matched = { r:re.match(compiled[r],message) for r in iter(patterns) } |
||||||
|
self.logger.debug("$$$ matched: {}".format(matched)) |
||||||
|
lst = [v for k, v in matched.items() if v] |
||||||
|
self.logger.debug("lst: {}".format(lst)) |
||||||
|
pattern = [k for k, v in matched.items() if v] |
||||||
|
self.logger.debug("pattern: {}".format(pattern)) |
||||||
|
for p in pattern: |
||||||
|
match p: |
||||||
|
case 'listen'|'stopping'|'ignoring': |
||||||
|
continue |
||||||
|
case 'pam_unix'|'failed'|'invalid'|'closed'|'reset'|'pam'|'pamano': |
||||||
|
pattern0 = pattern[0] |
||||||
|
self.logger.debug("::: pattern0: {}".format(pattern0)) |
||||||
|
val = lst[0] if lst else None |
||||||
|
if val: |
||||||
|
self.__add(hostname=val['hostname'], user=val['user'], pattern=pattern0) |
||||||
|
case _: |
||||||
|
print('======== Unmatched =============') |
||||||
|
break |
||||||
|
|
||||||
|
logging.basicConfig(filename='/tmp/ssh_reporting.log',level=logging.DEBUG) |
||||||
|
logger=logging.getLogger(__name__) |
||||||
|
j = journal.Reader() |
||||||
|
c = connection(logger) |
||||||
|
j.seek_realtime(datetime.now() - timedelta(hours=24)) |
||||||
|
j.add_match('_SYSTEMD_UNIT=ssh.service') |
||||||
|
for entry in j: |
||||||
|
logger.debug(entry['MESSAGE']) |
||||||
|
c.addmessage(entry['MESSAGE']) |
||||||
|
logger.debug("--- Generating report") |
||||||
|
c.report() |
||||||
Loading…
Reference in new issue