Mercurial > prosody-modules
view mod_audit/mod_audit.lua @ 5516:f25df3af02c1
mod_client_management: Include client software version number in listing
Should you ever wish to revoke a client by version number, e.g. for
security reasons affecting certain versions, then it would be good to at
the very least see which version is used.
Also includes the OAuth2 software ID, an optional unique identifier that
should be the same for all installations of a particular software.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Sat, 03 Jun 2023 19:21:39 +0200 |
parents | c35f3c1762b5 |
children | 9bbf5b0673a2 |
line wrap: on
line source
module:set_global(); 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) local store = rawget(self, host); if store then return store end store = module:context(host):open_store("audit", "archive"); rawset(self, host, store); return store; end 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 = { xmlns = "xmpp:prosody.im/audit", }; if session.id then attr.id = session.id; end if session.type then attr.type = session.type; end local stanza = st.stanza("session", attr); 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 local function audit(host, user, source, event_type, extra) if not host or host == "*" then error("cannot log audit events for global"); end local user_key = user or host_wide_user; local attr = { ["source"] = source, ["type"] = event_type, }; if user_key ~= host_wide_user then attr.user = user_key; end local stanza = st.stanza("audit-event", attr); if extra then if extra.session then local child = session_extra(extra.session); if child then stanza:add_child(child); end end if extra.custom then for _, child in ipairs(extra.custom) do if not st.is_stanza(child) then error("all extra.custom items must be stanzas") end stanza:add_child(child); end end end 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); 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