comparison mod_audit/mod_audit.lua @ 5650:0eb2d5ea2428

merge
author Stephen Paul Weber <singpolyma@singpolyma.net>
date Sat, 06 May 2023 19:40:23 -0500
parents c35f3c1762b5
children 9bbf5b0673a2
comparison
equal deleted inserted replaced
5649:2c69577b28c2 5650:0eb2d5ea2428
1 module:set_global(); 1 module:set_global();
2 2
3 local audit_log_limit = module:get_option_number("audit_log_limit", 10000);
4 local cleanup_after = module:get_option_string("audit_log_expires_after", "2w");
5
6 local time_now = os.time; 3 local time_now = os.time;
4 local parse_duration = require "util.human.io".parse_duration;
5 local ip = require "util.ip";
7 local st = require "util.stanza"; 6 local st = require "util.stanza";
8 local moduleapi = require "core.moduleapi"; 7 local moduleapi = require "core.moduleapi";
9 8
10 local host_wide_user = "@"; 9 local host_wide_user = "@";
10
11 local cleanup_after = module:get_option_string("audit_log_expires_after", "28d");
12 if cleanup_after == "never" then
13 cleanup_after = nil;
14 else
15 cleanup_after = parse_duration(cleanup_after);
16 end
17
18 local attach_ips = module:get_option_boolean("audit_log_ips", true);
19 local attach_ipv4_prefix = module:get_option_number("audit_log_ipv4_prefix", nil);
20 local attach_ipv6_prefix = module:get_option_number("audit_log_ipv6_prefix", nil);
21
22 local have_geoip, geoip = pcall(require, "geoip.country");
23 local attach_location = have_geoip and module:get_option_boolean("audit_log_location", true);
24
25 local geoip4_country, geoip6_country;
26 if have_geoip and attach_location then
27 geoip4_country = geoip.open(module:get_option_string("geoip_ipv4_country", "/usr/share/GeoIP/GeoIP.dat"));
28 geoip6_country = geoip.open(module:get_option_string("geoip_ipv6_country", "/usr/share/GeoIP/GeoIPv6.dat"));
29 end
30
11 31
12 local stores = {}; 32 local stores = {};
13 33
14 local function get_store(self, host) 34 local function get_store(self, host)
15 local store = rawget(self, host); 35 local store = rawget(self, host);
21 return store; 41 return store;
22 end 42 end
23 43
24 setmetatable(stores, { __index = get_store }); 44 setmetatable(stores, { __index = get_store });
25 45
46 local function prune_audit_log(host)
47 local before = os.time() - cleanup_after;
48 module:context(host):log("debug", "Pruning audit log for entries older than %s", os.date("%Y-%m-%d %R:%S", before));
49 local ok, err = stores[host]:delete(nil, { ["end"] = before });
50 if not ok then
51 module:context(host):log("error", "Unable to prune audit log: %s", err);
52 return;
53 end
54 local sum = tonumber(ok);
55 if sum then
56 module:context(host):log("debug", "Pruned %d expired audit log entries", sum);
57 return sum > 0;
58 end
59 module:context(host):log("debug", "Pruned expired audit log entries");
60 return true;
61 end
62
63 local function get_ip_network(ip_addr)
64 local _ip = ip.new_ip(ip_addr);
65 local proto = _ip.proto;
66 local network;
67 if proto == "IPv4" and attach_ipv4_prefix then
68 network = ip.truncate(_ip, attach_ipv4_prefix).normal.."/"..attach_ipv4_prefix;
69 elseif proto == "IPv6" and attach_ipv6_prefix then
70 network = ip.truncate(_ip, attach_ipv6_prefix).normal.."/"..attach_ipv6_prefix;
71 end
72 return network;
73 end
26 74
27 local function session_extra(session) 75 local function session_extra(session)
28 local attr = { 76 local attr = {
29 xmlns = "xmpp:prosody.im/audit", 77 xmlns = "xmpp:prosody.im/audit",
30 }; 78 };
33 end 81 end
34 if session.type then 82 if session.type then
35 attr.type = session.type; 83 attr.type = session.type;
36 end 84 end
37 local stanza = st.stanza("session", attr); 85 local stanza = st.stanza("session", attr);
38 if session.ip then 86 if attach_ips and session.ip then
39 stanza:text_tag("remote-ip", session.ip); 87 local remote_ip, network = session.ip;
88 if attach_ipv4_prefix or attach_ipv6_prefix then
89 network = get_ip_network(remote_ip);
90 end
91 stanza:text_tag("remote-ip", network or remote_ip);
92 end
93 if attach_location and session.ip then
94 local remote_ip = ip.new(session.ip);
95 local geoip_country = ip.proto == "IPv6" and geoip6_country or geoip4_country;
96 stanza:tag("location", {
97 country = geoip_country:query_by_addr(remote_ip.normal);
98 }):up();
99 end
100 if session.client_id then
101 stanza:text_tag("client", session.client_id);
40 end 102 end
41 return stanza 103 return stanza
42 end 104 end
43 105
44 local function audit(host, user, source, event_type, extra) 106 local function audit(host, user, source, event_type, extra)
53 }; 115 };
54 if user_key ~= host_wide_user then 116 if user_key ~= host_wide_user then
55 attr.user = user_key; 117 attr.user = user_key;
56 end 118 end
57 local stanza = st.stanza("audit-event", attr); 119 local stanza = st.stanza("audit-event", attr);
58 if extra ~= nil then 120 if extra then
59 if extra.session then 121 if extra.session then
60 local child = session_extra(extra.session); 122 local child = session_extra(extra.session);
61 if child then 123 if child then
62 stanza:add_child(child); 124 stanza:add_child(child);
63 end 125 end
64 end 126 end
65 if extra.custom then 127 if extra.custom then
66 for _, child in extra.custom do 128 for _, child in ipairs(extra.custom) do
67 if not st.is_stanza(child) then 129 if not st.is_stanza(child) then
68 error("all extra.custom items must be stanzas") 130 error("all extra.custom items must be stanzas")
69 end 131 end
70 stanza:add_child(child); 132 stanza:add_child(child);
71 end 133 end
72 end 134 end
73 end 135 end
74 136
75 local id, err = stores[host]:append(nil, nil, stanza, time_now(), user_key); 137 local store = stores[host];
76 if err then 138 local id, err = store:append(nil, nil, stanza, extra and extra.timestamp or time_now(), user_key);
77 module:log("error", "failed to persist audit event: %s", err); 139 if not id then
78 return 140 if err == "quota-limit" then
141 local limit = store.caps and store.caps.quota or 1000;
142 local truncate_to = math.floor(limit * 0.99);
143 if type(cleanup_after) == "number" then
144 module:log("debug", "Audit log has reached quota - forcing prune");
145 if prune_audit_log(host) then
146 -- Retry append
147 id, err = store:append(nil, nil, stanza, extra and extra.timestamp or time_now(), user_key);
148 end
149 end
150 if not id and (store.caps and store.caps.truncate) then
151 module:log("debug", "Audit log has reached quota - truncating");
152 local truncated = store:delete(nil, {
153 truncate = truncate_to;
154 });
155 if truncated then
156 -- Retry append
157 id, err = store:append(nil, nil, stanza, extra and extra.timestamp or time_now(), user_key);
158 end
159 end
160 end
161 if not id then
162 module:log("error", "Failed to persist audit event: %s", err);
163 return;
164 end
79 else 165 else
80 module:log("debug", "persisted audit event %s as %s", stanza:top_tag(), id); 166 module:log("debug", "Persisted audit event %s as %s", stanza:top_tag(), id);
81 end 167 end
82 end 168 end
83 169
84 function moduleapi.audit(module, user, event_type, extra) 170 function moduleapi.audit(module, user, event_type, extra)
85 audit(module.host, user, "mod_" .. module:get_name(), event_type, extra); 171 audit(module.host, user, "mod_" .. module:get_name(), event_type, extra);
86 end 172 end
173
174 function module.command(arg_)
175 local jid = require "util.jid";
176 local arg = require "util.argparse".parse(arg_, {
177 value_params = { "limit" };
178 });
179
180 for k, v in pairs(arg) do print("U", k, v) end
181 local query_user, host = jid.prepped_split(arg[1]);
182
183 if arg.prune then
184 local sm = require "core.storagemanager";
185 if host then
186 sm.initialize_host(host);
187 prune_audit_log(host);
188 else
189 for _host in pairs(prosody.hosts) do
190 sm.initialize_host(_host);
191 prune_audit_log(_host);
192 end
193 end
194 return;
195 end
196
197 if not host then
198 print("EE: Please supply the host for which you want to show events");
199 return 1;
200 elseif not prosody.hosts[host] then
201 print("EE: Unknown host: "..host);
202 return 1;
203 end
204
205 require "core.storagemanager".initialize_host(host);
206 local store = stores[host];
207 local c = 0;
208
209 if arg.global then
210 if query_user then
211 print("WW: Specifying a user account is incompatible with --global. Showing only global events.");
212 end
213 query_user = "@";
214 end
215
216 local results, err = store:find(nil, {
217 with = query_user;
218 limit = arg.limit and tonumber(arg.limit) or nil;
219 reverse = true;
220 })
221 if not results then
222 print("EE: Failed to query audit log: "..tostring(err));
223 return 1;
224 end
225
226 local colspec = {
227 { title = "Date", key = "when", width = 19, mapper = function (when) return os.date("%Y-%m-%d %R:%S", when); end };
228 { title = "Source", key = "source", width = "2p" };
229 { title = "Event", key = "event_type", width = "2p" };
230 };
231
232 if arg.show_user ~= false and (not arg.global and not query_user) or arg.show_user then
233 table.insert(colspec, {
234 title = "User", key = "username", width = "2p",
235 mapper = function (user)
236 if user == "@" then return ""; end
237 if user:sub(-#host-1, -1) == ("@"..host) then
238 return (user:gsub("@.+$", ""));
239 end
240 end;
241 });
242 end
243 if arg.show_ip ~= false and (not arg.global and attach_ips) or arg.show_ip then
244 table.insert(colspec, {
245 title = "IP", key = "ip", width = "2p";
246 });
247 end
248 if arg.show_location ~= false and (not arg.global and attach_location) or arg.show_location then
249 table.insert(colspec, {
250 title = "Location", key = "country", width = 2;
251 });
252 end
253
254 if arg.show_note then
255 table.insert(colspec, {
256 title = "Note", key = "note", width = "2p";
257 });
258 end
259
260 local row, width = require "util.human.io".table(colspec);
261
262 print(string.rep("-", width));
263 print(row());
264 print(string.rep("-", width));
265 for _, entry, when, user in results do
266 if arg.global ~= false or user ~= "@" then
267 c = c + 1;
268 print(row({
269 when = when;
270 source = entry.attr.source;
271 event_type = entry.attr.type:gsub("%-", " ");
272 username = user;
273 ip = entry:get_child_text("remote-ip");
274 location = entry:find("location@country");
275 note = entry:get_child_text("note");
276 }));
277 end
278 end
279 print(string.rep("-", width));
280 print(("%d records displayed"):format(c));
281 end
282
283 function module.add_host(host_module)
284 host_module:depends("cron");
285 host_module:daily("Prune audit logs", function ()
286 prune_audit_log(host_module.host);
287 end);
288 end