ntopng/scripts/lua/modules/alert_utils.lua
2024-05-17 11:49:30 +02:00

919 lines
32 KiB
Lua

--
-- (C) 2014-24 - 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
local clock_start = os.clock()
-- This file contains the description of all functions
-- used to trigger host alerts
local verbose = ntop.getCache("ntopng.prefs.alerts.debug") == "1"
local json = require("dkjson")
local recovery_utils = require "recovery_utils"
local alert_entities = require "alert_entities"
local alert_consts = require "alert_consts"
local format_utils = require "format_utils"
local alerts_api = require "alerts_api"
local icmp_utils = require "icmp_utils"
local flow_risk_utils = require "flow_risk_utils"
require "check_redis_prefs"
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 = {}
-- ##############################################
-- @brief Return a callback for formatting the alert
-- @param alert_key The alert type
-- @param entity_id The alert entity
-- @return a format callback as defined in alert_definitions whose parameters are:
-- - ifid
-- - alert
-- - alert_json
-- - local_explorer: true if called by the local alert explorer, false if exported to an endpoint
local function alertTypeDescription(alert_key, entity_id)
local alert_id = alert_consts.getAlertType(alert_key, entity_id)
if alert_id and alert_consts.alert_types[alert_id].format then
return alert_consts.alert_types[alert_id].format
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
-- #################################
-- @brief Deletes all stored alerts matching an host and an IP
-- @return nil
function alert_utils.deleteFlowAlertsMatchingHost(host_ip, vlan_id, alert_id)
local flow_alert_store = require("flow_alert_store").new()
if not isEmptyString(host_ip) then
flow_alert_store:add_ip_filter(hostkey2hostinfo(host_ip)["host"])
end
if (vlan_id) and (tonumber(vlan_id) ~= 0) then
flow_alert_store:add_vlan_filter(vlan_id)
end
flow_alert_store:add_alert_id_filter(alert_id)
-- Perform the actual deletion
flow_alert_store:delete()
end
-- #################################
-- @brief Deletes all stored alerts matching a domain
-- @return nil
function alert_utils.deleteFlowAlertsMatchingDomain(domain_name, alert_id)
local flow_alert_store = require("flow_alert_store").new()
if not isEmptyString(domain_name) then
flow_alert_store:add_domain_filter(domain_name)
end
if alert_id then
flow_alert_store:add_alert_id_filter(alert_id)
end
-- Perform the actual deletion
flow_alert_store:delete()
end
-- #################################
-- @brief Deletes all stored alerts matching a certificate
-- @return nil
function alert_utils.deleteFlowAlertsMatchingCertificate(certificate, alert_id)
-- TODO
end
-- #################################
-- @brief Deletes all stored alerts matching an host and an IP
-- @return nil
function alert_utils.deleteHostAlertsMatchingHost(host_ip, vlan_id, alert_id)
local host_alert_store = require("host_alert_store").new()
if not isEmptyString(host_ip) then
host_alert_store:add_ip_filter(hostkey2hostinfo(host_ip)["host"])
end
if (vlan_id) and (tonumber(vlan_id) ~= 0) then
host_alert_store:add_vlan_filter(vlan_id)
end
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
end
if not isEmptyString(what) then
opts.status = what
end
opts.ifid = interface.getId()
return opts
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
function alert_utils.formatRawFlow(alert, nohtml)
require "flow_utils"
local time_bounds
local add_links = false
-- pretend alert is a flow to reuse getFlowLabel
local flow = {
["cli.ip"] = alert["cli_ip"],
["cli.port"] = tonumber(alert["cli_port"]),
["cli.blacklisted"] = tostring(alert["cli_blacklisted"]) == "1",
["cli.localhost"] = tostring(alert["cli_localhost"]) == "1",
["cli.host"] = alert["cli_name"],
["srv.ip"] = alert["srv_ip"],
["srv.port"] = tonumber(alert["srv_port"]),
["srv.blacklisted"] = tostring(alert["srv_blacklisted"]) == "1",
["srv.localhost"] = tostring(alert["srv_localhost"]) == "1",
["srv.host"] = alert["srv_name"],
["vlan"] = alert["vlan_id"]
}
flow = "<i class=\"fas fa-stream\"></i> " .. (getFlowLabel(flow, false, add_links, time_bounds, {
page = "alerts"
}, nil, true, nohtml) or "")
return flow
end
-- #################################
function alert_utils.disableAlertsGeneration()
if not isAdministratorOrPrintErr() 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
-- #################################
local function alertNotificationActionToLabel(action, use_emoji)
local label = ""
if action == "engage" then
label = "["
if (use_emoji) then
label = label .. "\xE2\x9D\x97 "
end
label = label .. "Engaged"
label = label .. "]"
elseif action == "release" then
label = "["
if (use_emoji) then
label = label .. "\xE2\x9C\x94 "
end
label = label .. "Released"
label = label .. "]"
end
return label
end
-- #################################
function alert_utils.getConfigsetURL(script_key, subdir)
return string.format('%s/lua/admin/edit_configset.lua?subdir=%s&check=%s#all', ntop.getHttpPrefix(), subdir,
script_key)
end
-- #################################
function alert_utils.getConfigsetAlertLink(alert_json, alert --[[ optional --]] , alert_entity)
if isAdministrator() then
local info = alert_json.alert_generation or (alert_json.alert_info and alert_json.alert_info.alert_generation)
if alert_entity and alert_entity == alert_entities.am_host.entity_id then
local host = alert_json.host.host or alert_json.host
if host then
local measurement = alert_json.host.measurement or alert_json.measurement
if measurement then
return
' <a href="' .. ntop.getHttpPrefix() .. '/lua/monitor/active_monitoring_monitor.lua?am_host=' ..
host .. '&measurement=' .. measurement .. '&page=overview"><i class="fas fa-cog" title="' ..
i18n("edit_configuration") .. '"></i></a>'
else
return
' <a href="' .. ntop.getHttpPrefix() .. '/lua/monitor/active_monitoring_monitor.lua?am_host=' ..
host .. '&page=overview"><i class="fas fa-cog" title="' .. i18n("edit_configuration") ..
'"></i></a>'
end
else
return ' <a href="' .. ntop.getHttpPrefix() ..
'/lua/monitor/active_monitoring_monitor.lua?page=overview"><i class="fas fa-cog" title="' ..
i18n("edit_configuration") .. '"></i></a>'
end
elseif info then
return (' <a href="' .. alert_utils.getConfigsetURL(info.script_key, info.subdir) .. '">' ..
'<i class="fas fa-cog" title="' .. i18n("edit_configuration") .. '"></i></a>')
else
return (' <a href="' .. ntop.getHttpPrefix() .. '/lua/admin/edit_configset.lua?subdir=interface#all">' ..
'<i class="fas fa-cog" title="' .. i18n("edit_configuration") .. '"></i></a>')
end
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, local_explorer)
local msg
if (alert_json == nil) then
alert_json = alert_utils.getAlertInfo(alert)
end
local description = alertTypeDescription(alert.alert_id, alert.entity_id)
msg = alert_json
if (type(description) == "string") then
-- localization string
msg = i18n(description, msg)
elseif (type(description) == "function") then
msg = description(ifid, alert, alert_json, local_explorer)
end
if (type(msg) == "table") then
return ("")
end
if isEmptyString(msg) then
msg = alert_consts.alertTypeLabel(tonumber(alert.alert_id), true --[[ no_html --]] , alert.entity_id)
end
if not isEmptyString(alert["user_label"]) then
msg = string.format('%s <small><span class="text-muted">%s</span></small>', msg, alert["user_label"])
end
return (msg or "")
end
-- #################################
-- Return a risk info (raw text, do not return a formatted value)
function alert_utils.get_flow_risk_info(alert_risk, alert_json)
if (alert_json) and (alert_json.alert_generation) and (alert_json.alert_generation.flow_risk_info) then
local flow_risk_info = json.decode(alert_json.alert_generation.flow_risk_info)
if (flow_risk_info) and (flow_risk_info[tostring(alert_risk)]) then
return flow_risk_info[tostring(alert_risk)]
end
end
return ''
end
-- #################################
function alert_utils.format_score(msg, alert_score)
if (alert_score) and (tonumber(alert_score)) and (tonumber(alert_score) > 0) then
msg = string.format("%s [Score: %d] ", msg, tonumber(alert_score))
end
return msg
end
-- #################################
function alert_utils.formatFlowAlertMessage(ifid, alert, alert_json, add_score, local_explorer)
local msg
local alert_risk
if tonumber(alert.alert_id) then
alert_risk = ntop.getFlowAlertRisk(tonumber(alert.alert_id))
end
if not alert_json then
alert_json = alert_utils.getAlertInfo(alert)
end
local description = alertTypeDescription(alert.alert_id, alert_entities.flow.entity_id)
if (type(description) == "string") then
-- localization string
msg = i18n(description, alert_json)
elseif (type(description) == "function") then
msg = description(ifid, alert, alert_json, local_explorer)
end
if isEmptyString(msg) then
if alert_json and alert_json.alert_generation and alert_risk and alert_risk > 0 then
-- Flow risks most of the times already have a default description, use this in case of emtpy descr
msg = alert_utils.get_flow_risk_info(alert_risk, alert_json)
else
-- Normal alerts
msg = alert_consts.alertTypeLabel(tonumber(alert.alert_id), true --[[ no_html --]] , alert.entity_id)
end
end
if not isEmptyString(alert["user_label"]) then
msg = string.format('%s <small><span class="text-muted">%s</span></small>', msg, alert["user_label"])
end
if add_score then
if tonumber(alert.alert_id) then
local alert_score = ntop.getFlowAlertScore(tonumber(alert.alert_id))
msg = alert_utils.format_score(msg, alert_score)
end
end
-- Add the link to the documentation
if alert_risk and alert_risk > 0 then
msg = string.format("%s %s %s",
msg, flow_risk_utils.get_documentation_link(alert_risk),
flow_risk_utils.get_remediation_documentation_link(alert.alert_id))
local info_msg = alert_utils.get_flow_risk_info(alert_risk, alert_json)
-- Add check info_msg ~= alert.info to avoid duplicated in description msg
--[[if (not isEmptyString(info_msg) and info_msg ~= alert.info) then
msg = string.format("%s", msg, info_msg)
end--]]
end
return msg or ""
end
-- #################################
function alert_utils.getLinkToPastFlows(ifid, alert, alert_json)
if not ntop.isEnterpriseM() or not hasClickHouseSupport() then
-- nIndex not enabled or enabled but not available for this particular interface
return
end
local epoch_begin = alert["tstamp"]
local epoch_end = alert["tstamp_end"]
-- Fetch the alert id
local alert_id = alert_consts.getAlertType(alert.alert_id, alert.entity_id)
if alert_id and epoch_begin and epoch_end then
local final_filter = {}
local filters = {}
-- Look a bit around the epochs
epoch_begin = epoch_begin - (5 * 60)
epoch_end = epoch_end + (5 * 60)
-- IP
if not isEmptyString(alert["ip"]) then
filters[#filters + 1] = {
name = "ip",
op = "eq",
val = alert["ip"]
}
-- Add the hostname here cause it's needed to check if the ip is equal to the name
-- Hostname
if not isEmptyString(alert["name"]) and (alert["ip"] ~= alert["name"]) then
filters[#filters + 1] = {
name = "name",
op = "eq",
val = alert["name"]
}
end
end
-- VLAN ID
if not isEmptyString(alert["vlan_id"]) and tonumber(alert["vlan_id"]) > 0 then
filters[#filters + 1] = {
name = "vlan_id",
op = "eq",
val = alert["vlan_id"]
}
end
-- Host alerts could have a custom function to format the url, in case call it
-- and then merge the filters
if alert_consts.alert_types[alert_id].filter_to_past_flows then
local past_flows_filter = alert_consts.alert_types[alert_id].filter_to_past_flows(ifid, alert, alert_json)
table.merge(filters, past_flows_filter)
end
for _, tag in pairs(filters) do
final_filter[tag.name] = string.format("%s%s%s", tag.val, alert_consts.SEPARATOR, tag.op)
end
-- Return the link augmented with the filter
local res = string.format("%s/lua/pro/db_search.lua?epoch_begin=%u&epoch_end=%u&%s", ntop.getHttpPrefix(),
epoch_begin, epoch_end, table.tconcat(final_filter, "=", "&"))
return res
end
return nil
end
-- #################################
function alert_utils.notification_timestamp_rev(a, b)
return (a.tstamp > b.tstamp)
end
function alert_utils.severity_rev(a, b)
return (a.severity_id > b.severity_id)
end
-- #################################
--
-- Returns a summary of the alert as readable text
function alert_utils.formatAlertNotification(notif, options)
-- Otherwise format the alert
local defaults = {
show_severity = true,
nohtml = false,
nolabelhtml = false,
emoji = false
}
options = table.merge(defaults, options)
local ifname
local severity
local when
if (notif.ifid ~= -1) then
ifname = string.format(" [Interface: %s]", getInterfaceName(notif.ifid))
else
ifname = ""
end
if (options.show_severity == false) then
severity = ""
else
severity = " [Severity: " ..
alert_consts.alertSeverityLabel(notif.score, options.nohtml or options.nolabelhtml, options.emoji) ..
"]"
end
if (options.nodate == true) then
when = ""
else
if options.timezone then
when = format_utils.formatEpochISO8601(notif.tstamp_end or notif.tstamp or 0)
else
when = format_utils.formatEpoch(notif.tstamp_end or notif.tstamp or 0)
end
if (not options.no_bracket_around_date) then
when = " [" .. when .. "]"
end
when = when .. " "
end
local msg = string.format("%s%s%s", when, ifname, severity)
local entity_label = ""
if notif.entity_id then
entity_label = alert_consts.alertEntityLabel(notif.entity_id) or ""
end
-- entity can be hidden for example when one is OK with just the message
if options.show_entity and not isEmptyString(entity_label) then
msg = msg .. " [" .. entity_label .. "]"
end
local alert_type_label = alert_consts.alertTypeLabel(notif.alert_id, options.nohtml or options.nolabelhtml,
notif.entity_id, true)
if alert_type_label then
msg = msg .. " [" .. alert_type_label .. "]"
end
-- entity can be hidden for example when one is OK with just the message
if options.show_entity and notif.entity_id and notif.entity_val then
local ev = notif.entity_val
if notif.entity_id == alert_entities.flow.entity_id then
ev = noHtml(alert_utils.formatRawFlow(notif, options.nohtml))
elseif notif.entity_id == alert_entities.host.entity_id then
-- suppress @0 when the vlan is zero
ev = hostinfo2hostkey(hostkey2hostinfo(notif.entity_val))
elseif notif.entity_id == alert_entities.am_host.entity_id then
-- show host only, hiding measurement id (e.g. vs@)
local parts = split(notif.entity_val, "@")
if #parts == 2 then
ev = parts[2]
end
end
msg = msg .. " [" .. (ev or '') .. "]"
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
local alert_title = ""
if not isEmptyString(entity_label) then
alert_title = string.format("[%s]: ", entity_label)
end
alert_title = alert_title .. alert_consts.alertTypeLabel(notif.alert_id, options.nohtml, notif.entity_id)
return msg, alert_title, entity_label
end
-- ##############################################
function alert_utils.formatAlertCulprit(notif)
local msg
-- Formatting cli-srv
if not isEmptyString(notif.cli_ip) and not isEmptyString(notif.srv_ip) then
local client = notif.cli_ip
local server = notif.srv_ip
local client_port = ''
local server_port = ''
if not isEmptyString(notif.cli_name) then
client = notif.cli_name
end
if not isEmptyString(notif.srv_name) then
server = notif.srv_name
end
if notif.cli_port ~= 0 then
client_port = ':' .. notif.cli_port
end
if notif.srv_port ~= 0 then
server_port = ':' .. notif.srv_port
end
msg = string.format('%s%s \xE2\x9E\xA1 %s%s', client, client_port, server, server_port)
elseif not isEmptyString(notif.entity_val) then
msg = notif.entity_val
if notif.name and not isEmptyString(notif.name) then
msg = notif.name
end
else
msg = ''
end
return msg
end
-- ##############################################
local function notify_ntopng_status(started)
local info = ntop.getInfo()
local score = 10
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)
score = 100
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
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_score(score)
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
-- ##############################################
local function addDayEpoch(res, day_epoch)
res[day_epoch] =
{0, --[[ Counter for alerts between 00:00 and 00:59 UTC --]] 0, --[[ Counter for alerts between 01:00 and 01:59 UTC --]]
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 --[[ Counter for alerts in other hours, until 23:00 to 23:59 --]] }
return res
end
-- ##############################################
-- @brief Function used to format the old timeseries format (CheckMK integration)
function alert_utils.formatOldTimeseries(q_res, _epoch_begin, _epoch_end)
local hour_secs = 60 * 60
local day_secs = 60 * 60 * 24
local epoch_begin = _epoch_begin - (_epoch_begin % day_secs) -- Round begin to start of day
local epoch_end = _epoch_end - (_epoch_end % day_secs) + day_secs -- Round end to end of day
local res = {}
for _, v in ipairs(q_res) do
local tstamp = v.hour or v.tstamp
-- Midnight UTC of the day containing v.hour
local day_epoch = tstamp - (tstamp % day_secs)
-- Hour of the day containing v.hour, from 0 to 23, inclusive
-- NOTE: Use the `floor` to make sure hour is an integer as it will be used to index a Lua array
local hour = math.floor((tstamp - day_epoch) / hour_secs)
-- Here we add 1 to the hour as Lua array are indexed starting from 1, whereas `hour` is an integer starting from zero
if not res[day_epoch] then
res = addDayEpoch(res, day_epoch)
end
-- This is done for both, enganged and historical alert, historical have v.hour and v.count, instead engaged
-- There is one entry per engaged, reporting the starting time (tstamp), so just add +1 to the hour having that alert
res[day_epoch][hour + 1] = tonumber( --[[ Historical ]]
v.count or --[[ Engaged ]] ((res[day_epoch][hour + 1] or 0) + 1))
end -- for
return res
end
-- ##############################################
function alert_utils.format_other_alerts(alert_bitmap, predominant_alert, alert_json, add_score, no_html, json_format)
-- Unpack all flow alerts, iterating the alerts_map. The alerts_map is stored as an HEX.
local other_alerts_by_score = {} -- Table used to keep messages ordered by score
local additional_alerts = {}
local nibble_num = 0 -- Current nibble being processed
for alerts_map_nibble_id = #alert_bitmap, 1, -1 do
-- Extract the nibble
local alerts_map_hex_nibble = alert_bitmap:sub(alerts_map_nibble_id, alerts_map_nibble_id)
-- Convert the HEX nibble into a decimal value
local alerts_map_nibble = tonumber(alerts_map_hex_nibble, 16)
if alerts_map_nibble > 0 then
for bit_num = 0, 7 do
-- Checks the bits set in this current nibble
local has_bit = alerts_map_nibble & (1 << bit_num) == (1 << bit_num)
if has_bit then -- The bit is set
-- The actual alert id is the bit number times the current byte multiplied by 8
local alert_id = math.floor(8 * nibble_num / 2) + bit_num
if alert_id ~= tonumber(predominant_alert) then -- Do not add the predominant alert to the list of additional alerts
local message = alert_consts.alertTypeLabel(alert_id, true, alert_entities.flow.entity_id)
local alert_score = ntop.getFlowAlertScore(alert_id)
if add_score then
message = alert_utils.format_score(message, alert_score)
end
local alert_risk = ntop.getFlowAlertRisk(alert_id)
if alert_risk > 0 then
if not no_html then
message = string.format("%s %s", message,
flow_risk_utils.get_documentation_link(alert_risk))
end
local info_msg = alert_utils.get_flow_risk_info(alert_risk, alert_json)
if not isEmptyString(info_msg) then
message = string.format("%s [%s]", message, info_msg)
end
end
if not other_alerts_by_score[alert_score] then
other_alerts_by_score[alert_score] = {}
end
other_alerts_by_score[alert_score][#other_alerts_by_score[alert_score] + 1] = message
if json_format then
additional_alerts[#additional_alerts + 1] = {
msg = message,
score = alert_score,
alert_id = alert_id
}
else
additional_alerts[#additional_alerts + 1] = message
end
end
end
end
end
-- Increment the nibble
nibble_num = nibble_num + 1
end
return other_alerts_by_score, additional_alerts
end
-- ##############################################
function alert_utils.filter_notification(notification, recipient_id)
local alert_info = json.decode(notification.alert)
local alert_key = alert_info.alert_id
local entity_id = alert_info.entity_id
local entity_val = alert_info.entity_val
if notification.score == 0 then
return true
end
local alert_id = alert_consts.getAlertType(alert_key, entity_id)
return alert_utils.check_alert_policy(entity_id, entity_val, alert_id, alert_info, recipient_id)
end
-- ##############################################
function alert_utils.check_alert_policy(entity_id, entity_val, alert_id, alert_info, recipient_id)
local alert_key = ""
local alert_key_fields = {}
local not_set = true
local silence_alerts = ntop.getCache("ntopng.prefs.silence_multiple_alerts." .. recipient_id) or "1"
-- In case the alerts have to be silenced for the endpoint then silence them, otherwise skip
if silence_alerts == "1" then
if alert_consts.alert_types[alert_id].alert_retention_policy_key then
alert_key_fields = alert_consts.alert_types[alert_id].alert_retention_policy_key(alert_info)
for _, field in ipairs(alert_key_fields) do
alert_key = alert_key .. "." .. alert_info[field]
end
else
if not alert_entities[entity_val] then
entity_val = alert_consts.alertEntityRaw(entity_id)
end
alert_key_fields = alert_entities[entity_val].alert_key_fields
if (alert_key_fields) then
for _, field in ipairs(alert_key_fields) do
alert_key = alert_key .. "." .. alert_info[field]
end
else
return not_set
end
end
if isEmptyString(alert_key) then
return not_set
end
local redis_key = string.format("ntopng.cache.alert.retention.%s.%s.%s%s", recipient_id, entity_id, alert_id,
alert_key)
not_set = isEmptyString(ntop.getCache(redis_key))
if not_set then
-- Set key with expiration on redis to filter out the same alert for some time
-- TODO: 3600 must be update with a user preference
ntop.setCache(redis_key, "1", 3600)
end
end
return not_set -- true to send the alert, false to suppress this alert until the key expires
end
-- ##############################################
if (trace_script_duration ~= nil) then
io.write(debug.getinfo(1, 'S').source .. " executed in " .. (os.clock() - clock_start) * 1000 .. " ms\n")
end
return alert_utils