+"""
+Get login statistics for a samba server daemon (uses journald). Recommended
+setup in /etc/smbd.conf is to set `logging = syslog@3 ...` (ensure smbd was
+built with `configure --with-syslog`).
+"""
+
+import re
+import glob
+from systemd import journal
+
+from logparse.formatting import *
+from logparse.util import readlog, resolve
+from logparse import config
+from logparse.load_parsers import Parser
+
+class SmbdJournald(Parser):
+
+ def __init__(self):
+ super().__init__()
+ self.name = "smbd_journald"
+ self.info = "Get login statistics for a samba server."
+
+ def parse_log(self):
+ logger.debug("Starting smbd section")
+ section = Section("smbd")
+
+ j = journal.Reader()
+ j.this_boot()
+ j.log_level(journal.LOG_DEBUG)
+ j.add_match(_COMM="smbd")
+
+ messages = [entry["MESSAGE"] for entry in j if "MESSAGE" in entry]
+
+ total_auths = 0 # total number of logins for all users and all shares
+ shares = {} # file shares (each share is mapped to a list of user-hostname pairs)
+
+ logger.debug("Found {0} samba logins".format(str(len(messages))))
+
+ for msg in messages: # one log file for each client
+
+ if "connect to service" in msg:
+ entry = re.search('(\w*)\s*\(ipv.:(.+):.+\) connect to service (\S+) initially as user (\S+)', msg) # [('client', 'ip', 'share', 'user')]
+ try:
+ client, ip, share, user = entry.group(1,2,3,4)
+ except:
+ logger.warning("Malformed log message: " + msg)
+ continue
+
+ if not share in shares:
+ share_match = False
+ for pattern in config.prefs.get("smbd", "shares").split():
+ share_match = re.fullmatch(pattern, share) or share_match
+ if not share_match:
+ logger.debug("Ignoring share {0} due to config".format(share))
+ continue
+
+ if (not client.strip()):
+ client = ip
+ userhost = user + '@' + resolve(client, fqdn=config.prefs.get("smbd", "smbd-resolve-domains"))
+
+ user_match = False
+ for pattern in config.prefs.get("smbd", "users").split():
+ user_match = re.fullmatch(pattern, userhost) or user_match
+ if not user_match:
+ logger.debug("Ignoring login to {0} by user {1} due to config".format(share, userhost))
+ continue
+
+ total_auths += 1
+ if share in shares:
+ shares[share].append(userhost)
+ else:
+ shares[share] = [userhost]
+
+ section.append_data(Data(subtitle="Total of {0} authentications".format(str(total_auths))))
+
+ for share, logins in shares.items():
+ share_data = Data()
+ share_data.items = logins
+ share_data.orderbyfreq()
+ share_data.truncl(config.prefs.getint("logparse", "maxlist"))
+ share_data.subtitle = share + " ({0}, {1})".format(plural("user", len(share_data.items)), plural("login", len(logins)))
+ section.append_data(share_data)
+ logger.debug("Found {0} logins for share {1}".format(str(len(logins)), share))
+
+ logger.info("Finished smbd section")
+ return section