Mercurial > prosody-modules
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 |
