1"""
2Get login statistics for a samba server daemon (uses journald). Recommended
3setup in /etc/smbd.conf is to set `logging = syslog@3 ...` (ensure smbd was
4built with `configure --with-syslog`).
5"""
6
7import re
8from systemd import journal
9
10from logparse import config
11from logparse.formatting import *
12from logparse.load_parsers import Parser
13from logparse.util import LogPeriod, resolve
14
15class SmbdJournald(Parser):
16
17 def __init__(self):
18
19 super().__init__()
20 self.name = "smbd_journald"
21 self.info = "Get login statistics for a samba server."
22
23 def parse_log(self):
24
25 logger.debug("Starting smbd section")
26 section = Section("smbd")
27
28 j = journal.Reader()
29 j.this_boot()
30 j.log_level(journal.LOG_DEBUG)
31 j.add_match(_COMM="smbd")
32
33 messages = [entry["MESSAGE"] for entry in j if "MESSAGE" in entry]
34
35 total_auths = 0 # total no. of logins for all users and all shares
36
37 shares = {} # file shares (each share is mapped to a list of
38 # user-hostname pairs)
39
40 logger.debug("Found {0} samba logins".format(str(len(messages))))
41 logger.debug("Parsing data")
42
43 for msg in messages: # one log file for each client
44
45 if "connect to service" in msg:
46
47 # Generate list of [('client', 'ip', 'share', 'user')]
48 entry = re.search("(\w*)\s*\(ipv.:(.+):.+\) connect to service"
49 "(\S+) initially as user (\S+)", msg)
50
51 try:
52 client, ip, share, user = entry.group(1,2,3,4)
53 except:
54 logger.warning("Malformed log message: " + msg)
55 continue
56
57 if not share in shares:
58 share_match = False
59 for pattern in config.prefs.get("smbd", "shares").split():
60 share_match = re.fullmatch(pattern, share) or share_match
61 if not share_match:
62 logger.debug("Ignoring share {0} due to config".format(share))
63 continue
64
65 if (not client.strip()):
66 client = ip
67 userhost = user + '@' + resolve(client,
68 fqdn=config.prefs.get("smbd", "smbd-resolve-domains"))
69
70 user_match = False
71 for pattern in config.prefs.get("smbd", "users").split():
72 user_match = re.fullmatch(pattern, userhost) or user_match
73 if not user_match:
74 logger.debug("Ignoring login to {0} by user {1} "
75 "due to config".format(share, userhost))
76 continue
77
78 total_auths += 1
79 if share in shares:
80 shares[share].append(userhost)
81 else:
82 shares[share] = [userhost]
83
84 # Format Data() objects
85
86 section.append_data(Data(subtitle="Total of {0} authentications"
87 .format(str(total_auths))))
88
89 for share, logins in shares.items():
90 share_data = Data()
91 share_data.items = logins
92 share_data.orderbyfreq()
93 share_data.truncl(config.prefs.getint("logparse", "maxlist"))
94 share_data.subtitle = share + " ({0}, {1})".format(
95 plural("user", len(share_data.items)),
96 plural("login", len(logins)))
97 section.append_data(share_data)
98 logger.debug("Found {0} logins for share {1}".format(
99 str(len(logins)), share))
100
101 logger.info("Finished smbd section")
102 return section