-- -- (C) 2014-21 - ntop.org -- local dirs = ntop.getDirs() package.path = dirs.installdir .. "/scripts/lua/modules/pools/?.lua;" .. package.path package.path = dirs.installdir .. "/scripts/lua/modules/alert_store/?.lua;" .. package.path -- 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 = require "host_pools" local recovery_utils = require "recovery_utils" local alert_severities = require "alert_severities" local alert_entities = require "alert_entities" 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 icmp_utils = require "icmp_utils" 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 -- ############################################## local alert_utils = {} -- ############################################## if ntop.isEnterpriseM() then local dirs = ntop.getDirs() package.path = dirs.installdir .. "/pro/scripts/lua/enterprise/modules/?.lua;" .. package.path -- add enterprise utils to this module alert_utils = require "enterprise_alert_utils" end -- ############################################## local function alertTypeDescription(alert_key, entity_id) local alert_id = alert_consts.getAlertType(alert_key, entity_id) if(alert_id) then if alert_consts.alert_types[alert_id].format then -- New API return alert_consts.alert_types[alert_id].format else -- TODO: Possible removed once migration is done return(alert_consts.alert_types[alert_id].i18n_description) end end return nil end -- ############################################## local 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 -- ################################# local function performAlertsQuery(statement, what, opts, force_query, group_by) local wargs = {"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)..'"' 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_id) ~= nil then wargs[#wargs+1] = "AND alert_id = "..(opts.alert_id) end if tonumber(opts.severity) ~= nil then wargs[#wargs+1] = "AND severity = "..(opts.severity) end if what == "historical-flows" then if tonumber(opts.alert_l7_proto) ~= nil then wargs[#wargs+1] = "AND l7_proto = "..(opts.alert_l7_proto) end end if((not isEmptyString(opts.sortColumn)) and (not isEmptyString(opts.sortOrder))) then local order_by if opts.sortColumn == "column_date" then order_by = "tstamp" elseif opts.sortColumn == "column_key" then order_by = "rowid" elseif opts.sortColumn == "column_severity" then order_by = "severity" elseif opts.sortColumn == "column_type" then order_by = "alert_id" elseif opts.sortColumn == "column_count" and what ~= "engaged" then order_by = "alert_counter" elseif opts.sortColumn == "column_score" and what ~= "engaged" then order_by = "score" 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, " ") group_by = table.concat(oargs, " ") .. group_by local res -- Uncomment to debug the queries -- tprint(statement.." (from "..what..") WHERE "..query .. " ".. group_by) if((what == "engaged") or (what == "historical")) then res = interface.queryAlertsRaw(statement, query, group_by, force_query) elseif what == "historical-flows" then res = interface.queryFlowAlertsRaw(statement, query, group_by, 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) if(res ~= nil) then return(res.num_alerts) end return(0) end -- ################################# -- Remove pagination options from the options local 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 -- ################################# function alert_utils.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 alert_utils.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_id) local severity_filter = tonumber(params.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) local sort_2_col = {} -- Sort for idx, alert in pairs(alerts) do if sortColumn == "column_type" then sort_2_col[idx] = alert.alert_id elseif sortColumn == "column_severity" then sort_2_col[idx] = alert.severity elseif sortColumn == "column_duration" then sort_2_col[idx] = os.time() - alert.tstamp else -- column_date sort_2_col[idx] = 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 alert_utils.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 = alert_utils.getNumAlerts(what, options) end end return alerts, num_alerts end -- ################################# function alert_utils.getNumAlertsPerHour(what, epoch_begin, epoch_end, alert_id, severity, host_info) local opts = { epoch_begin = epoch_begin, epoch_end = epoch_end, alert_id = alert_id, severity = severity, } if host_info and host_info.host then local entity_info = alerts_api.hostAlertEntity(host_info.host, host_info.vlan) opts.entity = entity_info.alert_entity.entity_id opts.entity_val = entity_info.alert_entity_val end return performAlertsQuery("select (alert_tstamp - alert_tstamp % 3600) as hour, count(*) count", what, opts, nil, "hour") end -- ################################# function alert_utils.getNumAlertsPerType(what, epoch_begin, epoch_end) local opts = { epoch_begin = epoch_begin, epoch_end = epoch_end, } return performAlertsQuery("select alert_id id, count(*) count", what, opts, nil, "alert_id" --[[ group by ]]) end -- ################################# function alert_utils.getNumAlertsPerSeverity(what, epoch_begin, epoch_end) local opts = { epoch_begin = epoch_begin, epoch_end = epoch_end, } return performAlertsQuery("select severity, count(*) count", what, opts, nil, "severity" --[[ group by ]]) 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 -- ################################# local function deleteAlerts(what, options) local opts = getUnpagedAlertOptions(options or {}) performAlertsQuery("DELETE", what, opts) end -- ################################# --@brief Deletes all stored alerts matching a user script `filter` -- @param subdir the modules subdir -- @param user_script The string script identifier -- @param filter An already validated user script filter -- @return nil function alert_utils.deleteAlertsMatchingUserScriptFilter(subdir, user_script, filter) local res = {} local statement = "DELETE " -- TODO: change to DELETE local query = user_scripts.prepareFilterSQLiteWhere(subdir, user_script, filter) if subdir ~= "flow" then res = interface.queryAlertsRaw(statement, query, group_by, true) else res = interface.queryFlowAlertsRaw(statement, query, group_by, true) end end -- ################################# --@brief Deletes all stored alerts matching an host and an IP -- @return nil function alert_utils.deleteFlowAlertsMatching(host_ip, alert_id) local flow_alert_store = require("flow_alert_store").new() flow_alert_store:add_ip_filter(hostkey2hostinfo(host_ip)["host"]) flow_alert_store:add_alert_id_filter(alert_id) -- Perform the actual deletion flow_alert_store:delete() end -- ################################# --@brief Deletes all stored alerts matching an host and an IP -- @return nil function alert_utils.deleteHostAlertsMatching(host_ip, alert_id) local host_alert_store = require("host_alert_store").new() host_alert_store:add_ip_filter(hostkey2hostinfo(host_ip)["host"]) host_alert_store:add_alert_id_filter(alert_id) -- Perform the actual deletion host_alert_store:delete() end -- ################################# -- this function returns an object with parameters specific for one tab function alert_utils.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_id = nil opts.severity = nil end if not isEmptyString(what) then opts.status = what end opts.ifid = interface.getId() return opts end -- ################################# function alert_utils.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 = alert_utils.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 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_id = (alert_consts.alert_types[alert_consts.getAlertType(_POST["alert_type"])]).meta, severity = alert_severities[alert_consts.alertSeverityRaw(_POST["alert_severity"])], subtype = _POST["alert_subtype"], granularity = alert_consts.alerts_granularities[alert_consts.sec2granularity(_POST["alert_granularity"])], } alerts_api.release(entity_info, type_info) end end -- ################################# -- Return more information for the flow alert description local function getAlertTypeInfo(record, alert_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 = alert_info["icmp.icmp_type"], code = alert_info["icmp.icmp_code"]} if table.empty(type_code) and alert_info["icmp"] then -- This is the new format created when setting the alert from lua type_code = {type = alert_info["icmp"]["type"], code = alert_info["icmp"]["code"]} end if alert_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=alert_info["icmp.unreach.dst_ip"], unreach_port=alert_info["icmp.unreach.dst_port"], unreach_protocol = l4_proto_to_string(alert_info["icmp.unreach.protocol"])})) elseif alert_info["icmp"] and alert_info["icmp"]["unreach"] then -- New format res = string.format("[%s]", i18n("icmp_page.icmp_port_unreachable_extra", {unreach_host=alert_info["icmp"]["unreach"]["dst_ip"], unreach_port=alert_info["icmp"]["unreach"]["dst_port"], unreach_protocol = l4_proto_to_string(alert_info["icmp"]["unreach"]["protocol"])})) else res = string.format("[%s]", icmp_utils.get_icmp_label(4 --[[ ipv4 --]], type_code["type"], type_code["code"])) end end return string.format(" %s", res) end -- ################################# -- This function formats flows in alerts local function formatRawFlow(ifid, alert, alert_json) require "flow_utils" local time_bounds local add_links = (not skip_add_links) if interfaceHasNindexSupport() and not skip_add_links then -- only add links if nindex is present add_links = true time_bounds = {getAlertTimeBounds(alert)} end -- TODO: adapter just to be compatible with old alerts, can be removed at some point if alert_json["alert_info"] then alert_json = json.decode(alert_json["alert_info"]) end -- active flow lookup if not interface.isView() and alert_json and alert_json["ntopng.key"] and alert_json["hash_entry_id"] and alert["alert_tstamp"] then -- attempt a lookup on the active flows local active_flow = interface.findFlowByKeyAndHashId(alert_json["ntopng.key"], alert_json["hash_entry_id"]) if active_flow and active_flow["seen.first"] < tonumber(alert["alert_tstamp"]) then return string.format(" %s %s", '', ntop.getHttpPrefix(), active_flow["ntopng.key"], active_flow["hash_entry_id"], getFlowLabel(active_flow, true, true)) end end -- pretend alert is a flow to reuse getFlowLabel local flow = { ["cli.ip"] = alert["cli_addr"], ["cli.port"] = tonumber(alert["cli_port"]), ["cli.blacklisted"] = tostring(alert["cli_blacklisted"]) == "1", ["cli.localhost"] = tostring(alert["cli_localhost"]) == "1", ["srv.ip"] = alert["srv_addr"], ["srv.port"] = tonumber(alert["srv_port"]), ["srv.blacklisted"] = tostring(alert["srv_blacklisted"]) == "1", ["srv.localhost"] = tostring(alert["srv_localhost"]) == "1", ["vlan"] = alert["vlan_id"]} flow = "[ "..(getFlowLabel(flow, false, add_links, time_bounds, {page = "alerts"}) or "").."] " local l4_proto_label = l4_proto_to_string(alert["proto"] or 0) or "" if not isEmptyString(l4_proto_label) then flow = flow.."[" .. l4_proto_label .. "] " end if alert_json ~= nil then -- render the json local msg = "" if not isEmptyString(flow) then msg = msg..flow.." " end if not isEmptyString(alert_json["info"]) then local lb = "" local info if string.len(alert_json["info"]) > 24 then info = "".. shortenString(alert_json["info"], 24) else info = alert_json["info"] end msg = msg.."[" .. info ..lb.."] " end flow = msg end if alert_json then flow = flow..getAlertTypeInfo(alert, alert_json) end return flow end -- ################################# local function getMenuEntries(status, selection_name, get_params) local actual_entries = {} -- table.clone needed to modify some parameters while keeping the original unchanged local params = table.clone(get_params) -- Remove previous filters params.alert_severity = nil params.alert_type = nil params.l7_proto = nil local select_clause = {} -- Contains the selection which is , local group_by_clause = {} -- The clause used to group alerts. It is , for all non-flow alerts if status == "historical-flows" then -- Flows don't have the alert_entity as a table column so we just put the entity id as a placeholder select_clause[#select_clause + 1] = string.format("%u entity", alert_entities.flow.entity_id) else -- TODO: entities will be removed when every entity will have its own database table select_clause[#select_clause + 1] = "alert_entity entity" group_by_clause[#group_by_clause + 1] = "alert_entity" end if selection_name == "severity" then select_clause[#select_clause + 1] = "severity id" group_by_clause[#group_by_clause + 1] = "severity" elseif selection_name == "type" then select_clause[#select_clause + 1] = "alert_id id" group_by_clause[#group_by_clause + 1] = "alert_id" elseif selection_name == "l7_proto" then select_clause[#select_clause + 1] = "l7_proto id" group_by_clause[#group_by_clause + 1] = "l7_proto" end select_clause[#select_clause + 1] = "count(*) count" local select_str = "SELECT "..table.concat(select_clause, ", ") local group_by_str = table.concat(group_by_clause, ", ") actual_entries = performAlertsQuery(select_str, status, params, nil, group_by_str --[[ group by ]]) -- tprint({select_str, group_by_str, status, params}) 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, 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 = alert_consts.alertSeverityLabel elseif selection_name == "type" then id_to_label = alert_consts.alertTypeLabel elseif selection_name == "l7_proto" then id_to_label = interface.getnDPIProtoName 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 if selection_name == "l7_proto" then button_label = firstToUpper(interface.getnDPIProtoName(active_entry))..'' else button_label = firstToUpper(active_entry)..'' end end buttons = buttons..'' buttons = buttons..'
' return buttons end -- ################################# function alert_utils.printAlertTables(entity_type, alert_source, page_name, page_params, alt_name, options) local has_engaged_alerts, has_past_alerts, has_flow_alerts = false,false,false local tab = _GET["tab"] local have_nedge = ntop.isnEdge() options = options or {} local function printTab(tab, content, sel_tab) if(tab == sel_tab) then print("\t\n") end -- these fields will be used to perform queries _GET["entity"] = alert_consts.alertEntity(entity_type) _GET["entity_val"] = alert_source -- possibly process pending delete arguments alert_utils.checkDeleteStoredAlerts() -- possibly add a tab if there are alerts configured for the host has_engaged_alerts = alert_utils.hasAlerts("engaged", alert_utils.getTabParameters(_GET, "engaged")) has_past_alerts = alert_utils.hasAlerts("historical", alert_utils.getTabParameters(_GET, "historical")) has_flow_alerts = alert_utils.hasAlerts("historical-flows", alert_utils.getTabParameters(_GET, "historical-flows")) if(has_engaged_alerts or has_past_alerts or has_flow_alerts) then print("
") print("
") print('') print("
") alert_utils.drawAlertTables(has_past_alerts, has_engaged_alerts, has_flow_alerts, false, _GET, true, nil, { dont_nest_alerts = true }) end -- ################################# function alert_utils.optimizeAlerts() if(not areAlertsEnabled()) then return end interface.optimizeAlerts() end -- ################################# function alert_utils.drawAlertPCAPDownloadDialog(ifid) local modalID = "pcapDownloadModal" print[[ ]] print(template.gen("traffic_extraction_dialog.html", { dialog = { id = modalID, title = i18n("traffic_recording.pcap_download"), message = i18n("traffic_recording.about_to_download_flow", {date_begin = '', date_end = ''}), submit = i18n("traffic_recording.download"), form_method = "post", validator_options = "{ custom: { bpf: bpfValidator }, errors: { bpf: '"..i18n("traffic_recording.invalid_bpf").."' } }", form_action = ntop.getHttpPrefix().."/lua/traffic_extraction.lua", form_onsubmit = "submitPcapDownload", advanced_class = "d-none", extract_now_class = "d-none", -- direct download only }})) print(template.gen("modal_confirm_dialog.html", { dialog = { id = "no-recording-data", title = i18n("traffic_recording.pcap_download"), message = "", }})) end -- ################################# function alert_utils.drawAlertTables(has_past_alerts, has_engaged_alerts, has_flow_alerts, has_disabled_alerts, get_params, hide_extended_title, alt_nav_tabs, options, disable_delete) local alert_items = {} local url_params = {} local options = options or {} local ifid = interface.getId() local additional_params = {} local err = "" -- this paramater is used to print out a card container for the table local is_standalone = options.is_standalone or false 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_alert_filter_dialog.html", { dialog={ id = "filter_alert_dialog", action = "filterAlertByFilters(subdir, script_key, alert_id)", title = i18n("show_alerts.filter_alert"), message = i18n("show_alerts.confirm_filter_alert"), delete_message = i18n("show_alerts.confirm_delete_filtered_alerts"), delete_alerts = i18n("delete_disabled_alerts"), alert_filter = "default_filter", confirm = i18n("filter"), confirm_button = "btn-warning", } }) ) -- Filtering for flow alerts print( template.gen("modal_flow_alerts_filter_dialog.html", { dialog={ id = "flow_alerts_filter_dialog", action = "filterFlowAlerts(alert_id)", title = i18n("show_alerts.filter_alert"), message = i18n("show_alerts.confirm_filter_alert"), delete_message = i18n("show_alerts.confirm_delete_filtered_alerts"), delete_alerts = i18n("delete_disabled_alerts"), alert_filter = "default_filter", confirm = i18n("filter"), confirm_button = "btn-warning", } }) ) -- Filtering for host alerts print( template.gen("modal_host_alerts_filter_dialog.html", { dialog={ id = "host_alerts_filter_dialog", action = "filterHostAlerts(alert_id)", title = i18n("show_alerts.filter_alert"), message = i18n("show_alerts.confirm_filter_alert"), delete_message = i18n("show_alerts.confirm_delete_filtered_alerts"), delete_alerts = i18n("delete_disabled_alerts"), alert_filter = "default_filter", confirm = i18n("filter"), confirm_button = "btn-warning", } }) ) 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 = "myModal", action = "checkModalDelete()", title = i18n("show_alerts.purge_all_alerts"), confirm_button = "btn-danger", custom_alert_class = "alert alert-danger", message = i18n("show_alerts.purge_subj_alerts_confirm", {subj = ''}), confirm = i18n("show_alerts.purge_num_alerts", { num_alerts = '' }), } }) ) if is_standalone then print("
") print("
") end for k,v in pairs(get_params) do if k ~= "csrf" then url_params[k] = v end end if not alt_nav_tabs then print[[ ]] nav_tab_id = "alert-tabs" else nav_tab_id = alt_nav_tabs end if is_standalone then print("
") end print[[ ]] if not alt_nav_tabs then print [[
]] 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) if(has_engaged_alerts) then alert_items[#alert_items + 1] = { ["label"] = i18n("show_alerts.engaged_alerts"), ["chart"] = ternary(areInterfaceTimeseriesEnabled(ifid), "iface:alerts_stats", ""), ["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 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 [[
]] 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 [[
]] print [[
]] end local has_fixed_period = ((not isEmptyString(_GET["epoch_begin"])) or (not isEmptyString(_GET["epoch_end"]))) -- the dont_print_footer option is used to skip the card footer printing if not options.dont_print_footer then print([[ ]]) end print[[
]] end end -- ################################# function alert_utils.drawAlerts(options) local has_engaged_alerts = alert_utils.hasAlerts("engaged", alert_utils.getTabParameters(_GET, "engaged")) local has_past_alerts = alert_utils.hasAlerts("historical", alert_utils.getTabParameters(_GET, "historical")) local has_flow_alerts = false if _GET["entity"] == nil then has_flow_alerts = alert_utils.hasAlerts("historical-flows", alert_utils.getTabParameters(_GET, "historical-flows")) end alert_utils.checkDeleteStoredAlerts() return alert_utils.drawAlertTables(has_past_alerts, has_engaged_alerts, num_flow_alerts, false, _GET, true, nil, options) end -- ################################# -- A redis set with mac addresses as keys function alert_utils.getActiveDevicesHashKey(ifid) return "ntopng.cache.active_devices.ifid_" .. ifid end function alert_utils.deleteActiveDevicesKey(ifid) ntop.delCache(alert_utils.getActiveDevicesHashKey(ifid)) end -- ################################# -- A redis set with host pools as keys local function getActivePoolsHashKey(ifid) return "ntopng.cache.active_pools.ifid_" .. ifid end function alert_utils.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 alert_utils.check_host_pools_alerts(params, ifid, alert_pool_connection_enabled, alerts_on_quota_exceeded) local active_pools_set = getActivePoolsHashKey(ifid) local prev_active_pools = swapKeysValues(ntop.getMembersCache(active_pools_set)) or {} local pools_stats = interface.getHostPoolsStats() 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 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 local alert = alert_consts.alert_types.alert_quota_exceeded.new( "traffic_quota", pool, proto, info.bytes_value, info.bytes_quota ) alert:set_severity(params.user_script_config.severity) alert:store(alerts_api.hostPoolEntity(pool)) end if info.time_exceeded and not prev_exceeded[2] then local alert = alert_consts.alert_types.alert_quota_exceeded.new( "time_quota", pool, proto, info.time_value, info.time_quota ) alert:set_severity(alert_severities.warning) alert:store(alerts_api.hostPoolEntity(pool)) 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.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 local alert = alert_consts.alert_types.alert_host_pool_connection.new( pool ) alert:set_severity(alert_severities.notice) alert:store(alerts_api.hostPoolEntity(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 local alert = alert_consts.alert_types.alert_host_pool_disconnection.new( pool ) alert:set_severity(alert_severities.notice) alert:store(alerts_api.hostPoolEntity(pool)) end end end end -- ################################# function alert_utils.disableAlertsGeneration() if not haveAdminPrivileges() then return end -- Ensure we do not conflict with others ntop.setPref("ntopng.prefs.disable_alerts_generation", "1") if(verbose) then io.write("[Alerts] Disable done\n") end end -- ################################# function alert_utils.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.alerts.*") deleteCachePattern("ntopng.prefs.alerts.*") -- Avoid using 'alert' instead of 'alerts' if we do not want to touch user scripts configurations -- as it also deletes ntopng.prefs.plugins_consts_utils.assigned_ids.const_type_alert and others deleteCachePattern("ntopng.prefs.*alerts*") 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 -- ################################# local function alertNotificationActionToLabel(action, use_emoji) local label = "[" if action == "engage" then if(use_emoji) then label = label .."\xE2\x9D\x97 " end label = label .. "Engaged]" elseif action == "release" then if(use_emoji) then label = label .."\xE2\x9C\x94 " end label = label .. "Released]" end return label end -- ################################# function alert_utils.getConfigsetAlertLink(alert_json) local info = alert_json.alert_generation or (alert_json.alert_info and alert_json.alert_info.alert_generation) if(info and isAdministrator()) then return(' '.. '') end return('') end -- ################################# function alert_utils.getAlertInfo(alert) local alert_json = alert["json"] or alert["alert_json"] if isEmptyString(alert_json) then alert_json = {} elseif(string.sub(alert_json, 1, 1) == "{") then alert_json = json.decode(alert_json) or {} end return alert_json end -- ################################# function alert_utils.formatAlertMessage(ifid, alert, alert_json, skip_live_data) local msg if(alert_json == nil) then alert_json = alert_utils.getAlertInfo(alert) end msg = alert_json local description = alertTypeDescription(alert.alert_id, alert.entity_id) if(type(description) == "string") then -- localization string msg = i18n(description, msg) elseif(type(description) == "function") then msg = description(ifid, alert, msg) end if(type(msg) == "table") then return("") end if(msg) then if(alert_consts.getAlertType(alert.alert_id, alert.entity_id) == "alert_am_threshold_cross") then local plugins_utils = require "plugins_utils" local active_monitoring_utils = plugins_utils.loadModule("active_monitoring", "am_utils") local host = json.decode(alert.json)["host"] if host and host.measurement and not host.is_infrastructure then msg = msg .. ' ' end else msg = msg .. alert_utils.getConfigsetAlertLink(alert_json) end end return(msg or "") end -- ################################# function alert_utils.formatFlowAlertMessage(ifid, alert, alert_json, skip_live_data) local msg if(alert_json == nil) then alert_json = alert_utils.getAlertInfo(alert) end msg = alert_json local description = alertTypeDescription(alert.alert_id, alert_entities.flow.entity_id) if(type(description) == "string") then -- localization string msg = i18n(description, msg) elseif(type(description) == "function") then msg = description(ifid, alert, msg) end return msg or "" end -- ################################# function alert_utils.notification_timestamp_rev(a, b) return (a.tstamp > b.tstamp) end -- Returns a summary of the alert as readable text function alert_utils.formatAlertNotification(notif, options) local defaults = { nohtml = false, show_severity = true, } options = table.merge(defaults, options) local ifname local severity local when if(notif.ifid ~= -1) then ifname = string.format(" [%s]", getInterfaceName(notif.ifid)) else ifname = "" end if(options.show_severity == false) then severity = "" else severity = " [" .. alert_consts.alertSeverityLabel(notif.severity, options.nohtml, options.emoji) .. "]" end if(options.nodate == true) then when = "" else when = formatEpoch(notif.tstamp_end or notif.tstamp or 0) if(not options.no_bracket_around_date) then when = "[" .. when .. "]" end when = when .. " " end local msg = string.format("%s%s%s [%s]", when, ifname, severity, alert_consts.alertTypeLabel(notif.alert_id, 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.entity_id).."]" if notif.entity_id ~= "flow" then local ev = notif.entity_val if notif.entity_id == "host" then -- suppresses @0 when the vlan is zero ev = hostinfo2hostkey(hostkey2hostinfo(notif.entity_val)) end msg = msg.."["..(ev or '').."]" end end -- add the label, that is, engaged or released msg = msg .. " " .. alertNotificationActionToLabel(notif.action, options.emoji).. " " local alert_message = alert_utils.formatAlertMessage(notif.ifid, notif) if(options.add_cr) then msg = msg .. "\n" end 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_id == "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 = alert_consts.alert_types.alert_ip_outsite_dhcp_range.new( router_info, alert.mac_address, alert.client_mac, alert.sender_mac ) type_info:set_severity(alert_severities.warning) type_info:set_subtype(string.format("%s_%s_%s", hostinfo2hostkey(router_info), alert.client_mac, alert.sender_mac)) elseif(alert.alert_id == "mac_ip_association_change") then local name = getDeviceName(alert.new_mac) entity_info = alerts_api.macEntity(alert.new_mac) type_info = alert_consts.alert_types.alert_mac_ip_association_change.new( name, alert.ip, alert.old_mac, alert.new_mac ) type_info:set_severity(alert_severities.warning) type_info:set_subtype(string.format("%s_%s_%s", alert.ip, alert.old_mac, alert.new_mac)) elseif(alert.alert_id == "login_failed") then entity_info = alerts_api.userEntity(alert.user) type_info = alert_consts.alert_types.alert_login_failed.new() type_info:set_severity(alert_severities.warning) elseif(alert.alert_id == "broadcast_domain_too_large") then entity_info = alerts_api.macEntity(alert.src_mac) type_info = alert_consts.alert_types.alert_broadcast_domain_too_large.new(alert.src_mac, alert.dst_mac, alert.vlan_id, alert.spa, alert.tpa) type_info:set_severity(alert_severities.warning) type_info:set_subtype(string.format("%u_%s_%s_%s_%s", alert.vlan_id, alert.src_mac, alert.spa, alert.dst_mac, alert.tpa)) elseif((alert.alert_id == "user_activity") and (alert.scope == "login")) then entity_info = alerts_api.userEntity(alert.user) type_info = alert_consts.alert_types.alert_user_activity.new( "login", nil, nil, nil, "authorized" ) type_info:set_severity(alert_severities.notice) type_info:set_subtype("login//") elseif(alert.alert_id == "nfq_flushed") then entity_info = alerts_api.interfaceAlertEntity(alert.ifid) type_info = alert_consts.alert_types.alert_nfq_flushed.new( getInterfaceName(alert.ifid), alert.pct, alert.tot, alert.dropped ) type_info:set_severity(alert_severities.error) else traceError(TRACE_ERROR, TRACE_CONSOLE, "Unknown alert type " .. (alert.alert_id or "")) end return entity_info, type_info end -- ############################################## -- @brief Process notifications arriving from the internal C queue -- Such notifications are transformed into stored alerts function alert_utils.process_notifications_from_c_queue() local budget = 1024 -- maximum 1024 alerts per call local budget_used = 0 -- Check for alerts pushed by the datapath to an internal queue (from C) -- and store them (push them to the SQLite and Notification queues). -- NOTE: this is executed in a system VM, with no interfaces references while budget_used <= budget do local alert = ntop.popInternalAlerts() if alert == nil then break end if(verbose) then tprint(alert) end local entity_info, type_info = processStoreAlertFromQueue(alert) if type_info and entity_info then type_info:store(entity_info) end budget_used = budget_used + 1 end end -- ############################################## local function notify_ntopng_status(started) local info = ntop.getInfo() local severity = alert_consts.alertSeverity("info") local msg local msg_details = string.format("%s v.%s (%s) [OS: %s][pid: %s][options: %s]", info.product, info.version, info.revision, info.OS, info.pid, info.command_line) local anomalous = false local event if(started) then -- reading current version and last version to check if it has been updated local last_version_key = "ntopng.updates.last_version" local last_version = ntop.getCache(last_version_key) local curr_version = info["version"].."-"..info["revision"] ntop.setCache(last_version_key, curr_version) -- 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 = alert_consts.alertSeverity("error") anomalous = true event = "anomalous_termination" elseif not isEmptyString(last_version) and last_version ~= curr_version then -- software update msg = string.format("%s %s", i18n("alert_messages.ntopng_update"), msg_details) event = "update" 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 = ntop.getInfo().product obj = { entity_id = alerts_api.systemEntity(entity_value), entity_val = entity_value, type = alert_consts.alertType("alert_process_notification"), severity = severity, message = msg, when = os.time() } if anomalous then telemetry_utils.notify(obj) end local entity_info = alerts_api.systemEntity(entity_value) local type_info = alert_consts.alert_types.alert_process_notification.new( event, msg_details ) type_info:set_severity(alert_severities[alert_consts.alertSeverityRaw(severity)]) return(type_info:store(entity_info)) end function alert_utils.notify_ntopng_start() return(notify_ntopng_status(true)) end function alert_utils.notify_ntopng_stop() return(notify_ntopng_status(false)) end return alert_utils