diff 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
line wrap: on
line diff
--- a/mod_audit/mod_audit.lua	Wed Feb 22 22:47:45 2023 -0500
+++ b/mod_audit/mod_audit.lua	Sat May 06 19:40:23 2023 -0500
@@ -1,14 +1,34 @@
 module:set_global();
 
-local audit_log_limit = module:get_option_number("audit_log_limit", 10000);
-local cleanup_after = module:get_option_string("audit_log_expires_after", "2w");
-
 local time_now = os.time;
+local parse_duration = require "util.human.io".parse_duration;
+local ip = require "util.ip";
 local st = require "util.stanza";
 local moduleapi = require "core.moduleapi";
 
 local host_wide_user = "@";
 
+local cleanup_after = module:get_option_string("audit_log_expires_after", "28d");
+if cleanup_after == "never" then
+	cleanup_after = nil;
+else
+	cleanup_after = parse_duration(cleanup_after);
+end
+
+local attach_ips = module:get_option_boolean("audit_log_ips", true);
+local attach_ipv4_prefix = module:get_option_number("audit_log_ipv4_prefix", nil);
+local attach_ipv6_prefix = module:get_option_number("audit_log_ipv6_prefix", nil);
+
+local have_geoip, geoip = pcall(require, "geoip.country");
+local attach_location = have_geoip and module:get_option_boolean("audit_log_location", true);
+
+local geoip4_country, geoip6_country;
+if have_geoip and attach_location then
+	geoip4_country = geoip.open(module:get_option_string("geoip_ipv4_country", "/usr/share/GeoIP/GeoIP.dat"));
+	geoip6_country = geoip.open(module:get_option_string("geoip_ipv6_country", "/usr/share/GeoIP/GeoIPv6.dat"));
+end
+
+
 local stores = {};
 
 local function get_store(self, host)
@@ -23,6 +43,34 @@
 
 setmetatable(stores, { __index = get_store });
 
+local function prune_audit_log(host)
+	local before = os.time() - cleanup_after;
+	module:context(host):log("debug", "Pruning audit log for entries older than %s", os.date("%Y-%m-%d %R:%S", before));
+	local ok, err = stores[host]:delete(nil, { ["end"] = before });
+	if not ok then
+		module:context(host):log("error", "Unable to prune audit log: %s", err);
+		return;
+	end
+	local sum = tonumber(ok);
+	if sum then
+		module:context(host):log("debug", "Pruned %d expired audit log entries", sum);
+		return sum > 0;
+	end
+	module:context(host):log("debug", "Pruned expired audit log entries");
+	return true;
+end
+
+local function get_ip_network(ip_addr)
+	local _ip = ip.new_ip(ip_addr);
+	local proto = _ip.proto;
+	local network;
+	if proto == "IPv4" and attach_ipv4_prefix then
+		network = ip.truncate(_ip, attach_ipv4_prefix).normal.."/"..attach_ipv4_prefix;
+	elseif proto == "IPv6" and attach_ipv6_prefix then
+		network = ip.truncate(_ip, attach_ipv6_prefix).normal.."/"..attach_ipv6_prefix;
+	end
+	return network;
+end
 
 local function session_extra(session)
 	local attr = {
@@ -35,8 +83,22 @@
 		attr.type = session.type;
 	end
 	local stanza = st.stanza("session", attr);
-	if session.ip then
-		stanza:text_tag("remote-ip", session.ip);
+	if attach_ips and session.ip then
+		local remote_ip, network = session.ip;
+		if attach_ipv4_prefix or attach_ipv6_prefix then
+			network = get_ip_network(remote_ip);
+		end
+		stanza:text_tag("remote-ip", network or remote_ip);
+	end
+	if attach_location and session.ip then
+		local remote_ip = ip.new(session.ip);
+		local geoip_country = ip.proto == "IPv6" and geoip6_country or geoip4_country;
+		stanza:tag("location", {
+			country = geoip_country:query_by_addr(remote_ip.normal);
+		}):up();
+	end
+	if session.client_id then
+		stanza:text_tag("client", session.client_id);
 	end
 	return stanza
 end
@@ -55,7 +117,7 @@
 		attr.user = user_key;
 	end
 	local stanza = st.stanza("audit-event", attr);
-	if extra ~= nil then
+	if extra then
 		if extra.session then
 			local child = session_extra(extra.session);
 			if child then
@@ -63,7 +125,7 @@
 			end
 		end
 		if extra.custom then
-			for _, child in extra.custom do
+			for _, child in ipairs(extra.custom) do
 				if not st.is_stanza(child) then
 					error("all extra.custom items must be stanzas")
 				end
@@ -72,15 +134,155 @@
 		end
 	end
 
-	local id, err = stores[host]:append(nil, nil, stanza, time_now(), user_key);
-	if err then
-		module:log("error", "failed to persist audit event: %s", err);
-		return
+	local store = stores[host];
+	local id, err = store:append(nil, nil, stanza, extra and extra.timestamp or time_now(), user_key);
+	if not id then
+		if err == "quota-limit" then
+			local limit = store.caps and store.caps.quota or 1000;
+			local truncate_to = math.floor(limit * 0.99);
+			if type(cleanup_after) == "number" then
+				module:log("debug", "Audit log has reached quota - forcing prune");
+				if prune_audit_log(host) then
+					-- Retry append
+					id, err = store:append(nil, nil, stanza, extra and extra.timestamp or time_now(), user_key);
+				end
+			end
+			if not id and (store.caps and store.caps.truncate) then
+				module:log("debug", "Audit log has reached quota - truncating");
+				local truncated = store:delete(nil, {
+					truncate = truncate_to;
+				});
+				if truncated then
+					-- Retry append
+					id, err = store:append(nil, nil, stanza, extra and extra.timestamp or time_now(), user_key);
+				end
+			end
+		end
+		if not id then
+			module:log("error", "Failed to persist audit event: %s", err);
+			return;
+		end
 	else
-		module:log("debug", "persisted audit event %s as %s", stanza:top_tag(), id);
+		module:log("debug", "Persisted audit event %s as %s", stanza:top_tag(), id);
 	end
 end
 
 function moduleapi.audit(module, user, event_type, extra)
 	audit(module.host, user, "mod_" .. module:get_name(), event_type, extra);
 end
+
+function module.command(arg_)
+	local jid = require "util.jid";
+	local arg = require "util.argparse".parse(arg_, {
+		value_params = { "limit" };
+	 });
+
+	for k, v in pairs(arg) do print("U", k, v) end
+	local query_user, host = jid.prepped_split(arg[1]);
+
+	if arg.prune then
+		local sm = require "core.storagemanager";
+		if host then
+			sm.initialize_host(host);
+			prune_audit_log(host);
+		else
+			for _host in pairs(prosody.hosts) do
+				sm.initialize_host(_host);
+				prune_audit_log(_host);
+			end
+		end
+		return;
+	end
+
+	if not host then
+		print("EE: Please supply the host for which you want to show events");
+		return 1;
+	elseif not prosody.hosts[host] then
+		print("EE: Unknown host: "..host);
+		return 1;
+	end
+
+	require "core.storagemanager".initialize_host(host);
+	local store = stores[host];
+	local c = 0;
+
+	if arg.global then
+		if query_user then
+			print("WW: Specifying a user account is incompatible with --global. Showing only global events.");
+		end
+		query_user = "@";
+	end
+
+	local results, err = store:find(nil, {
+		with = query_user;
+		limit = arg.limit and tonumber(arg.limit) or nil;
+		reverse = true;
+	})
+	if not results then
+		print("EE: Failed to query audit log: "..tostring(err));
+		return 1;
+	end
+
+	local colspec = {
+		{ title = "Date", key = "when", width = 19, mapper = function (when) return os.date("%Y-%m-%d %R:%S", when); end };
+		{ title = "Source", key = "source", width = "2p" };
+		{ title = "Event", key = "event_type", width = "2p" };
+	};
+
+	if arg.show_user ~= false and (not arg.global and not query_user) or arg.show_user then
+		table.insert(colspec, {
+			title = "User", key = "username", width = "2p",
+			mapper = function (user)
+				if user == "@" then return ""; end
+				if user:sub(-#host-1, -1) == ("@"..host) then
+					return (user:gsub("@.+$", ""));
+				end
+			end;
+		});
+	end
+	if arg.show_ip ~= false and (not arg.global and attach_ips) or arg.show_ip then
+		table.insert(colspec, {
+			title = "IP", key = "ip", width = "2p";
+		});
+	end
+	if arg.show_location ~= false and (not arg.global and attach_location) or arg.show_location then
+		table.insert(colspec, {
+			title = "Location", key = "country", width = 2;
+		});
+	end
+
+	if arg.show_note then
+		table.insert(colspec, {
+			title = "Note", key = "note", width = "2p";
+		});
+	end
+
+	local row, width = require "util.human.io".table(colspec);
+
+	print(string.rep("-", width));
+	print(row());
+	print(string.rep("-", width));
+	for _, entry, when, user in results do
+		if arg.global ~= false or user ~= "@" then
+			c = c + 1;
+			print(row({
+				when = when;
+				source = entry.attr.source;
+				event_type = entry.attr.type:gsub("%-", " ");
+				username = user;
+				ip = entry:get_child_text("remote-ip");
+				location = entry:find("location@country");
+				note = entry:get_child_text("note");
+			}));
+		end
+	end
+	print(string.rep("-", width));
+	print(("%d records displayed"):format(c));
+end
+
+function module.add_host(host_module)
+	host_module:depends("cron");
+	host_module:daily("Prune audit logs", function ()
+		prune_audit_log(host_module.host);
+	end);
+end