-- -- (C) 2014-19 - ntop.org -- -- This file contains the description of all functions -- used to trigger host alerts local verbose = ntop.getCache("ntopng.prefs.alerts.debug") == "1" local callback_utils = require "callback_utils" local template = require "template_utils" local json = require("dkjson") local host_pools_utils = require("host_pools_utils") local recovery_utils = require "recovery_utils" local alert_consts = require "alert_consts" local format_utils = require "format_utils" local telemetry_utils = require "telemetry_utils" local tracker = require "tracker" local alerts_api = require "alerts_api" local alert_endpoints = require "alert_endpoints_utils" local flow_consts = require "flow_consts" local user_scripts = require "user_scripts" local shaper_utils = nil if(ntop.isnEdge()) then package.path = dirs.installdir .. "/pro/scripts/lua/modules/?.lua;" .. package.path shaper_utils = require("shaper_utils") end if ntop.isEnterprise() then local dirs = ntop.getDirs() package.path = dirs.installdir .. "/pro/scripts/lua/enterprise/modules/?.lua;" .. package.path require "enterprise_alert_utils" end -- ############################################## function alertSeverityRaw(severity_id) severity_id = tonumber(severity_id) for key, severity_info in pairs(alert_consts.alert_severities) do if(severity_info.severity_id == severity_id) then return(key) end end end function alertSeverityLabel(v, nohtml) local severity_id = alertSeverityRaw(v) if(severity_id) then local severity_info = alert_consts.alert_severities[severity_id] local title = i18n(severity_info.i18n_title) or severity_info.i18n_title if(nohtml) then return(title) else return(string.format('%s', severity_info.label, title)) end end end function alertSeverity(v) return(alert_consts.alert_severities[v].severity_id) end -- ############################################## function alertTypeRaw(type_id) type_id = tonumber(type_id) for key, type_info in pairs(alert_consts.alert_types) do if(type_info.alert_id == type_id) then return(key) end end end function alertTypeLabel(v, nohtml) local alert_id = alertTypeRaw(v) if(alert_id) then local type_info = alert_consts.alert_types[alert_id] local title = i18n(type_info.i18n_title) or type_info.i18n_title if(nohtml) then return(title) else return(string.format(' %s', type_info.icon, title)) end end return(i18n("unknown")) end function alertType(v) return(alert_consts.alert_types[v].alert_id) end function alertTypeDescription(v) local alert_id = alertTypeRaw(v) if(alert_id) then return(alert_consts.alert_types[alert_id].i18n_description) end end -- ############################################## -- Rename engine -> granulariy function alertEngineRaw(granularity_id) granularity_id = tonumber(granularity_id) for key, granularity_info in pairs(alert_consts.alerts_granularities) do if(granularity_info.granularity_id == granularity_id) then return(key) end end end function alertEngine(v) return(alert_consts.alerts_granularities[v].granularity_id) end function alertEngineLabel(v) local granularity_id = alertEngineRaw(v) if(granularity_id ~= nil) then return(i18n(alert_consts.alerts_granularities[granularity_id].i18n_title)) end end function alertEngineDescription(v) local granularity_id = alertEngineRaw(v) if(granularity_id ~= nil) then return(i18n(alert_consts.alerts_granularities[granularity_id].i18n_description)) end end function granularity2sec(v) return(alert_consts.alerts_granularities[v].granularity_seconds) end -- See NetworkInterface::checkHostsAlerts() function granularity2id(granularity) -- TODO replace alertEngine return(alertEngine(granularity)) end function sec2granularity(seconds) seconds = tonumber(seconds) for key, granularity_info in pairs(alert_consts.alerts_granularities) do if(granularity_info.granularity_seconds == seconds) then return(key) end end end -- ############################################################################## function get_make_room_keys(ifId) return {flows="ntopng.cache.alerts.ifid_"..ifId..".make_room_flow_alerts", entities="ntopng.cache.alerts.ifid_"..ifId..".make_room_closed_alerts"} end -- ################################# -- This function maps the SQLite table names to the conventional table -- names used in this script local function luaTableName(sqlite_table_name) --~ ALERTS_MANAGER_FLOWS_TABLE_NAME "flows_alerts" if(sqlite_table_name == "flows_alerts") then return("historical-flows") else return("historical") end end -- ################################# function performAlertsQuery(statement, what, opts, force_query, group_by) local wargs = {"WHERE", "1=1"} local oargs = {} if(group_by ~= nil) then group_by = " GROUP BY " .. group_by else group_by = "" end if tonumber(opts.row_id) ~= nil then wargs[#wargs+1] = 'AND rowid = '..(opts.row_id) end if (not isEmptyString(opts.entity)) and (not isEmptyString(opts.entity_val)) then if(what == "historical-flows") then if(tonumber(opts.entity) ~= alert_consts.alertEntity("host")) then return({}) else -- need to handle differently for flows table local info = hostkey2hostinfo(opts.entity_val) wargs[#wargs+1] = 'AND (cli_addr="'..(info.host)..'" OR srv_addr="'..(info.host)..'")' wargs[#wargs+1] = 'AND vlan_id='..(info.vlan) end else wargs[#wargs+1] = 'AND alert_entity = "'..(opts.entity)..'"' wargs[#wargs+1] = 'AND alert_entity_val = "'..(opts.entity_val)..'"' end elseif (what ~= "historical-flows") then if (not isEmptyString(opts.entity)) then wargs[#wargs+1] = 'AND alert_entity = "'..(opts.entity)..'"' elseif(not isEmptyString(opts.entity_excludes)) then local excludes = string.split(opts.entity_excludes, ",") or {opts.entity_excludes} for _, entity in pairs(excludes) do wargs[#wargs+1] = 'AND alert_entity != "'.. entity ..'"' end end end if not isEmptyString(opts.origin) then local info = hostkey2hostinfo(opts.origin) wargs[#wargs+1] = 'AND cli_addr="'..(info.host)..'"' wargs[#wargs+1] = 'AND vlan_id='..(info.vlan) end if not isEmptyString(opts.target) then local info = hostkey2hostinfo(opts.target) wargs[#wargs+1] = 'AND srv_addr="'..(info.host)..'"' wargs[#wargs+1] = 'AND vlan_id='..(info.vlan) end if tonumber(opts.epoch_begin) ~= nil then wargs[#wargs+1] = 'AND alert_tstamp >= '..(opts.epoch_begin) end if tonumber(opts.epoch_end) ~= nil then wargs[#wargs+1] = 'AND alert_tstamp <= '..(opts.epoch_end) end if not isEmptyString(opts.flowhosts_type) then if opts.flowhosts_type ~= "all_hosts" then local cli_local, srv_local = 0, 0 if opts.flowhosts_type == "local_only" then cli_local, srv_local = 1, 1 elseif opts.flowhosts_type == "remote_only" then cli_local, srv_local = 0, 0 elseif opts.flowhosts_type == "local_origin_remote_target" then cli_local, srv_local = 1, 0 elseif opts.flowhosts_type == "remote_origin_local_target" then cli_local, srv_local = 0, 1 end if what == "historical-flows" then wargs[#wargs+1] = "AND cli_localhost = "..cli_local wargs[#wargs+1] = "AND srv_localhost = "..srv_local end -- TODO cannot apply it to other tables right now end end if tonumber(opts.alert_type) ~= nil then wargs[#wargs+1] = "AND alert_type = "..(opts.alert_type) end if tonumber(opts.alert_severity) ~= nil then wargs[#wargs+1] = "AND alert_severity = "..(opts.alert_severity) end if((not isEmptyString(opts.sortColumn)) and (not isEmptyString(opts.sortOrder))) then local order_by if opts.sortColumn == "column_date" then order_by = "alert_tstamp" elseif opts.sortColumn == "column_key" then order_by = "rowid" elseif opts.sortColumn == "column_severity" then order_by = "alert_severity" elseif opts.sortColumn == "column_type" then order_by = "alert_type" elseif opts.sortColumn == "column_count" and what ~= "engaged" then order_by = "alert_counter" elseif((opts.sortColumn == "column_duration") and (what == "historical")) then order_by = "(alert_tstamp_end - alert_tstamp)" else -- default order_by = "alert_tstamp" end oargs[#oargs+1] = "ORDER BY "..order_by oargs[#oargs+1] = string.upper(opts.sortOrder) end -- pagination if((tonumber(opts.perPage) ~= nil) and (tonumber(opts.currentPage) ~= nil)) then local to_skip = (tonumber(opts.currentPage)-1) * tonumber(opts.perPage) oargs[#oargs+1] = "LIMIT" oargs[#oargs+1] = to_skip..","..(opts.perPage) end local query = table.concat(wargs, " ") local res query = query .. " " .. table.concat(oargs, " ") .. group_by -- Uncomment to debug the queries --~ tprint(statement.." (from "..what..") "..query) if((what == "engaged") or (what == "historical")) then res = interface.queryAlertsRaw(statement, query, force_query) elseif what == "historical-flows" then res = interface.queryFlowAlertsRaw(statement, query, force_query) else error("Invalid alert subject: "..what) end return res end -- ################################# local function getNumEngagedAlerts(options) local entity_type_filter = tonumber(options.entity) local entity_value_filter = options.entity_val local res = interface.getEngagedAlertsCount(entity_type_filter, entity_value_filter, options.entity_excludes) if(res ~= nil) then return(res.num_alerts) end return(0) end -- ################################# function getNumAlerts(what, options) local num = 0 if(what == "engaged") then num = getNumEngagedAlerts(options) else local opts = getUnpagedAlertOptions(options or {}) local res = performAlertsQuery("SELECT COUNT(*) AS count", what, opts) if((res ~= nil) and (#res == 1) and (res[1].count ~= nil)) then num = tonumber(res[1].count) end end return num end -- ################################# -- Faster than of getNumAlerts function hasAlerts(what, options) if(what == "engaged") then return(getNumEngagedAlerts(options) > 0) end local opts = getUnpagedAlertOptions(options or {}) -- limit 1 opts.perPage = 1 opts.currentPage = 1 local res = performAlertsQuery("SELECT rowid", what, opts) if((res ~= nil) and (#res == 1)) then return(true) else return(false) end end -- ################################# local function engagedAlertsQuery(params) local type_filter = tonumber(params.alert_type) local severity_filter = tonumber(params.alert_severity) local entity_type_filter = tonumber(params.entity) local entity_value_filter = params.entity_val local perPage = tonumber(params.perPage or 10) local sortColumn = params.sortColumn or "column_" local sortOrder = params.sortOrder or "desc" local sOrder = ternary(sortOrder == "desc", rev_insensitive, asc_insensitive) local currentPage = tonumber(params.currentPage or 1) local totalRows = 0 --~ tprint(string.format("type=%s sev=%s entity=%s val=%s", type_filter, severity_filter, entity_type_filter, entity_value_filter)) local alerts = interface.getEngagedAlerts(entity_type_filter, entity_value_filter, type_filter, severity_filter, params.entity_excludes) local sort_2_col = {} -- Sort for idx, alert in pairs(alerts) do if sortColumn == "column_type" then sort_2_col[idx] = alert.alert_type elseif sortColumn == "column_severity" then sort_2_col[idx] = alert.alert_severity elseif sortColumn == "column_duration" then sort_2_col[idx] = os.time() - alert.alert_tstamp else -- column_date sort_2_col[idx] = alert.alert_tstamp end totalRows = totalRows + 1 end -- Pagination local to_skip = (currentPage-1) * perPage local totalRows = #alerts local res = {} local i = 0 for idx in pairsByValues(sort_2_col, sOrder) do if i >= to_skip + perPage then break end if (i >= to_skip) then res[#res + 1] = alerts[idx] end i = i + 1 end return res, totalRows end -- ################################# function getAlerts(what, options, with_counters) local alerts, num_alerts if what == "engaged" then alerts, num_alerts = engagedAlertsQuery(options) if not with_counters then num_alerts = nil end else alerts = performAlertsQuery("SELECT rowid, *", what, options) if with_counters then num_alerts = getNumAlerts(what, options) end end return alerts, num_alerts end -- ################################# local function refreshAlerts(ifid) ntop.delCache(string.format("ntopng.cache.alerts.ifid_%d.has_alerts", ifid)) ntop.delCache("ntopng.cache.update_alerts_stats_time") end -- ################################# function deleteAlerts(what, options) local opts = getUnpagedAlertOptions(options or {}) performAlertsQuery("DELETE", what, opts) end -- ################################# -- this function returns an object with parameters specific for one tab function getTabParameters(_get, what) local opts = {} for k,v in pairs(_get) do opts[k] = v end -- these options are contextual to the current tab (status) if _get.status ~= what then opts.alert_type = nil opts.alert_severity = nil end if not isEmptyString(what) then opts.status = what end opts.ifid = interface.getId() return opts end -- ################################# -- Remove pagination options from the options function getUnpagedAlertOptions(options) local res = {} local paged_option = { currentPage=1, perPage=1, sortColumn=1, sortOrder=1 } for k,v in pairs(options) do if not paged_option[k] then res[k] = v end end return res end -- ################################# local function checkDisableAlerts() local ifid = interface.getId() if(_POST["action"] == "disable_alert") then local entity = _POST["entity"] local entity_val = _POST["entity_val"] local alert_type = _POST["alert_type"] local disabled_alerts = alerts_api.getEntityAlertsDisabledBitmap(ifid, entity, entity_val) disabled_alerts = ntop.bitmapSet(disabled_alerts, tonumber(alert_type)) alerts_api.setEntityAlertsDisabledBitmap(ifid, entity, entity_val, disabled_alerts) elseif(_POST["action"] == "enable_alert") then local entity = _POST["entity"] local entity_val = _POST["entity_val"] local alert_type = _POST["alert_type"] local disabled_alerts = alerts_api.getEntityAlertsDisabledBitmap(ifid, entity, entity_val) disabled_alerts = ntop.bitmapClear(disabled_alerts, tonumber(alert_type)) alerts_api.setEntityAlertsDisabledBitmap(ifid, entity, entity_val, disabled_alerts) end end -- ################################# function checkDeleteStoredAlerts() _GET["status"] = _GET["status"] or _POST["status"] if((_POST["id_to_delete"] ~= nil) and (_GET["status"] ~= nil)) then if(_POST["id_to_delete"] ~= "__all__") then _GET["row_id"] = tonumber(_POST["id_to_delete"]) end deleteAlerts(_GET["status"], _GET) -- TRACKER HOOK tracker.log("checkDeleteStoredAlerts", {_GET["status"], _POST["id_to_delete"]}) -- to avoid performing the delete again _POST["id_to_delete"] = nil -- to avoid filtering by id _GET["row_id"] = nil -- in case of delete "older than" button, resets the time period after the delete took place if isEmptyString(_GET["epoch_begin"]) then _GET["epoch_end"] = nil end local has_alerts = hasAlerts(_GET["status"], _GET) if(not has_alerts) then -- reset the filter to avoid hiding the tab _GET["alert_severity"] = nil _GET["alert_type"] = nil end end checkDisableAlerts() if(_POST["action"] == "release_alert") then local entity_info = { alert_entity = alert_consts.alert_entities[alert_consts.alertEntityRaw(_POST["entity"])], alert_entity_val = _POST["entity_val"], } local type_info = { alert_type = alert_consts.alert_types[alertTypeRaw(_POST["alert_type"])], alert_severity = alert_consts.alert_severities[alertSeverityRaw(_POST["alert_severity"])], alert_subtype = _POST["alert_subtype"], alert_granularity = alert_consts.alerts_granularities[sec2granularity(_POST["alert_granularity"])], } alerts_api.release(entity_info, type_info) interface.refreshAlerts(); end end -- ################################# -- Return more information for the flow alert description local function getFlowStatusInfo(record, status_info) local res = "" local l7proto_name = interface.getnDPIProtoName(tonumber(record["l7_proto"]) or 0) if l7proto_name == "ICMP" then -- is ICMPv4 -- TODO: old format - remove when the all the flow alers will be generated in lua local type_code = {type = status_info["icmp.icmp_type"], code = status_info["icmp.icmp_code"]} if table.empty(type_code) and status_info["icmp"] then -- This is the new format created when setting the alert from lua type_code = {type = status_info["icmp"]["type"], code = status_info["icmp"]["code"]} end if status_info["icmp.unreach.src_ip"] then -- TODO: old format to be removed res = string.format("[%s]", i18n("icmp_page.icmp_port_unreachable_extra", {unreach_host=status_info["icmp.unreach.dst_ip"], unreach_port=status_info["icmp.unreach.dst_port"], unreach_protocol = l4_proto_to_string(status_info["icmp.unreach.protocol"])})) elseif status_info["icmp"] and status_info["icmp"]["unreach"] then -- New format res = string.format("[%s]", i18n("icmp_page.icmp_port_unreachable_extra", {unreach_host=status_info["icmp"]["unreach"]["dst_ip"], unreach_port=status_info["icmp"]["unreach"]["dst_port"], unreach_protocol = l4_proto_to_string(status_info["icmp"]["unreach"]["protocol"])})) else res = string.format("[%s]", getICMPTypeCode(type_code)) end end return string.format(" %s", res) end -- ################################# local function formatRawFlow(record, flow_json, skip_add_links) require "flow_utils" local time_bounds local add_links = (not skip_add_links) local host_page = "&page=alerts" if interfaceHasNindexSupport() and not skip_add_links then -- only add links if nindex is present add_links = true time_bounds = {getAlertTimeBounds(record)} end local decoded = json.decode(flow_json) or {} if((type(decoded["status_info"]) == "string") and (string.sub(decoded["status_info"], 1, 1) == "{")) then -- status_info may contain a JSON string or a plain message decoded["status_info"] = json.decode(decoded["status_info"]) end local status_info = decoded.status_info -- active flow lookup if not interface.isView() and status_info and status_info["ntopng.key"] and status_info["hash_entry_id"] and record["alert_tstamp"] then -- attempt a lookup on the active flows local active_flow = interface.findFlowByKeyAndHashId(status_info["ntopng.key"], status_info["hash_entry_id"]) if active_flow and active_flow["seen.first"] < tonumber(record["alert_tstamp"]) then return string.format("%s [%s: Info %s]", flow_consts.getStatusDescription(tonumber(record["flow_status"]), status_info), i18n("flow"), ntop.getHttpPrefix(), active_flow["ntopng.key"], active_flow["hash_entry_id"], getFlowLabel(active_flow, true, true)) end end -- pretend record is a flow to reuse getFlowLabel local flow = { ["cli.ip"] = record["cli_addr"], ["cli.port"] = tonumber(record["cli_port"]), ["cli.blacklisted"] = tostring(record["cli_blacklisted"]) == "1", ["srv.ip"] = record["srv_addr"], ["srv.port"] = tonumber(record["srv_port"]), ["srv.blacklisted"] = tostring(record["srv_blacklisted"]) == "1", ["vlan"] = record["vlan_id"]} flow = "["..i18n("flow")..": "..(getFlowLabel(flow, false, add_links, time_bounds, host_page) or "").."] " local l4_proto_label = l4_proto_to_string(record["proto"] or 0) or "" if not isEmptyString(l4_proto_label) then flow = flow.."[" .. l4_proto_label .. "] " end local l7proto_name = interface.getnDPIProtoName(tonumber(record["l7_proto"]) or 0) if record["l7_master_proto"] and record["l7_master_proto"] ~= "0" then local l7proto_master_name = interface.getnDPIProtoName(tonumber(record["l7_master_proto"])) if l7proto_master_name ~= l7proto_name then l7proto_name = string.format("%s.%s", l7proto_master_name, l7proto_name) end end if not isEmptyString(l7proto_name) and l4_proto_label ~= l7proto_name then flow = flow.."["..i18n("application")..": " ..l7proto_name.."] " end if decoded ~= nil then -- render the json local msg = "" if not isEmptyString(record["flow_status"]) then msg = msg..flow_consts.getStatusDescription(tonumber(record["flow_status"]), status_info).." " end if not isEmptyString(flow) then msg = msg..flow.." " end if not isEmptyString(decoded["info"]) then local lb = "" if (record["flow_status"] == "13") -- blacklisted flow and (not flow["srv.blacklisted"]) and (not flow["cli.blacklisted"]) then lb = " " end msg = msg.."["..i18n("info")..": "..decoded["info"]..lb.."] " end flow = msg end if status_info then flow = flow..getFlowStatusInfo(record, status_info) end return flow end -- ################################# local function getMenuEntries(status, selection_name, get_params) local actual_entries = {} local params = table.clone(get_params) -- Remove previous filters params.alert_severity = nil params.alert_type = nil if selection_name == "severity" then actual_entries = performAlertsQuery("select alert_severity id, count(*) count", status, params, nil, "alert_severity" --[[ group by ]]) elseif selection_name == "type" then actual_entries = performAlertsQuery("select alert_type id, count(*) count", status, params, nil, "alert_type" --[[ group by ]]) end return(actual_entries) end -- ################################# local function dropdownUrlParams(get_params) local buttons = "" for param, val in pairs(get_params) do -- NOTE: exclude the ifid parameter to avoid interface selection issues with system interface alerts if((param ~= "alert_severity") and (param ~= "alert_type") and (param ~= "status") and (param ~= "ifid")) then buttons = buttons.."&"..param.."="..val end end return(buttons) end -- ################################# local function drawDropdown(status, selection_name, active_entry, entries_table, button_label, get_params, actual_entries) -- alert_consts.alert_severity_keys and alert_consts.alert_type_keys are defined in lua_utils local id_to_label if selection_name == "severity" then id_to_label = alertSeverityLabel elseif selection_name == "type" then id_to_label = alertTypeLabel end actual_entries = actual_entries or getMenuEntries(status, selection_name, get_params) local buttons = '
' button_label = button_label or firstToUpper(selection_name) if active_entry ~= nil and active_entry ~= "" then button_label = firstToUpper(active_entry)..'' end buttons = buttons..'' buttons = buttons..'
' return buttons end -- ################################# local function printProbesTab(entity_probes, entity_type, entity_value, page_name, page_params, alt_name, options) local system_scripts = require("system_scripts_utils") if #entity_probes > 0 then print[[
]] for _, probe in ipairs(entity_probes) do print[[ ]] end print[[
]] print(i18n("system_stats.probe")) print[[ ]] print(i18n("system_stats.probe_config")) print[[
]] print(probe["probe"]["name"]) print[[
]] end end -- ################################# local function printConfigTab(entity_type, entity_value, page_name, page_params, alt_name, options) local trigger_alerts = true local ifid = interface.getId() local trigger_alerts_checked local cur_bitmap if(entity_type == "host") then cur_bitmap = alerts_api.getHostDisabledStatusBitmap(ifid, entity_value) end local entity_type_id = alert_consts.alertEntity(entity_type) if _SERVER["REQUEST_METHOD"] == "POST" then if _POST["trigger_alerts"] ~= "1" then trigger_alerts = false else trigger_alerts = true end alerts_api.setSuppressedAlerts(ifid, entity_type_id, entity_value, (not trigger_alerts)) if(entity_type == "host") then local bitmap = 0 if not isEmptyString(_POST["disabled_status"]) then local status_selection = split(_POST["disabled_status"], ",") or { _POST["disabled_status"] } for _, status in pairs(status_selection) do bitmap = ntop.bitmapSet(bitmap, tonumber(status)) end end if(bitmap ~= cur_bitmap) then alerts_api.setHostDisabledStatusBitmap(ifid, entity_value, bitmap) cur_bitmap = bitmap end end else trigger_alerts = (not alerts_api.hasSuppressedAlerts(ifid, entity_type_id, entity_value)) end if trigger_alerts == false then trigger_alerts_checked = "" else trigger_alerts = true trigger_alerts_checked = "checked" end local enable_label = options.enable_label or i18n("show_alerts.trigger_alert_descr") print[[
]] print[[]] if(entity_type == "host") then print[[]] end print[[
]] print(i18n("device_protocols.alert")) print[[ ]] print(enable_label) print[[
]] print(i18n("host_details.status_ignore")) print[[
]] print(i18n("host_details.multiple_selection")) print[[


]] end -- ################################# function drawAlertSourceSettings(entity_type, alert_source, delete_button_msg, delete_confirm_msg, page_name, page_params, alt_name, show_entity, options) options = options or {} local tab = _GET["tab"] or "min" local ts_utils = require("ts_utils") local ifid = interface.getId() local entity_value = alert_source local subdir = entity_type if interface.isPcapDumpInterface() then if entity_type == "interface" then tab = "flows" else return end else local function printTab(tab, content, sel_tab) if(tab == sel_tab) then print("\t\n") end print('') end -- !isPcapDumpInterface if((tab == "flows") and (entity_type == "interface")) then local flow_callbacks_utils = require "flow_callbacks_utils" flow_callbacks_utils.print_callbacks_config() else local available_modules = user_scripts.load(interface.getId(), user_scripts.script_types.traffic_element, entity_type) local no_modules_available = table.len(available_modules.modules) == 0 if((_POST["to_delete"] ~= nil) and (_POST["SaveAlerts"] == nil)) then if _POST["to_delete"] == "local" then user_scripts.deleteSpecificConfiguration(subdir, available_modules, tab, entity_value) else user_scripts.deleteGlobalConfiguration(subdir, available_modules, tab, options.remote_host) end elseif(not table.empty(_POST)) then user_scripts.handlePOST(subdir, available_modules, tab, entity_value, options.remote_host) end local label if entity_type == "host" then if options.remote_host then label = i18n("remote_hosts") else label = i18n("alerts_thresholds_config.active_local_hosts") end else label = firstToUpper(entity_type) .. "s" end print [[

]] if(tab == "min") then print[[]] end print[[]] print('\n') if no_modules_available then if areAlertsEnabled() then print[[]] else print[[]] end else local benchmarks = user_scripts.getLastBenchmark(ifid, entity_type) or {} for mod_k, user_script in pairsByKeys(available_modules.modules, asc) do local key = user_script.key local gui_conf = user_script.gui local show_input = true if user_script.granularity then -- check if the check is performed and thus has to -- be configured at this granularity show_input = false for _, gran in pairs(user_script.granularity) do if gran == tab then show_input = true break end end end if(user_script.local_only and options.remote_host) then show_input = false end if not gui_conf or not show_input then goto next_module end print("\n") ::next_module:: end end print [[
]] print(i18n("alerts_thresholds_config.threshold_type")) print[[]] print(i18n("chart")) print[[]] print(i18n("alerts_thresholds_config.thresholds_single_source", {source=firstToUpper(entity_type),alt_name=ternary(alt_name ~= nil, alt_name, alert_source)})) print[[]] print(i18n("alerts_thresholds_config.common_thresholds_local_sources", {source=label})) print[[]] print(i18n("flow_callbacks.callback_latest_run")) print[[
]] print(i18n("flow_callbacks.no_callbacks_available")) print[[.
]] print(i18n("flow_callbacks.no_callbacks_available_disabled_alerts", {url = ntop.getHttpPrefix().."/lua/admin/prefs.lua?tab=alerts"})) print[[.
".. (i18n(gui_conf.i18n_title) or gui_conf.i18n_title) .."
") print("".. (i18n(gui_conf.i18n_description) or gui_conf.i18n_description) .."\n") if(tab == "min") then print("
") if ts_utils.exists("elem_user_script:duration", {ifid=ifid, user_script=mod_k, subdir=entity_type}) then print('') end end for _, prefix in pairs({"", "global_"}) do if user_script.gui.input_builder then local k = prefix..key local is_global = (prefix == "global_") local conf print("") if is_global then conf = user_scripts.getGlobalConfiguration(user_script, tab, options.remote_host) else conf = user_scripts.getConfiguration(user_script, tab, entity_value, options.remote_host) end if(conf ~= nil) then -- TODO remove after implementing the new gui local value = ternary(user_script.gui.post_handler == user_scripts.checkbox_post_handler, conf.enabled, conf.script_conf) print(user_script.gui.input_builder(user_script.gui or {}, k, value)) end end end print("\n") local script_benchmark = benchmarks[mod_k] if script_benchmark and (script_benchmark[tab] or script_benchmark["all"]) then local hook = ternary(script_benchmark[tab], tab, "all") if script_benchmark[hook]["tot_elapsed"] then if script_benchmark[hook]["tot_num_calls"] > 1 then print(i18n("flow_callbacks.callback_function_duration_fmt_long", {num_calls = format_utils.formatValue(script_benchmark[hook]["tot_num_calls"]), time = format_utils.secondsToTime(script_benchmark[hook]["tot_elapsed"]), speed = format_utils.formatValue(round(script_benchmark[hook]["avg_speed"], 0))})) else print(i18n("flow_callbacks.callback_function_duration_fmt_short", {time = format_utils.secondsToTime(script_benchmark[hook]["tot_elapsed"])})) end end end print("
]] if not no_modules_available then print[[
]] end print("
" .. i18n("alerts_thresholds_config.notes") .. ":
") print[[ ]] end end -- ################################# function printAlertTables(entity_type, alert_source, page_name, page_params, alt_name, show_entity, options) local has_engaged_alerts, has_past_alerts, has_flow_alerts = false,false,false local has_disabled_alerts = alerts_api.hasEntitiesWithAlertsDisabled(interface.getId()) local tab = _GET["tab"] local have_nedge = ntop.isnEdge() options = options or {} local anomaly_config_key = nil local flow_rate_alert_thresh, syn_alert_thresh if entity_type == "host" then anomaly_config_key = 'ntopng.prefs.'..(options.host_ip)..':'..tostring(options.host_vlan)..'.alerts_config' end print('') if((show_entity) and is_alert_list_tab) then drawAlertTables(has_past_alerts, has_engaged_alerts, has_flow_alerts, has_disabled_alerts, _GET, true, nil, { dont_nest_alerts = true }) elseif(tab == "config") then printConfigTab(entity_type, alert_source, page_name, page_params, alt_name, options) elseif(tab == "probes") and #entity_probes > 0 then printProbesTab(entity_probes, entity_type, alert_source, page_name, page_params, alt_name, options) end end -- ################################# function optimizeAlerts() if(not areAlertsEnabled()) then return end interface.optimizeAlerts() end -- ################################# function housekeepingAlertsMakeRoom(ifId) local prefs = ntop.getPrefs() local max_num_alerts_per_entity = prefs.max_num_alerts_per_entity local max_num_flow_alerts = prefs.max_num_flow_alerts local k = get_make_room_keys(ifId) if ntop.getCache(k["entities"]) == "1" then ntop.delCache(k["entities"]) local res = interface.queryAlertsRaw( "SELECT alert_entity, alert_entity_val, count(*) count", "GROUP BY alert_entity, alert_entity_val HAVING COUNT >= "..max_num_alerts_per_entity) or {} for _, e in pairs(res) do local to_keep = (max_num_alerts_per_entity * 0.8) -- deletes 20% more alerts than the maximum number to_keep = round(to_keep, 0) -- tprint({e=e, total=e.count, to_keep=to_keep, to_delete=to_delete, to_delete_not_discounted=(e.count - max_num_alerts_per_entity)}) local cleanup = interface.queryAlertsRaw( "DELETE", "WHERE alert_entity="..e.alert_entity.." AND alert_entity_val=\""..e.alert_entity_val.."\" " .." AND rowid NOT IN (SELECT rowid FROM alerts WHERE alert_entity="..e.alert_entity.." AND alert_entity_val=\""..e.alert_entity_val.."\" " .." ORDER BY alert_tstamp DESC LIMIT "..to_keep..")", false) end end if ntop.getCache(k["flows"]) == "1" then ntop.delCache(k["flows"]) local res = interface.queryFlowAlertsRaw("SELECT count(*) count", "WHERE 1=1") or {} local count = tonumber(res[1].count) if count ~= nil and count >= max_num_flow_alerts then local to_keep = (max_num_flow_alerts * 0.8) to_keep = round(to_keep, 0) local cleanup = interface.queryFlowAlertsRaw("DELETE", "WHERE rowid NOT IN (SELECT rowid FROM flows_alerts ORDER BY alert_tstamp DESC LIMIT "..to_keep..")") -- tprint({total=count, to_delete=to_delete, cleanup=cleanup}) -- tprint(cleanup) -- TODO: possibly raise a too many flow alerts end end end -- ################################# local function menuEntriesToDbFormat(entries) local res = {} for entry_id, entry_val in pairs(entries) do res[#res + 1] = { id = tostring(entry_id), count = tostring(entry_val), } end return(res) end -- ################################# local function printDisabledAlerts(ifid) print[[ ]] end -- ################################# function drawAlertTables(has_past_alerts, has_engaged_alerts, has_flow_alerts, has_disabled_alerts, get_params, hide_extended_title, alt_nav_tabs, options) local alert_items = {} local url_params = {} local options = options or {} local ifid = interface.getId() print( template.gen("modal_confirm_dialog.html", { dialog={ id = "delete_alert_dialog", action = "deleteAlertById(delete_alert_id)", title = i18n("show_alerts.delete_alert"), message = i18n("show_alerts.confirm_delete_alert").."?", confirm = i18n("delete"), confirm_button = "btn-danger", } }) ) print( template.gen("modal_confirm_dialog.html", { dialog={ id = "release_single_alert", action = "releaseAlert(alert_to_release)", title = i18n("show_alerts.release_alert"), message = i18n("show_alerts.confirm_release_alert"), confirm = i18n("show_alerts.release_alert_action"), confirm_button = "btn-primary", } }) ) print( template.gen("modal_confirm_dialog.html", { dialog={ id = "enable_alert_type", action = "toggleAlert(false)", title = i18n("show_alerts.enable_alerts_title"), message = i18n("show_alerts.enable_alerts_message", { type = "", entity_value = "" }), confirm = i18n("show_alerts.enable_alerts"), } }) ) print( template.gen("modal_confirm_dialog.html", { dialog={ id = "disable_alert_type", action = "toggleAlert(true)", title = i18n("show_alerts.disable_alerts_title"), message = i18n("show_alerts.disable_alerts_message", { type = "", entity_value = "" }), confirm = i18n("show_alerts.disable_alerts"), } }) ) print( template.gen("modal_confirm_dialog.html", { dialog={ id = "myModal", action = "checkModalDelete()", title = "", message = i18n("show_alerts.purge_subj_alerts_confirm", {subj = ''}), confirm = i18n("show_alerts.purge_num_alerts", { num_alerts = '' }), } }) ) for k,v in pairs(get_params) do if k ~= "csrf" then url_params[k] = v end end if not alt_nav_tabs then if not options.dont_nest_alerts then print("
") end print[[ ]] nav_tab_id = "alert-tabs" else nav_tab_id = alt_nav_tabs end print[[ ]] if not alt_nav_tabs then print [[
]] end local status = _GET["status"] if(status == nil) then local tab = _GET["tab"] if(tab == "past_alert_list") then status = "historical" elseif(tab == "flow_alert_list") then status = "historical-flows" end end local status_reset = (status == nil) local ts_utils = require "ts_utils" if(has_engaged_alerts) then alert_items[#alert_items + 1] = { ["label"] = i18n("show_alerts.engaged_alerts"), ["chart"] = ternary(ts_utils.exists("iface:engaged_alerts", {ifid = ifid}), "iface:engaged_alerts", ""), ["div-id"] = "table-engaged-alerts", ["status"] = "engaged"} elseif status == "engaged" then status = nil; status_reset = 1 end if(has_past_alerts) then alert_items[#alert_items +1] = { ["label"] = i18n("show_alerts.past_alerts"), ["chart"] = "", ["div-id"] = "table-alerts-history", ["status"] = "historical"} elseif status == "historical" then status = nil; status_reset = 1 end if(has_flow_alerts) then alert_items[#alert_items +1] = { ["label"] = i18n("show_alerts.flow_alerts"), ["chart"] = "", ["div-id"] = "table-flow-alerts-history", ["status"] = "historical-flows"} elseif status == "historical-flows" then status = nil; status_reset = 1 end if has_disabled_alerts then alert_items[#alert_items +1] = { ["label"] = i18n("show_alerts.disabled_alerts"), ["chart"] = "", ["div-id"] = "table-disabled-alerts", ["status"] = "disabled-alerts"} end for k, t in ipairs(alert_items) do local clicked = "0" if((not alt_nav_tabs) and ((k == 1 and status == nil) or (status ~= nil and status == t["status"]))) then clicked = "1" end print [[
]] if t["status"] == "disabled-alerts" then printDisabledAlerts(ifid) goto next_menu_item end print[[ ]] ::next_menu_item:: end local zoom_vals = { { i18n("show_alerts.5_min"), 5*60*1, i18n("show_alerts.older_5_minutes_ago") }, { i18n("show_alerts.30_min"), 30*60*1, i18n("show_alerts.older_30_minutes_ago") }, { i18n("show_alerts.1_hour"), 60*60*1, i18n("show_alerts.older_1_hour_ago") }, { i18n("show_alerts.1_day"), 60*60*24, i18n("show_alerts.older_1_day_ago") }, { i18n("show_alerts.1_week"), 60*60*24*7, i18n("show_alerts.older_1_week_ago") }, { i18n("show_alerts.1_month"), 60*60*24*31, i18n("show_alerts.older_1_month_ago") }, { i18n("show_alerts.6_months"), 60*60*24*31*6, i18n("show_alerts.older_6_months_ago") }, { i18n("show_alerts.1_year"), 60*60*24*366 , i18n("show_alerts.older_1_year_ago") } } if(has_engaged_alerts or has_past_alerts or has_flow_alerts) then -- trigger the click on the right tab to force table load print[[ ]] if not alt_nav_tabs then print [[
]] end local has_fixed_period = ((not isEmptyString(_GET["epoch_begin"])) or (not isEmptyString(_GET["epoch_end"]))) print('
') print('
' .. i18n("show_alerts.alerts_to_purge") .. ': ') print[[ ]] -- we need to dynamically modify parameters at js-time because we switch tab local delete_params = getTabParameters(url_params, nil) delete_params.epoch_end = -1 print[[
]] end end -- ################################# function drawAlerts(options) local has_engaged_alerts = hasAlerts("engaged", getTabParameters(_GET, "engaged")) local has_past_alerts = hasAlerts("historical", getTabParameters(_GET, "historical")) local has_disabled_alerts = alerts_api.hasEntitiesWithAlertsDisabled(interface.getId()) local has_flow_alerts = false if _GET["entity"] == nil then has_flow_alerts = hasAlerts("historical-flows", getTabParameters(_GET, "historical-flows")) end checkDeleteStoredAlerts() checkDisableAlerts() return drawAlertTables(has_past_alerts, has_engaged_alerts, num_flow_alerts, has_disabled_alerts, _GET, true, nil, options) end -- ################################# function newAlertsWorkingStatus(ifstats, granularity) local res = { granularity = granularity, engine = alertEngine(granularity), ifid = ifstats.id, now = os.time(), interval = granularity2sec(granularity), } return res end -- ################################# -- A redis set with mac addresses as keys local function getActiveDevicesHashKey(ifid) return "ntopng.cache.active_devices.ifid_" .. ifid end function deleteActiveDevicesKey(ifid) ntop.delCache(getActiveDevicesHashKey(ifid)) end -- ################################# local function getSavedDeviceNameKey(mac) return "ntopng.cache.devnames." .. mac end local function setSavedDeviceName(mac, name) local key = getSavedDeviceNameKey(mac) ntop.setCache(key, name) end local function getSavedDeviceName(mac) local key = getSavedDeviceNameKey(mac) return ntop.getCache(key) end function check_macs_alerts(ifid) local active_devices_set = getActiveDevicesHashKey(ifid) local seen_devices_hash = getFirstSeenDevicesHashKey(ifid) local seen_devices = ntop.getHashAllCache(seen_devices_hash) or {} local max_active_devices_cardinality = 16384 local prev_active_devices = swapKeysValues(ntop.getMembersCache(active_devices_set) or {}) local alert_new_devices_enabled = ntop.getPref("ntopng.prefs.alerts.device_first_seen_alert") == "1" local alert_device_connection_enabled = ntop.getPref("ntopng.prefs.alerts.device_connection_alert") == "1" local new_active_devices = {} local num_devices = table.len(seen_devices) if(num_devices >= max_active_devices_cardinality) then traceError(TRACE_INFO, TRACE_CONSOLE, string.format("Too many active devices, discarding %u devices", num_devices)) ntop.delCache(active_devices_set) prev_active_devices = {} end callback_utils.foreachDevice(getInterfaceName(ifid), nil, function(devicename, devicestats, devicebase) -- note: location is always lan when capturing from a local interface if (not devicestats.special_mac) and (devicestats.location == "lan") then local mac = devicestats.mac if not seen_devices[mac] then -- First time we see a device ntop.setHashCache(seen_devices_hash, mac, tostring(os.time())) if alert_new_devices_enabled then local name = getDeviceName(mac) setSavedDeviceName(mac, name) alerts_api.store( alerts_api.macEntity(mac), alerts_api.newDeviceType(name) ) end end if not prev_active_devices[mac] then -- Device connection ntop.setMembersCache(active_devices_set, mac) if alert_device_connection_enabled then local name = getDeviceName(mac) setSavedDeviceName(mac, name) alerts_api.store( alerts_api.macEntity(mac), alerts_api.deviceHasConnectedType(name) ) end else new_active_devices[mac] = 1 end end end) for mac in pairs(prev_active_devices) do if not new_active_devices[mac] then -- Device disconnection local name = getSavedDeviceName(mac) ntop.delMembersCache(active_devices_set, mac) if alert_device_connection_enabled then alerts_api.store( alerts_api.macEntity(mac), alerts_api.deviceHasDisconnectedType(name) ) end end end end -- ################################# -- A redis set with host pools as keys local function getActivePoolsHashKey(ifid) return "ntopng.cache.active_pools.ifid_" .. ifid end function deleteActivePoolsKey(ifid) ntop.delCache(getActivePoolsHashKey(ifid)) end -- ################################# -- Redis hashe with key=pool and value=list of quota_exceed_items, separated by | local function getPoolsQuotaExceededItemsKey(ifid) return "ntopng.cache.quota_exceeded_pools.ifid_" .. ifid end function deletePoolsQuotaExceededItemsKey(ifid) ntop.delCache(getPoolsQuotaExceededItemsKey(ifid)) end -- ################################# function check_host_pools_alerts(ifid) local active_pools_set = getActivePoolsHashKey(ifid) local prev_active_pools = swapKeysValues(ntop.getMembersCache(active_pools_set)) or {} local alert_pool_connection_enabled = ntop.getPref("ntopng.prefs.alerts.pool_connection_alert") == "1" local alerts_on_quota_exceeded = ntop.isPro() and ntop.getPref("ntopng.prefs.alerts.quota_exceeded_alert") == "1" local pools_stats = nil local quota_exceeded_pools_key = getPoolsQuotaExceededItemsKey(ifid) local quota_exceeded_pools_values = ntop.getHashAllCache(quota_exceeded_pools_key) or {} local quota_exceeded_pools = {} local now_active_pools = {} -- Deserialize quota_exceeded_pools for pool, v in pairs(quota_exceeded_pools_values) do quota_exceeded_pools[pool] = {} for _, group in pairs(split(quota_exceeded_pools_values[pool], "|")) do local parts = split(group, "=") if #parts == 2 then local proto = parts[1] local quota = parts[2] local parts = split(quota, ",") quota_exceeded_pools[pool][proto] = {toboolean(parts[1]), toboolean(parts[2])} end end -- quota_exceeded_pools[pool] is like {Youtube={true, false}}, where true is bytes_exceeded, false is time_exceeded end if ntop.isPro() then pools_stats = interface.getHostPoolsStats() end local pools = interface.getHostPoolsInfo() if(pools ~= nil) and (pools_stats ~= nil) then for pool, info in pairs(pools.num_members_per_pool) do local pool_stats = pools_stats[tonumber(pool)] local pool_exceeded_quotas = quota_exceeded_pools[pool] or {} -- Pool quota if((pool_stats ~= nil) and (shaper_utils ~= nil)) then local quotas_info = shaper_utils.getQuotasInfo(ifid, pool, pool_stats) for proto, info in pairs(quotas_info) do local prev_exceeded = pool_exceeded_quotas[proto] or {false,false} if alerts_on_quota_exceeded then if info.bytes_exceeded and not prev_exceeded[1] then alerts_api.store( alerts_api.hostPoolEntity(pool), alerts_api.poolQuotaExceededType(pool, proto, "traffic_quota", info.bytes_value, info.bytes_quota) ) end if info.time_exceeded and not prev_exceeded[2] then alerts_api.store( alerts_api.hostPoolEntity(pool), alerts_api.poolQuotaExceededType(pool, proto, "time_quota", info.time_value, info.time_quota) ) end end if not info.bytes_exceeded and not info.time_exceeded then -- delete as no quota is left pool_exceeded_quotas[proto] = nil else -- update/add serialized pool_exceeded_quotas[proto] = {info.bytes_exceeded, info.time_exceeded} end end if table.empty(pool_exceeded_quotas) then ntop.delHashCache(quota_exceeded_pools_key, pool) else -- Serialize the new quota information for the pool for proto, value in pairs(pool_exceeded_quotas) do pool_exceeded_quotas[proto] = table.concat({tostring(value[1]), tostring(value[2])}, ",") end ntop.setHashCache(quota_exceeded_pools_key, pool, table.tconcat(pool_exceeded_quotas, "=", "|")) end end -- Pool presence if (pool ~= host_pools_utils.DEFAULT_POOL_ID) and (info.num_hosts > 0) then now_active_pools[pool] = 1 if not prev_active_pools[pool] then -- Pool connection ntop.setMembersCache(active_pools_set, pool) if alert_pool_connection_enabled then alerts_api.store( alerts_api.hostPoolEntity(pool), alerts_api.poolConnectionType(pool) ) end end end end end -- Pool presence for pool in pairs(prev_active_pools) do if not now_active_pools[pool] then -- Pool disconnection ntop.delMembersCache(active_pools_set, pool) if alert_pool_connection_enabled then alerts_api.store( alerts_api.hostPoolEntity(pool), alerts_api.poolDisconnectionType(pool) ) end end end end -- ################################# function disableAlertsGeneration() if not haveAdminPrivileges() then return end -- Ensure we do not conflict with others ntop.setPref("ntopng.prefs.disable_alerts_generation", "1") ntop.reloadPreferences() if(verbose) then io.write("[Alerts] Disable done\n") end end -- ################################# function flushAlertsData() if not haveAdminPrivileges() then return end local selected_interface = ifname local ifnames = interface.getIfNames() local force_query = true local generation_toggle_backup = ntop.getPref("ntopng.prefs.disable_alerts_generation") if(verbose) then io.write("[Alerts] Temporary disabling alerts generation...\n") end ntop.setAlertsTemporaryDisabled(true); ntop.msleep(3000) callback_utils.foreachInterface(ifnames, nil, function(ifname, ifstats) if(verbose) then io.write("[Alerts] Processing interface "..ifname.."...\n") end if(verbose) then io.write("[Alerts] Flushing SQLite configuration...\n") end performAlertsQuery("DELETE", "engaged", {}, force_query) performAlertsQuery("DELETE", "historical", {}, force_query) performAlertsQuery("DELETE", "historical-flows", {}, force_query) end) if(verbose) then io.write("[Alerts] Flushing Redis configuration...\n") end deleteCachePattern("ntopng.prefs.*alert*") deleteCachePattern("ntopng.alerts.*") user_scripts.deleteConfigurations() alerts_api.purgeAlertsPrefs() for _, key in pairs(get_make_room_keys("*")) do deleteCachePattern(key) end if(verbose) then io.write("[Alerts] Enabling alerts generation...\n") end ntop.setAlertsTemporaryDisabled(false); ntop.setPref("ntopng.prefs.disable_alerts_generation", generation_toggle_backup) refreshAlerts(interface.getId()) if(verbose) then io.write("[Alerts] Flush done\n") end interface.select(selected_interface) end -- ################################# function alertNotificationActionToLabel(action) local label = "" if action == "engage" then label = "[Engaged]" elseif action == "release" then label = "[Released]" end return label end -- ################################# function formatAlertMessage(ifid, alert) local msg if(alert.alert_entity == alert_consts.alertEntity("flow") or (alert.alert_entity == nil)) then msg = formatRawFlow(alert, alert["alert_json"]) else msg = alert["alert_json"] if isEmptyString(msg) then msg = {} elseif(string.sub(msg, 1, 1) == "{") then msg = json.decode(msg) end local description = alertTypeDescription(alert.alert_type) if(type(description) == "string") then -- localization string msg = i18n(description, msg) elseif(type(description) == "function") then msg = description(ifid, alert, msg) end end return(msg) end -- ################################# function notification_timestamp_asc(a, b) return (a.alert_tstamp < b.alert_tstamp) end function notification_timestamp_rev(a, b) return (a.alert_tstamp > b.alert_tstamp) end -- Returns a summary of the alert as readable text function formatAlertNotification(notif, options) local defaults = { nohtml = false, show_severity = true, } options = table.merge(defaults, options) local msg = "[" .. formatEpoch(notif.alert_tstamp or 0) .. "]" msg = msg .. ternary(options.show_severity == false, "", "[" .. alertSeverityLabel(notif.alert_severity, options.nohtml) .. "]") .. "[" .. alertTypeLabel(notif.alert_type, options.nohtml) .."]" -- entity can be hidden for example when one is OK with just the message if options.show_entity then msg = msg.."["..alert_consts.alertEntityLabel(notif.alert_entity).."]" if notif.alert_entity ~= "flow" then local ev = notif.alert_entity_val if notif.alert_entity == "host" then -- suppresses @0 when the vlan is zero ev = hostinfo2hostkey(hostkey2hostinfo(notif.alert_entity_val)) end msg = msg.."["..ev.."]" end end -- add the label, that is, engaged or released msg = msg .. alertNotificationActionToLabel(notif.action).. " " local alert_message = formatAlertMessage(notif.ifid, notif) if options.nohtml then msg = msg .. noHtml(alert_message) else msg = msg .. alert_message end return msg end -- ############################################## -- Processes queued alerts and returns the information necessary to store them. -- Alerts are only enqueued by AlertsQueue in C. From lua, the alerts_api -- can be called directly as slow operations will be postponed local function processStoreAlertFromQueue(alert) local entity_info = nil local type_info = nil interface.select(tostring(alert.ifid)) if(alert.alert_type == "misconfigured_dhcp_range") then local router_info = {host = alert.router_ip, vlan = alert.vlan_id} entity_info = alerts_api.hostAlertEntity(alert.client_ip, alert.vlan_id) type_info = alerts_api.ipOutsideDHCPRangeType(router_info, alert.mac_address, alert.client_mac, alert.sender_mac) elseif(alert.alert_type == "slow_periodic_activity") then entity_info = alerts_api.periodicActivityEntity(alert.path) type_info = alerts_api.slowPeriodicActivityType(alert.duration_ms, alert.max_duration_ms) elseif(alert.alert_type == "mac_ip_association_change") then if(ntop.getPref("ntopng.prefs.ip_reassignment_alerts") == "1") then local name = getSavedDeviceName(alert.new_mac) entity_info = alerts_api.macEntity(alert.new_mac) type_info = alerts_api.macIpAssociationChangeType(name, alert.ip, alert.old_mac, alert.new_mac) end elseif(alert.alert_type == "login_failed") then entity_info = alerts_api.userEntity(alert.user) type_info = alerts_api.loginFailedType() elseif(alert.alert_type == "broadcast_domain_too_large") then entity_info = alerts_api.macEntity(alert.src_mac) type_info = alerts_api.broadcastDomainTooLargeType(alert.src_mac, alert.dst_mac, alert.vlan_id, alert.spa, alert.tpa) elseif(alert.alert_type == "remote_to_remote") then if(ntop.getPref("ntopng.prefs.remote_to_remote_alerts") == "1") then local host_info = {host = alert.host, vlan = alert.vlan} entity_info = alerts_api.hostAlertEntity(alert.host, alert.vlan) type_info = alerts_api.remoteToRemoteType(host_info, alert.mac_address) end elseif((alert.alert_type == "user_activity") and (alert.scope == "login")) then entity_info = alerts_api.userEntity(alert.user) type_info = alerts_api.userActivityType("login", nil, nil, nil, "authorized") elseif(alert.alert_type == "nfq_flushed") then entity_info = alerts_api.interfaceAlertEntity(alert.ifid) type_info = alerts_api.nfqFlushedType(getInterfaceName(alert.ifid), alert.pct, alert.tot, alert.dropped) else traceError(TRACE_ERROR, TRACE_CONSOLE, "Unknown alert type " .. (alert.alert_type or "")) end return entity_info, type_info end -- ############################################## -- Global function -- NOTE: this is executed in a system VM, with no interfaces references function checkStoreAlertsFromC(deadline) if(not areAlertsEnabled()) then return end while(os.time() <= deadline) do local message = ntop.popInternalAlerts() if((message == nil) or (message == "")) then break end if(verbose) then print(message.."\n") end local alert = json.decode(message) if(alert == nil) then if(verbose) then io.write("JSON Decoding error: "..message.."\n") end else local entity_info, type_info = processStoreAlertFromQueue(alert) if((type_info ~= nil) and (entity_info ~= nil)) then alerts_api.store(entity_info, type_info, alert.alert_tstamp) end end end end -- ############################################## -- NOTE: this is executed in a system VM, with no interfaces references function processAlertNotifications(now, periodic_frequency, force_export) if(not areAlertsEnabled()) then return end local interfaces = interface.getIfNames() -- Get new alerts while(true) do local json_message = ntop.popAlertNotification() if((json_message == nil) or (json_message == "")) then break end if(verbose) then io.write("Alert Notification: " .. json_message .. "\n") end local message = json.decode(json_message) if(not message) then goto continue end local str_ifid = tostring(message.ifid) if((interfaces[str_ifid] == nil) and (str_ifid ~= getSystemInterfaceId())) then goto continue end interface.select(str_ifid) if message.is_flow_alert then -- Silly but necessary due to the notifyFlowAlert message.alert_entity = alert_consts.alert_entities.flow.entity_id message.alert_entity_val = "flow" message.action = nil json_message = json.encode(message) end alert_endpoints.dispatchNotification(message, json_message) ::continue:: end alert_endpoints.processNotifications(now, periodic_frequency) end -- ############################################## local function notify_ntopng_status(started) local info = ntop.getInfo() local severity = alertSeverity("info") local msg local msg_details = string.format("%s v.%s (%s) [pid: %s][options: %s]", info.product, info.version, info.OS, info.pid, info.command_line) local anomalous = false local event if(started) then -- let's check if we are restarting from an anomalous termination -- e.g., from a crash if not recovery_utils.check_clean_shutdown() then -- anomalous termination msg = string.format("%s %s", i18n("alert_messages.ntopng_anomalous_termination", {url="https://www.ntop.org/support/need-help-2/need-help/"}), msg_details) severity = alertSeverity("error") anomalous = true event = "anomalous_termination" else -- normal termination msg = string.format("%s %s", i18n("alert_messages.ntopng_start"), msg_details) event = "start" end else msg = string.format("%s %s", i18n("alert_messages.ntopng_stop"), msg_details) event = "stop" end local entity_value = "ntopng" obj = { entity_type = alert_consts.alertEntity("process"), entity_value=entity_value, type = alertType("alert_process_notification"), severity = severity, message = msg, when = os.time() } if anomalous then telemetry_utils.notify(obj) end local entity_info = alerts_api.processEntity(entity_value) local type_info = alerts_api.processNotificationType(event, severity, msg_details) interface.select(getSystemInterfaceId()) return(alerts_api.store(entity_info, type_info)) end function notify_ntopng_start() return(notify_ntopng_status(true)) end function notify_ntopng_stop() return(notify_ntopng_status(false)) end