mirror of
https://github.com/ntop/ntopng.git
synced 2026-04-30 07:59:35 +00:00
2352 lines
78 KiB
Lua
2352 lines
78 KiB
Lua
--
|
|
-- (C) 2019-21 - ntop.org
|
|
--
|
|
|
|
-- User scripts provide a scriptable way to interact with the ntopng
|
|
-- core. Users can provide their own modules to trigger custom alerts,
|
|
-- export data, or perform periodic tasks.
|
|
|
|
local dirs = ntop.getDirs()
|
|
package.path = dirs.installdir .. "/scripts/lua/modules/pools/?.lua;" .. package.path
|
|
|
|
require "lua_utils"
|
|
|
|
local os_utils = require("os_utils")
|
|
local json = require("dkjson")
|
|
local plugins_utils = require("plugins_utils")
|
|
local alert_consts = require "alert_consts"
|
|
local http_lint = require("http_lint")
|
|
local ipv4_utils = require "ipv4_utils"
|
|
local pools_lua_utils = require "pools_lua_utils"
|
|
|
|
local info = ntop.getInfo()
|
|
|
|
local user_scripts = {}
|
|
|
|
-- ##############################################
|
|
|
|
local filters_debug = false
|
|
|
|
-- ##############################################
|
|
|
|
user_scripts.field_units = {
|
|
seconds = "field_units.seconds",
|
|
bytes = "field_units.bytes",
|
|
flows = "field_units.flows",
|
|
packets = "field_units.packets",
|
|
mbits = "field_units.mbits",
|
|
hosts = "field_units.hosts",
|
|
syn_sec = "field_units.syn_sec",
|
|
flow_sec = "field_units.flow_sec",
|
|
percentage = "field_units.percentage",
|
|
syn_min = "field_units.syn_min",
|
|
contacts = "field_units.contacts",
|
|
}
|
|
|
|
-- ##############################################
|
|
|
|
-- Operator functions associated to user scripts `operator`, which is specified
|
|
-- both inside user scripts default configuration values, as well as when user scripts
|
|
-- are configured from the UI.
|
|
--
|
|
user_scripts.operator_functions = {
|
|
gt --[[ greater than --]] = function(value, threshold) return value > threshold end,
|
|
lt --[[ less than --]] = function(value, threshold) return value < threshold end,
|
|
}
|
|
|
|
-- ##############################################
|
|
|
|
local NUM_FILTERED_KEY = "ntopng.cache.user_scripts.exclusion_counter.subdir_%s.script_key_%s"
|
|
local REQUEST_PERIODIC_USER_SCRIPTS_RUN_KEY = "ntopng.cache.ifid_%i.user_scripts.request.granularity_%s"
|
|
local NON_TRAFFIC_ELEMENT_CONF_KEY = "all"
|
|
local NON_TRAFFIC_ELEMENT_ENTITY = "no_entity"
|
|
local ALL_HOOKS_CONFIG_KEY = "all"
|
|
local CONFIGSETS_KEY = "ntopng.prefs.user_scripts.configsets_v2"
|
|
user_scripts.DEFAULT_CONFIGSET_ID = 0
|
|
|
|
-- NOTE: the subdir id must be unique
|
|
local available_subdirs = {
|
|
{
|
|
id = "host",
|
|
label = "hosts",
|
|
pools = "host_pools",
|
|
filter = {
|
|
default_fields = { "alert_entity_val" },
|
|
available_fields = {
|
|
alert_entity_val = {
|
|
lint = http_lint.validateNetworkWithVLAN, -- .e.g., 192.168.2.1@3, 192.168.2.0/24@0
|
|
match = function(context, val)
|
|
-- TODO: Add CIDR
|
|
-- Do the comparison
|
|
if not context or context.alert_entity ~= alert_consts.alertEntity("host") then
|
|
return false
|
|
end
|
|
|
|
return table.compare(hostkey2hostinfo(val), hostkey2hostinfo(context.alert_entity_val))
|
|
end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("(alert_entity = %u AND alert_entity_val = '%s')", alert_consts.alertEntity("host"), val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
return (alert[filter] and (alert[filter] == val))
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
id = "interface",
|
|
label = "interfaces",
|
|
pools = "interface_pools",
|
|
filter = {
|
|
default_fields = { "alert_entity_val" },
|
|
available_fields = {
|
|
alert_entity_val = {
|
|
lint = http_lint.validateInterface, -- An interface id
|
|
match = function(context, val)
|
|
-- Do the comparison
|
|
if not context or context.alert_entity ~= alert_consts.alertEntity("interface") then
|
|
return false
|
|
end
|
|
|
|
-- Match on the interface id
|
|
return tonumber(val) == tonumber(context.alert_entity_val)
|
|
end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("(alert_entity = %u AND alert_entity_val = '%s')", alert_consts.alertEntity("interface"), val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
return (alert[filter] and (alert[filter] == val))
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
id = "network",
|
|
label = "networks",
|
|
pools = "local_network_pools",
|
|
filter = {
|
|
default_fields = { "alert_entity_val" },
|
|
available_fields = {
|
|
alert_entity_val = {
|
|
lint = http_lint.validateNetworkWithVLAN, -- A local network
|
|
match = function(context, val)
|
|
-- Do the comparison
|
|
if not context or context.alert_entity ~= alert_consts.alertEntity("network") then
|
|
return false
|
|
end
|
|
|
|
-- Match on the interface id
|
|
return val == context.alert_entity_val
|
|
end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("(alert_entity = %u AND alert_entity_val = '%s')", alert_consts.alertEntity("network"), val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
return (alert[filter] and (alert[filter] == val))
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
id = "snmp_device",
|
|
label = "host_details.snmp",
|
|
pools = "snmp_device_pools",
|
|
filter = {
|
|
default_fields = { "alert_entity_val" },
|
|
available_fields = {
|
|
alert_entity_val = {
|
|
lint = http_lint.validateHost, -- The IP address of an SNMP device
|
|
match = function(context, val)
|
|
-- Do the comparison
|
|
if not context or context.alert_entity ~= alert_consts.alertEntity("snmp_device") then
|
|
return false
|
|
end
|
|
|
|
-- Match the SNMP device
|
|
return val == context.alert_entity_val
|
|
end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("(alert_entity = %u AND alert_entity_val = '%s')", alert_consts.alertEntity("snmp_device"), val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
return (alert[filter] and (alert[filter] == val))
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
id = "flow",
|
|
label = "flows",
|
|
-- User script execution filters (field names are those that arrive from the C Flow.cpp)
|
|
filter = {
|
|
-- Default fields populated automatically when creating filters
|
|
default_fields = {"srv_addr", "srv_port", "l7_proto", "proto" },
|
|
-- All possible filter fields
|
|
available_fields = {
|
|
cli_addr = {
|
|
lint = http_lint.validateNetwork,
|
|
match = function(context, val)
|
|
local client_ip = flow.getClientIp()
|
|
-- Attempt exact match
|
|
if client_ip == val then return true end
|
|
-- Attempt IPv4 network match
|
|
local network, netmask = ipv4_utils.cidr_2_addr(val)
|
|
if network and netmask then return ipv4_utils.includes(network, netmask, client_ip) end
|
|
-- No match
|
|
return false
|
|
end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("cli_addr = '%s'", val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
return (alert[filter] and (alert[filter] == val))
|
|
end,
|
|
},
|
|
cli_port = {
|
|
lint = http_lint.validatePort,
|
|
match = function(context, val) return flow.getClientPort() == tonumber(val) end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("cli_port = %u", val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
return (alert[filter] and (tonumber(alert[filter]) == tonumber(val)))
|
|
end,
|
|
},
|
|
srv_addr = {
|
|
lint = http_lint.validateNetwork,
|
|
match = function(context, val)
|
|
local server_ip = flow.getServerIp()
|
|
-- Attempt exact match
|
|
if server_ip == val then return true end
|
|
-- Attempt IPv4 network match
|
|
local network, netmask = ipv4_utils.cidr_2_addr(val)
|
|
if network and netmask then return ipv4_utils.includes(network, netmask, server_ip) end
|
|
-- No match
|
|
return false
|
|
end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("srv_addr = '%s'", val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
return (alert[filter] and (alert[filter] == val))
|
|
end,
|
|
},
|
|
srv_port = {
|
|
lint = http_lint.validatePort,
|
|
match = function(context, val) return flow.getServerPort() == tonumber(val) end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("srv_port = %u", val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
return (alert[filter] and (tonumber(alert[filter]) == tonumber(val)))
|
|
end,
|
|
},
|
|
l7_proto = {
|
|
lint = http_lint.validateProtocolIdOrName,
|
|
match = function(context, val)
|
|
-- If val is the application name, then it is converted to application id
|
|
if not tonumber(val) then val = interface.getnDPIProtoId(val) end
|
|
-- For integers represented as strings
|
|
val = tonumber(val)
|
|
-- Check for equality on either the master or application ids
|
|
return flow.getnDPIMasterProtoId() == val or flow.getnDPIAppProtoId() == val
|
|
end,
|
|
sqlite = function(val)
|
|
-- If val is the application name, then it is converted to application id
|
|
if not tonumber(val) then val = interface.getnDPIProtoId(val) end
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
-- Match both on the master and app proto
|
|
return string.format("(l7_proto = %u OR l7_master_proto = %u)", val, val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
-- Converting value into it's id if value is under string format
|
|
local value = tonumber(val)
|
|
if not value then value = interface.getnDPIProtoId(val) end
|
|
|
|
return (alert[filter] and (tonumber(alert[filter]) == value))
|
|
end,
|
|
},
|
|
proto = {
|
|
lint = http_lint.validateProtocolIdOrName,
|
|
match = function(context, val)
|
|
-- If val is the protocol name, then it is converted to L4 protocol id
|
|
if not tonumber(val) then val = l4_proto_to_id(val) end
|
|
-- Check for equality on either the master or application protocol
|
|
return flow.getProtocol() == tonumber(val)
|
|
end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("proto = %u", val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
-- Converting value into it's id if value is under string format
|
|
local value = tonumber(val)
|
|
if not value then value = l4_proto_to_id(val) end
|
|
|
|
return (alert[filter] and (tonumber(alert[filter]) == value))
|
|
end,
|
|
},
|
|
flow_risk_bitmap = {
|
|
lint = http_lint.validateNumber,
|
|
match = function(context, val)
|
|
-- Convert the string-bitmap to a number
|
|
val = tonumber(val)
|
|
-- Check if there's at least one risk in common between val
|
|
-- and the actual flow bitmap of risks
|
|
return (val & flow.getRiskBitmap()) ~= 0
|
|
end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
return string.format("flow_risk_bitmap = %u", val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
return (alert[filter] and (tonumber(alert[filter]) == tonumber(val)))
|
|
end,
|
|
},
|
|
info = {
|
|
lint = http_lint.validateSingleWord,
|
|
match = function(context, val)
|
|
-- Search for substring val inside the flow info field
|
|
return not not flow.getFlowInfoField():find(val)
|
|
end,
|
|
sqlite = function(val)
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
-- As the info is stored inside the JSON alert, it is necessary to
|
|
-- use sqlite json_extract to access it
|
|
return string.format("json_extract(alert_json, '$.info') like '%%%s%%'", val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
-- Search for substring val inside the flow info field
|
|
if alert_json and val then
|
|
return (alert_json[filter] and alert_json[filter]:find(val))
|
|
end
|
|
return false
|
|
end,
|
|
},
|
|
l7_cat = {
|
|
lint = http_lint.validateCategory,
|
|
match = function(context, val)
|
|
-- If val is the application name, then it is converted to application id
|
|
if not tonumber(val) then val = interface.getnDPICategoryId(val) end
|
|
-- For integers represented as strings
|
|
val = tonumber(val)
|
|
-- Check for equality on either the master or application ids
|
|
return flow.getnDPICategoryId() == val
|
|
end,
|
|
sqlite = function(val)
|
|
-- If val is the application name, then it is converted to application id
|
|
if not tonumber(val) then val = interface.getnDPICategoryId(val) end
|
|
-- Keep in sync with SQLite database schema declared in AlertsManager.cpp
|
|
-- Match both on the master and app proto
|
|
return string.format("l7_cat = %u", val)
|
|
end,
|
|
find = function(alert, alert_json, filter, val)
|
|
-- If val is the application name, then it is converted to application id
|
|
local value = tonumber(val)
|
|
if not value then value = interface.getnDPICategoryId(val) end
|
|
|
|
return (alert[filter] and (tonumber(alert[filter]) == value))
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
-- No pools for flows
|
|
}, {
|
|
id = "system",
|
|
label = "system",
|
|
}, {
|
|
id = "syslog",
|
|
label = "Syslog",
|
|
}
|
|
}
|
|
|
|
-- User scripts category consts
|
|
-- IMPORTANT keep it in sync with ntop_typedefs.h enum ScriptCategory
|
|
user_scripts.script_categories = {
|
|
other = {
|
|
id = 0,
|
|
icon = "fas fa-scroll",
|
|
i18n_title = "user_scripts.category_other",
|
|
i18n_descr = "user_scripts.category_other_descr",
|
|
},
|
|
security = {
|
|
id = 1,
|
|
icon = "fas fa-shield-alt",
|
|
i18n_title = "user_scripts.category_security",
|
|
i18n_descr = "user_scripts.category_security_descr",
|
|
},
|
|
internals = {
|
|
id = 2,
|
|
icon = "fas fa-wrench",
|
|
i18n_title = "user_scripts.category_internals",
|
|
i18n_descr = "user_scripts.category_internals_descr",
|
|
},
|
|
network = {
|
|
id = 3,
|
|
icon = "fas fa-network-wired",
|
|
i18n_title = "user_scripts.category_network",
|
|
i18n_descr = "user_scripts.category_network_descr",
|
|
},
|
|
system = {
|
|
id = 4,
|
|
icon = "fas fa-server",
|
|
i18n_title = "user_scripts.category_system",
|
|
i18n_descr = "user_scripts.category_system_descr",
|
|
}
|
|
}
|
|
|
|
-- Hook points for flow/periodic modules
|
|
-- NOTE: keep in sync with the Documentation
|
|
user_scripts.script_types = {
|
|
flow = {
|
|
parent_dir = "interface",
|
|
hooks = {"protocolDetected", "statusChanged", "flowEnd", "periodicUpdate"},
|
|
subdirs = {"flow"},
|
|
default_config_only = true, -- Only the default configset can be used
|
|
}, traffic_element = {
|
|
parent_dir = "interface",
|
|
hooks = {"min", "5mins", "hour", "day"},
|
|
subdirs = {"interface", "network"},
|
|
has_per_hook_config = true, -- Each hook has a separate configuration
|
|
}, host = {
|
|
parent_dir = "interface",
|
|
hooks = {"min", "5mins", "hour", "day"},
|
|
subdirs = {"host"},
|
|
has_per_hook_config = true, -- Each hook has a separate configuration
|
|
}, snmp_device = {
|
|
parent_dir = "system",
|
|
hooks = {"snmpDevice", "snmpDeviceInterface"},
|
|
subdirs = {"snmp_device"},
|
|
}, system = {
|
|
parent_dir = "system",
|
|
hooks = {"min", "5mins", "hour", "day"},
|
|
subdirs = {"system"},
|
|
has_per_hook_config = true, -- Each hook has a separate configuration
|
|
default_config_only = true, -- Only the default configset can be used
|
|
}, syslog = {
|
|
parent_dir = "system",
|
|
hooks = {"handleEvent"},
|
|
subdirs = {"syslog"},
|
|
default_config_only = true, -- Only the default configset can be used
|
|
}
|
|
}
|
|
|
|
-- ##############################################
|
|
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Given a category found in a user script, this method checks whether the category is valid
|
|
-- and, if not valid, it assigns to the plugin a default category
|
|
local function checkCategory(category)
|
|
if not category or not category["id"] then
|
|
return user_scripts.script_categories.other
|
|
end
|
|
|
|
for cat_k, cat_v in pairs(user_scripts.script_categories) do
|
|
if category["id"] == cat_v["id"] then
|
|
return cat_v
|
|
end
|
|
end
|
|
|
|
return user_scripts.script_categories.other
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Given a subdir, returns the corresponding script type
|
|
function user_scripts.getScriptType(search_subdir)
|
|
for _, script_type in pairs(user_scripts.script_types) do
|
|
for _, subdir in pairs(script_type.subdirs) do
|
|
if(subdir == search_subdir) then
|
|
return(script_type)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Not found
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Given a subdir, returns the corresponding numeric id
|
|
local function getSubdirId(subdir_name)
|
|
for id, values in pairs(available_subdirs) do
|
|
if values["id"] == subdir_name then
|
|
return id
|
|
end
|
|
end
|
|
|
|
return -1
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Table to keep per-subdir then per-module then per-hook benchmarks
|
|
--
|
|
-- The structure is the following
|
|
--
|
|
-- table
|
|
-- flow table
|
|
-- flow.mud table
|
|
-- flow.mud.protocolDetected table
|
|
-- flow.mud.protocolDetected.tot_elapsed number 0.00031600000000021
|
|
-- flow.mud.protocolDetected.tot_num_calls number 4
|
|
-- flow.score table
|
|
-- flow.score.protocolDetected table
|
|
-- flow.score.protocolDetected.tot_elapsed number 0.00013700000000005
|
|
-- flow.score.protocolDetected.tot_num_calls number 4
|
|
-- flow.score.statusChanged table
|
|
-- flow.score.statusChanged.tot_elapsed number 0
|
|
-- flow.score.statusChanged.tot_num_calls number 0
|
|
local benchmarks = {}
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getSubdirectoryPath(script_type, subdir, is_pro)
|
|
local prefix = plugins_utils.getRuntimePath() .. "/callbacks"
|
|
local path
|
|
|
|
if not isEmptyString(subdir) and subdir ~= "." then
|
|
path = string.format("%s/%s/%s", prefix, script_type.parent_dir, subdir)
|
|
else
|
|
path = string.format("%s/%s", prefix, script_type.parent_dir)
|
|
end
|
|
|
|
return os_utils.fixPath(path)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Wrap any hook function to compute its execution time which is then added
|
|
-- to the benchmarks table.
|
|
--
|
|
-- @param subdir the modules subdir
|
|
-- @param mod_k the key of the user script
|
|
-- @param hook the name of the hook in the user script
|
|
-- @param hook_fn the hook function in the user script
|
|
--
|
|
-- @return function(...) wrapper ready to be called for the execution of hook_fn
|
|
local function benchmark_hook_fn(subdir, mod_k, hook, hook_fn)
|
|
return function(...)
|
|
local start = ntop.getticks()
|
|
local result = {hook_fn(...)}
|
|
local finish = ntop.getticks()
|
|
local elapsed = finish - start
|
|
|
|
-- Update benchmark results by addin a function call and the elapsed time of this call
|
|
benchmarks[subdir][mod_k][hook]["tot_num_calls"] = benchmarks[subdir][mod_k][hook]["tot_num_calls"] + 1
|
|
benchmarks[subdir][mod_k][hook]["tot_elapsed"] = benchmarks[subdir][mod_k][hook]["tot_elapsed"] + elapsed
|
|
|
|
-- traceError(TRACE_NORMAL,TRACE_CONSOLE, string.format("[%s][elapsed: %.2f][tot_elapsed: %.2f][tot_num_calls: %u]",
|
|
-- hook, elapsed,
|
|
-- benchmarks[subdir][mod_k][hook]["tot_elapsed"],
|
|
-- benchmarks[subdir][mod_k][hook]["tot_num_calls"]))
|
|
|
|
return table.unpack(result)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Initializes benchmark facilities for any hook function
|
|
--
|
|
-- @param subdir the modules subdir
|
|
-- @param mod_k the key of the user script
|
|
-- @param hook the name of the hook in the user script
|
|
-- @param hook_fn the hook function in the user script
|
|
--
|
|
-- @return function(...) wrapper ready to be called for the execution of hook_fn
|
|
local function benchmark_init(subdir, mod_k, hook, hook_fn)
|
|
-- NOTE: 5min/hour/day are not monitored. They would collide in the user_scripts_benchmarks_key.
|
|
if((hook ~= "5min") and (hook ~= "hour") and (hook ~= "day")) then
|
|
-- Prepare the benchmark table fo the hook_fn which is being benchmarked
|
|
if not benchmarks[subdir] then
|
|
benchmarks[subdir] = {}
|
|
end
|
|
|
|
if not benchmarks[subdir][mod_k] then
|
|
benchmarks[subdir][mod_k] = {}
|
|
end
|
|
|
|
if not benchmarks[subdir][mod_k][hook] then
|
|
benchmarks[subdir][mod_k][hook] = {tot_num_calls = 0, tot_elapsed = 0}
|
|
end
|
|
|
|
-- Finally prepare and return the hook_fn wrapped with benchmark facilities
|
|
return benchmark_hook_fn(subdir, mod_k, hook, hook_fn)
|
|
else
|
|
return(hook_fn)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
--~ schema_prefix: "flow_user_script" or "elem_user_script"
|
|
function user_scripts.ts_dump(when, ifid, verbose, schema_prefix, all_scripts)
|
|
local ts_utils = require("ts_utils_core")
|
|
|
|
for subdir, script_type in pairs(all_scripts) do
|
|
local rv = user_scripts.getAggregatedStats(ifid, script_type, subdir)
|
|
local total = {tot_elapsed = 0, tot_num_calls = 0}
|
|
|
|
for modkey, stats in pairs(rv) do
|
|
ts_utils.append(schema_prefix .. ":duration", {ifid = ifid, user_script = modkey, subdir = subdir, num_ms = stats.tot_elapsed * 1000}, when)
|
|
ts_utils.append(schema_prefix .. ":num_calls", {ifid = ifid, user_script = modkey, subdir = subdir, num_calls = stats.tot_num_calls}, when)
|
|
|
|
total.tot_elapsed = total.tot_elapsed + stats.tot_elapsed
|
|
total.tot_num_calls = total.tot_num_calls + stats.tot_num_calls
|
|
end
|
|
|
|
ts_utils.append(schema_prefix .. ":total_stats", {ifid = ifid, subdir = subdir, num_ms = total.tot_elapsed * 1000, num_calls = total.tot_num_calls}, when)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function user_scripts_benchmarks_key(ifid, subdir)
|
|
return string.format("ntopng.cache.ifid_%d.user_scripts_benchmarks.subdir_%s", ifid, subdir)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Returns the benchmark stats, aggregating them by module
|
|
function user_scripts.getAggregatedStats(ifid, script_type, subdir)
|
|
local bencmark = ntop.getCache(user_scripts_benchmarks_key(ifid, subdir))
|
|
local rv = {}
|
|
|
|
if(not isEmptyString(bencmark)) then
|
|
bencmark = json.decode(bencmark)
|
|
|
|
if(bencmark ~= nil) then
|
|
for scriptk, hooks in pairs(bencmark) do
|
|
local aggr_val = {tot_num_calls = 0, tot_elapsed = 0}
|
|
|
|
for _, hook_benchmark in pairs(hooks) do
|
|
aggr_val.tot_elapsed = aggr_val.tot_elapsed + hook_benchmark.tot_elapsed
|
|
aggr_val.tot_num_calls = hook_benchmark.tot_num_calls + aggr_val.tot_num_calls
|
|
end
|
|
|
|
if(aggr_val.tot_num_calls > 0) then
|
|
rv[scriptk] = aggr_val
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Save benchmarks results and possibly print them to stdout
|
|
--
|
|
-- @param to_stdout dump results also to stdout
|
|
function user_scripts.benchmark_dump(ifid, to_stdout)
|
|
-- Convert ticks to seconds
|
|
for subdir, modules in pairs(benchmarks) do
|
|
local rv = {}
|
|
|
|
for mod_k, hooks in pairs(modules) do
|
|
for hook, hook_benchmark in pairs(hooks) do
|
|
if hook_benchmark["tot_num_calls"] > 0 then
|
|
hook_benchmark["tot_elapsed"] = hook_benchmark["tot_elapsed"] / ntop.gettickspersec()
|
|
|
|
rv[mod_k] = rv[mod_k] or {}
|
|
rv[mod_k][hook] = hook_benchmark
|
|
|
|
if to_stdout then
|
|
traceError(TRACE_NORMAL,TRACE_CONSOLE,
|
|
string.format("[%s] %s() [script: %s][elapsed: %.4f][num: %u][speed: %.4f]\n",
|
|
subdir, hook, mod_k, hook_benchmark["tot_elapsed"], hook_benchmark["tot_num_calls"],
|
|
hook_benchmark["tot_elapsed"] / hook_benchmark["tot_num_calls"]))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
ntop.setCache(user_scripts_benchmarks_key(ifid, subdir), json.encode(rv), 3600 --[[ 1 hour --]])
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function getScriptsDirectories(script_type, subdir)
|
|
local check_dirs = {
|
|
user_scripts.getSubdirectoryPath(script_type, subdir),
|
|
}
|
|
|
|
return(check_dirs)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Lists available user scripts.
|
|
-- @params script_type one of user_scripts.script_types
|
|
-- @params subdir the modules subdir
|
|
-- @return a list of available module names
|
|
function user_scripts.listScripts(script_type, subdir)
|
|
local check_dirs = getScriptsDirectories(script_type, subdir)
|
|
local rv = {}
|
|
|
|
for _, checks_dir in pairs(check_dirs) do
|
|
for fname in pairs(ntop.readdir(checks_dir)) do
|
|
if string.ends(fname, ".lua") then
|
|
local mod_fname = string.sub(fname, 1, string.len(fname) - 4)
|
|
rv[#rv + 1] = mod_fname
|
|
end
|
|
end
|
|
end
|
|
|
|
return rv
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getLastBenchmark(ifid, subdir)
|
|
local scripts_benchmarks = ntop.getCache(user_scripts_benchmarks_key(ifid, subdir))
|
|
|
|
if(not isEmptyString(scripts_benchmarks)) then
|
|
scripts_benchmarks = json.decode(scripts_benchmarks)
|
|
else
|
|
scripts_benchmarks = {}
|
|
end
|
|
|
|
return(scripts_benchmarks)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Tries and load a script template, returning a new instance (if found)
|
|
-- All templates loaded here must inherit from `user_script_template.lua`
|
|
local function loadAndCheckScriptTemplate(user_script, user_script_template)
|
|
local res
|
|
|
|
if not user_script_template then
|
|
-- Default name
|
|
user_script_template = "user_script_template"
|
|
end
|
|
|
|
-- First, try and load the template straight from the plugin templates
|
|
local template_require = plugins_utils.loadTemplate(user_script.plugin.key, user_script_template)
|
|
|
|
-- Then, if no template is found inside the plugin, try and load the template from the ntopng templates
|
|
-- in modules that can be shared across multiple plugins
|
|
if not template_require then
|
|
-- Attempt at locating the template class under modules (global to ntopng)
|
|
local template_path = os_utils.fixPath(dirs.installdir .. "/scripts/lua/modules/user_script_templates/"..user_script_template..".lua")
|
|
if ntop.exists(template_path) then
|
|
-- Require the template file
|
|
template_require = require("user_script_templates."..user_script_template)
|
|
end
|
|
end
|
|
|
|
if template_require then
|
|
-- Create an instance of the template
|
|
res = template_require.new(user_script)
|
|
end
|
|
|
|
return res
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function init_user_script(user_script, mod_fname, full_path, plugin, script_type, subdir)
|
|
user_script.key = mod_fname
|
|
user_script.path = full_path
|
|
user_script.subdir = subdir
|
|
user_script.default_enabled = ternary(user_script.default_enabled == false, false, true --[[ a nil value means enabled ]])
|
|
user_script.source_path = plugins_utils.getUserScriptSourcePath(user_script.path)
|
|
user_script.plugin = plugin
|
|
user_script.script_type = script_type
|
|
user_script.edition = plugin.edition
|
|
user_script.category = checkCategory(user_script.category)
|
|
user_script.num_filtered = tonumber(ntop.getCache(string.format(NUM_FILTERED_KEY, subdir, mod_fname))) or 0 -- math.random(1000,2000)
|
|
|
|
if user_script.gui then
|
|
user_script.template = loadAndCheckScriptTemplate(user_script, user_script.gui.input_builder)
|
|
|
|
if(user_script.template == nil) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Unknown template '%s' for user script '%s'", user_script.gui.input_builder, mod_fname))
|
|
end
|
|
|
|
-- Possibly localize the input title/description
|
|
if user_script.gui.input_title then
|
|
user_script.gui.input_title = i18n(user_script.gui.input_title) or user_script.gui.input_title
|
|
end
|
|
if user_script.gui.input_description then
|
|
user_script.gui.input_description = i18n(user_script.gui.input_description) or user_script.gui.input_description
|
|
end
|
|
end
|
|
|
|
-- Expand hooks
|
|
if(user_script.hooks["all"] ~= nil) then
|
|
local callback = user_script.hooks["all"]
|
|
user_script.hooks["all"] = nil
|
|
|
|
for _, hook in pairs(script_type.hooks) do
|
|
user_script.hooks[hook] = callback
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function loadAndCheckScript(mod_fname, full_path, plugin, script_type, subdir, return_all, scripts_filter, hook_filter)
|
|
local alerts_disabled = (not areAlertsEnabled())
|
|
local setup_ok = true
|
|
|
|
-- Recheck the edition as the demo mode may expire
|
|
if((plugin.edition == "pro" and (not ntop.isPro())) or
|
|
((plugin.edition == "enterprise" and (not ntop.isEnterpriseM())))) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping user script '%s' with '%s' edition", mod_fname, plugin.edition))
|
|
return(nil)
|
|
end
|
|
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Loading user script '%s'", mod_fname))
|
|
|
|
local user_script = dofile(full_path)
|
|
|
|
if(type(user_script) ~= "table") then
|
|
traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Loading '%s' failed", full_path))
|
|
return(nil)
|
|
end
|
|
|
|
if((not return_all) and user_script.packet_interface_only and (not interface.isPacketInterface())) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping module '%s' for non packet interface", mod_fname))
|
|
return(nil)
|
|
end
|
|
|
|
if((not return_all) and ((user_script.nedge_exclude and ntop.isnEdge()) or (user_script.nedge_only and (not ntop.isnEdge())))) then
|
|
return(nil)
|
|
end
|
|
|
|
if((not return_all) and (user_script.windows_exclude and ntop.isWindows())) then
|
|
return(nil)
|
|
end
|
|
|
|
if(table.empty(user_script.hooks)) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("No 'hooks' defined in user script '%s', skipping", mod_fname))
|
|
return(nil)
|
|
end
|
|
|
|
if(user_script.l7_proto ~= nil) then
|
|
user_script.l7_proto_id = interface.getnDPIProtoId(user_script.l7_proto)
|
|
|
|
if(user_script.l7_proto_id == -1) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Unknown L7 protocol filter '%s' in user script '%s', skipping", user_script.l7_proto, mod_fname))
|
|
return(nil)
|
|
end
|
|
end
|
|
|
|
if((not user_script.gui) or (not user_script.gui.i18n_title) or (not user_script.gui.i18n_description)) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Module '%s' does not define a gui", mod_fname))
|
|
end
|
|
|
|
-- Augument with additional attributes
|
|
init_user_script(user_script, mod_fname, full_path, plugin, script_type, subdir)
|
|
|
|
if((not return_all) and alerts_disabled and user_script.is_alert) then
|
|
return(nil)
|
|
end
|
|
|
|
if(hook_filter ~= nil) then
|
|
-- Only return modules which should be called for the specified hook
|
|
if((user_script.hooks[hook_filter] == nil) and (user_script.hooks["all"] == nil)) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping module '%s' for hook '%s'", user_script.key, hook_filter))
|
|
return(nil)
|
|
end
|
|
end
|
|
|
|
if(scripts_filter ~= nil) then
|
|
local script_ok = scripts_filter(user_script)
|
|
|
|
if(not script_ok) then
|
|
return(nil)
|
|
end
|
|
end
|
|
|
|
-- If a setup function is available, call it
|
|
if(user_script.setup ~= nil) then
|
|
setup_ok = user_script.setup()
|
|
end
|
|
|
|
if((not return_all) and (not setup_ok)) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping module '%s' as setup() returned %s", user_script.key, setup_ok))
|
|
return(nil)
|
|
end
|
|
|
|
return(user_script)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Load the user scripts.
|
|
-- @param ifid the interface ID
|
|
-- @param script_type one of user_scripts.script_types
|
|
-- @param subdir the modules subdir. *NOTE* this must be unique as it is used as a key.
|
|
-- @param options an optional table with the following supported options:
|
|
-- - hook_filter: if non nil, only load the user scripts for the specified hook
|
|
-- - do_benchmark: if true, computes benchmarks for every hook
|
|
-- - return_all: if true, returns all the scripts, even those with filters not matching the current configuration
|
|
-- NOTE: this can only be applied if the script type has the "has_no_entity" flag set.
|
|
-- - scripts_filter: a filter function(user_script) -> true, false. false will cause the script to be skipped.
|
|
-- @return {modules = key->user_script, hooks = user_script->function}
|
|
function user_scripts.load(ifid, script_type, subdir, options)
|
|
local rv = {modules = {}, hooks = {}, conf = {}}
|
|
local old_ifid = interface.getId()
|
|
options = options or {}
|
|
ifid = tonumber(ifid)
|
|
|
|
-- Load additional schemas
|
|
plugins_utils.loadSchemas(options.hook_filter)
|
|
|
|
local hook_filter = options.hook_filter
|
|
local do_benchmark = options.do_benchmark
|
|
local return_all = options.return_all
|
|
local scripts_filter = options.scripts_filter
|
|
|
|
if(old_ifid ~= ifid) then
|
|
interface.select(tostring(ifid)) -- required for interface.isPacketInterface() below
|
|
end
|
|
|
|
for _, hook in pairs(script_type.hooks) do
|
|
rv.hooks[hook] = {}
|
|
end
|
|
|
|
local check_dirs = getScriptsDirectories(script_type, subdir)
|
|
|
|
for _, checks_dir in pairs(check_dirs) do
|
|
for fname in pairs(ntop.readdir(checks_dir)) do
|
|
if string.ends(fname, ".lua") then
|
|
local mod_fname = string.sub(fname, 1, string.len(fname) - 4)
|
|
local full_path = os_utils.fixPath(checks_dir .. "/" .. fname)
|
|
local plugin = plugins_utils.getUserScriptPlugin(full_path)
|
|
|
|
if(plugin == nil) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Skipping unknown user script '%s'", mod_fname))
|
|
goto next_module
|
|
end
|
|
|
|
if(rv.modules[mod_fname]) then
|
|
traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Skipping duplicate module '%s'", mod_fname))
|
|
goto next_module
|
|
end
|
|
|
|
local user_script = loadAndCheckScript(mod_fname, full_path, plugin, script_type, subdir, return_all, scripts_filter, hook_filter)
|
|
|
|
if(not user_script) then
|
|
goto next_module
|
|
end
|
|
|
|
-- Checks passed, now load the script information
|
|
|
|
-- Populate hooks fast lookup table
|
|
for hook, hook_fn in pairs(user_script.hooks) do
|
|
-- load previously computed benchmarks (if any)
|
|
-- benchmarks are loaded even if their computation is disabled with a do_benchmark ~= true
|
|
if(rv.hooks[hook] == nil) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Unknown hook '%s' in module '%s'", hook, user_script.key))
|
|
else
|
|
if do_benchmark then
|
|
rv.hooks[hook][user_script.key] = benchmark_init(subdir, user_script.key, hook, hook_fn)
|
|
else
|
|
rv.hooks[hook][user_script.key] = hook_fn
|
|
end
|
|
end
|
|
end
|
|
|
|
rv.modules[user_script.key] = user_script
|
|
end
|
|
|
|
::next_module::
|
|
end
|
|
end
|
|
|
|
if(old_ifid ~= ifid) then
|
|
interface.select(tostring(old_ifid))
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Convenient method to only load a specific script
|
|
function user_scripts.loadModule(ifid, script_type, subdir, mod_fname)
|
|
local check_dirs = getScriptsDirectories(script_type, subdir)
|
|
|
|
for _, checks_dir in pairs(check_dirs) do
|
|
local full_path = os_utils.fixPath(checks_dir .. "/" .. mod_fname .. ".lua")
|
|
local plugin = plugins_utils.getUserScriptPlugin(full_path)
|
|
|
|
if(ntop.exists(full_path) and (plugin ~= nil)) then
|
|
local user_script = loadAndCheckScript(mod_fname, full_path, plugin, script_type, subdir)
|
|
|
|
return(user_script)
|
|
end
|
|
end
|
|
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.runPeriodicScripts()
|
|
local requested = {}
|
|
for granularity, _ in pairs(alert_consts.alerts_granularities) do
|
|
local k = string.format(REQUEST_PERIODIC_USER_SCRIPTS_RUN_KEY, interface.getId(), granularity)
|
|
|
|
if ntop.getCache(k) == "1" then
|
|
requested[granularity] = true
|
|
ntop.delCache(k)
|
|
end
|
|
end
|
|
|
|
if table.len(requested) > 0 then
|
|
interface.checkInterfaceAlerts(requested["min"], requested["5mins"], requested["hour"], requested["day"])
|
|
interface.checkNetworksAlerts(requested["min"], requested["5mins"], requested["hour"], requested["day"])
|
|
interface.checkHostsAlerts(requested["min"], requested["5mins"], requested["hour"], requested["day"])
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.schedulePeriodicScripts(granularity)
|
|
if alert_consts.alerts_granularities[granularity] then
|
|
local k = string.format(REQUEST_PERIODIC_USER_SCRIPTS_RUN_KEY, interface.getId(), granularity)
|
|
ntop.setCache(k, "1")
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Teardown function, to be called at the end of the VM
|
|
function user_scripts.teardown(available_modules, do_benchmark, do_print_benchmark)
|
|
for _, script in pairs(available_modules.modules) do
|
|
if script.teardown then
|
|
script.teardown()
|
|
end
|
|
end
|
|
|
|
if do_benchmark then
|
|
local ifid = interface.getId()
|
|
user_scripts.benchmark_dump(ifid, do_print_benchmark)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.listSubdirs()
|
|
local rv = {}
|
|
|
|
for _, subdir in ipairs(available_subdirs) do
|
|
local item = table.clone(subdir)
|
|
item.label = i18n(item.label) or item.label
|
|
|
|
rv[#rv + 1] = item
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Reload user scripts with their existing configurations.
|
|
-- Method called as part of plugins reload (during startup or when plugins are reloaded)
|
|
-- @param is_load Boolean, indicating whether callback onLoad/onUnload should be called
|
|
-- @return nil
|
|
function user_scripts.loadUnloadUserScripts(is_load)
|
|
-- Read all existing configsets.
|
|
-- NOTE: A user script can have more than one configuration associated, each
|
|
-- one identified with an id. For example, hosts have multiple configurations,
|
|
-- each one applied to a different subset of pools. On the other hand, flows
|
|
-- and system only have only one (default) user script configuration.
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
-- For each subdir available, (i.e., host, flow, interface, ...)
|
|
for _, subdir in ipairs(user_scripts.listSubdirs()) do
|
|
-- Load all the available user scripts for this subdir
|
|
local scripts = user_scripts.load(interface.getId(), user_scripts.getScriptType(subdir.id), subdir.id, {return_all = true})
|
|
|
|
for name, script in pairsByKeys(scripts.modules) do
|
|
for confid, config in pairs(configsets) do
|
|
-- Call user script callbacks for
|
|
-- each available configuration existing for the user script
|
|
if not config.config then
|
|
traceError(TRACE_ERROR,TRACE_CONSOLE, string.format("Configuration is missing"))
|
|
return
|
|
end
|
|
|
|
if not config.config[subdir.id] then
|
|
traceError(TRACE_ERROR,TRACE_CONSOLE, string.format("Missing subdir '%s' from config", subdir.id))
|
|
return
|
|
end
|
|
|
|
if not config.config[subdir.id][script.key] then
|
|
-- Configuration can be empty (for example the first time a user script is added)
|
|
traceError(TRACE_NORMAL,TRACE_CONSOLE,
|
|
string.format("Script '%s' configuration is missing from subdir '%s'. New user script?", script.key, subdir.id))
|
|
else
|
|
local s = config.config[subdir.id][script.key]
|
|
|
|
if(s ~= nil) then
|
|
for hook, hook_config in pairs(s) do
|
|
-- For each configuration there are multiple hooks.
|
|
-- Some hooks can be enabled, whereas some other hooks can be disabled:
|
|
-- methods onLoad/onUnload are only called for hooks that are enabled.
|
|
if script and hook_config.enabled then
|
|
-- onLoad/onUnload methods are ONLY called for user scripts that are enabled
|
|
if is_load and script.onLoad then
|
|
-- This is a load operation
|
|
script.onLoad(hook, hook_config, confid)
|
|
elseif not is_load and script.onUnload then
|
|
-- This is an unload operation
|
|
script.onUnload(hook, hook_config, confid)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.findConfigSet(configsets, name)
|
|
for id, configset in pairs(configsets) do
|
|
if(configset.name == name) then
|
|
return(configset)
|
|
end
|
|
end
|
|
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function getNewConfigSetId(configsets)
|
|
local max_id = -1
|
|
|
|
for i in pairs(configsets) do
|
|
max_id = math.max(max_id, tonumber(i))
|
|
end
|
|
|
|
return(max_id+1)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function saveConfigsets(configsets)
|
|
local to_delete = ntop.getHashKeysCache(CONFIGSETS_KEY) or {}
|
|
|
|
for _, configset in pairs(configsets) do
|
|
local k = string.format("%d", configset.id)
|
|
local v = json.encode(configset)
|
|
|
|
ntop.setHashCache(CONFIGSETS_KEY, k, v)
|
|
to_delete[k] = nil
|
|
end
|
|
|
|
for confid in pairs(to_delete) do
|
|
ntop.delHashCache(CONFIGSETS_KEY, confid)
|
|
end
|
|
|
|
-- Reload the periodic scripts as the configuration has changed
|
|
ntop.reloadPeriodicScripts()
|
|
|
|
return true
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local cached_config_sets = nil
|
|
|
|
function user_scripts.getConfigsets()
|
|
if cached_config_sets then
|
|
return(cached_config_sets)
|
|
end
|
|
|
|
local configsets = ntop.getHashAllCache(CONFIGSETS_KEY) or {}
|
|
local rv = {}
|
|
|
|
for _, confset_json in pairs(configsets) do
|
|
local confset = json.decode(confset_json)
|
|
|
|
if confset then
|
|
rv[confset.id] = confset
|
|
end
|
|
end
|
|
|
|
-- Cache to avoid loading them again
|
|
cached_config_sets = rv
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Returns all `subdir` pools which currently have `config` set as script configuration
|
|
function user_scripts.getConfigsetPools(subdir, confid)
|
|
confid = tonumber(confid)
|
|
|
|
for _, available_subdir in pairs(available_subdirs) do
|
|
if available_subdir["id"] == subdir then
|
|
if available_subdir["pools"] then
|
|
-- Use load to dynamically require only the pools class of interest
|
|
local required_pools, err = load(string.format('return require "%s"', available_subdir["pools"]))
|
|
|
|
if required_pools then
|
|
local ok, pools = pcall(required_pools)
|
|
|
|
if ok then
|
|
local pools_instance = pools:create()
|
|
-- Retrieve all pools which have confid currently assigned
|
|
return pools_instance:get_pools_by_configset_id(confid)
|
|
end
|
|
end
|
|
end
|
|
|
|
break
|
|
end
|
|
end
|
|
|
|
return {}
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.deleteConfigset(confid)
|
|
confid = tonumber(confid)
|
|
|
|
if(confid == user_scripts.DEFAULT_CONFIGSET_ID) then
|
|
return false, "Cannot delete default configset"
|
|
end
|
|
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
-- Remove the configset from all the pools to could have been associated to
|
|
pools_lua_utils.unbind_all_configset_id(confid)
|
|
|
|
configsets[confid] = nil
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.createOrReplaceConfigset(configset)
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
local existing = user_scripts.findConfigSet(configsets, configset.name)
|
|
if existing then
|
|
configsets[existing.id] = nil
|
|
pools_lua_utils.unbind_all_recipient_id(existing.id)
|
|
end
|
|
|
|
local new_confid = 0
|
|
if configset.id ~= 0 then
|
|
new_confid = getNewConfigSetId(configsets)
|
|
end
|
|
|
|
configsets[new_confid] = table.clone(configset)
|
|
configsets[new_confid].id = new_confid
|
|
|
|
local rv, err = saveConfigsets(configsets)
|
|
|
|
if not rv then
|
|
return rv, err
|
|
end
|
|
|
|
return true, new_confid
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.renameConfigset(confid, new_name)
|
|
if(confid == user_scripts.DEFAULT_CONFIGSET_ID) then
|
|
return false, "Cannot rename default configset"
|
|
end
|
|
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
local existing = user_scripts.findConfigSet(configsets, new_name)
|
|
|
|
if existing then
|
|
if(existing.id == confid) then
|
|
-- Renaming to the same name has no effect
|
|
return true
|
|
end
|
|
|
|
return false, i18n("configsets.error_exists", {name=new_name})
|
|
end
|
|
|
|
configsets[confid].name = new_name
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.cloneConfigset(confid, new_name)
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
local existing = user_scripts.findConfigSet(configsets, new_name)
|
|
|
|
if existing then
|
|
return false, i18n("configsets.error_exists", {name=new_name})
|
|
end
|
|
|
|
local new_confid = getNewConfigSetId(configsets)
|
|
|
|
configsets[new_confid] = table.clone(configsets[confid])
|
|
configsets[new_confid].id = new_confid
|
|
configsets[new_confid].name = new_name
|
|
|
|
local rv, err = saveConfigsets(configsets)
|
|
|
|
if(not rv) then
|
|
return rv, err
|
|
end
|
|
|
|
return true, new_confid
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function filterIsEqual(applied_config, new_filter)
|
|
local ctr = 1
|
|
|
|
if applied_config == nil then
|
|
applied_config = {}
|
|
|
|
return ctr
|
|
end
|
|
|
|
for counter, filter in pairs(applied_config) do
|
|
if table.compare(filter, new_filter) then
|
|
return 0
|
|
end
|
|
|
|
ctr = ctr + 1
|
|
end
|
|
|
|
return ctr
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Update the configuration of a specific script in a configset
|
|
function user_scripts.updateScriptConfig(confid, script_key, subdir, new_config, additional_params, additional_filters)
|
|
local configsets = user_scripts.getConfigsets()
|
|
-- additional_params contains additional params for script conf such as the severity
|
|
additional_params = additional_params or {}
|
|
new_config = new_config or {}
|
|
local applied_config = {}
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
local script_type = user_scripts.getScriptType(subdir)
|
|
local script = user_scripts.loadModule(interface.getId(), script_type, subdir, script_key)
|
|
|
|
if(script) then
|
|
-- Try to validate the configuration
|
|
for hook, conf in pairs(new_config) do
|
|
local valid = true
|
|
local rv_or_err = ""
|
|
|
|
for key, value in pairs(additional_params) do
|
|
conf.script_conf[key] = value
|
|
end
|
|
|
|
if(conf.enabled == nil) then
|
|
return false, "Missing 'enabled' item"
|
|
end
|
|
|
|
if(conf.script_conf == nil) then
|
|
return false, "Missing 'script_conf' item"
|
|
end
|
|
|
|
if conf.enabled then
|
|
valid, rv_or_err = script.template:parseConfig(conf.script_conf)
|
|
else
|
|
-- Assume the config is valid when the script is disabled to simplify the check
|
|
valid = true
|
|
rv_or_err = conf.script_conf
|
|
end
|
|
|
|
if(not valid) then
|
|
return false, rv_or_err
|
|
end
|
|
|
|
-- The validator may have changed the configuration
|
|
conf.script_conf = rv_or_err
|
|
applied_config[hook] = conf
|
|
end
|
|
end
|
|
|
|
local config = configsets[confid].config
|
|
local filter_conf = configsets[confid]
|
|
|
|
-- Creating the filters_conf if necessary
|
|
if not filter_conf["filters"] then
|
|
filter_conf["filters"] = {}
|
|
end
|
|
|
|
filter_conf = filter_conf["filters"]
|
|
|
|
if not filter_conf[subdir] then
|
|
filter_conf[subdir] = {}
|
|
end
|
|
|
|
filter_conf = filter_conf[subdir]
|
|
|
|
if not filter_conf[script_key] then
|
|
filter_conf[script_key] = {}
|
|
end
|
|
|
|
filter_conf = filter_conf[script_key]
|
|
------------------------------------
|
|
|
|
config[subdir] = config[subdir] or {}
|
|
|
|
if script then
|
|
local prev_config = config[subdir][script_key]
|
|
|
|
-- Perform hook callbacks for config changes, or enable/disable
|
|
for hook, hook_config in pairs(prev_config) do
|
|
local hook_applied_config = applied_config[hook]
|
|
|
|
if hook_applied_config then
|
|
if script.onDisable and hook_config.enabled and not hook_applied_config.enabled then
|
|
-- Hook previously disabled has been enabled
|
|
script.onDisable(hook, hook_applied_config, confid)
|
|
elseif script.onEnable and not hook_config.enabled and hook_applied_config.enabled then
|
|
-- Hook previously enabled has now been disabled
|
|
script.onEnable(hook, hook_applied_config, confid)
|
|
elseif script.onUpdateConfig and not table.compare(hook_config, applied_config[hook]) then
|
|
-- Configuration for the hook has changed
|
|
script.onUpdateConfig(hook, hook_applied_config, confid)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Updating the filters
|
|
if additional_filters then
|
|
local new_filter_conf = filter_conf
|
|
|
|
if not new_filter_conf["filter"] then
|
|
new_filter_conf["filter"] = {}
|
|
end
|
|
|
|
if not new_filter_conf["filter"]["current_filters"] then
|
|
new_filter_conf["filter"]["current_filters"] = {}
|
|
new_filter_conf["filter"]["current_filters"] = (user_scripts.getDefaultFilters(interface.getId(), subdir, script_key))["current_filters"] or {}
|
|
end
|
|
|
|
-- If filter reset requested, clear all the filters
|
|
if additional_filters["reset_filters"] == "true" then
|
|
new_filter_conf["filter"]["current_filters"] = {}
|
|
end
|
|
|
|
if table.len(additional_filters) == 0 then
|
|
new_filter_conf["filter"]["current_filters"] = {}
|
|
else
|
|
-- There can be multiple filters, so cycle through them
|
|
for _, new_filter in pairs(additional_filters["new_filters"]) do
|
|
local add_params = filterIsEqual(new_filter_conf["filter"]["current_filters"], new_filter)
|
|
if add_params > 0 then
|
|
new_filter_conf["filter"]["current_filters"][add_params] = new_filter
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Updating the configuration
|
|
configsets[confid]["filters"][subdir][script_key] = new_filter_conf
|
|
end
|
|
|
|
if table.len(applied_config) > 0 then
|
|
-- Set the new configuration
|
|
config[subdir][script_key] = applied_config
|
|
end
|
|
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Toggles script `script_key` configuration on or off depending on `enable` for configuration `configset`
|
|
-- Hooks onDisable and onEnable are called.
|
|
-- @param configset_id The id of the configuration passed in `configset`
|
|
-- @param configset A user script configuration, i.e., one of the configurations obtained with user_scripts.getConfigsets()
|
|
-- @param script_key The string script identifier
|
|
-- @param subdir The string identifying the sub directory (e.g., flow, host, ...)
|
|
-- @param enable A boolean indicating whether the script shall be toggled on or off
|
|
local function toggleScriptConfigset(configset_id, configset, script_key, subdir, enable)
|
|
local script_type = user_scripts.getScriptType(subdir)
|
|
local script = user_scripts.loadModule(interface.getId(), script_type, subdir, script_key)
|
|
|
|
if not script then
|
|
return false, i18n("configsets.unknown_user_script", {user_script=script_key})
|
|
end
|
|
|
|
local config = user_scripts.getScriptConfig(configset, script, subdir)
|
|
|
|
if config then
|
|
for hook, hook_config in pairs(config) do
|
|
-- Remember the previous toggle
|
|
local prev_hook_config = hook_config.enabled
|
|
-- Save the new toggle
|
|
hook_config.enabled = enable
|
|
|
|
if script.onDisable and prev_hook_config and not enable then
|
|
-- Hook has been enabled for the user script
|
|
script.onDisable(hook, hook_config, configset_id)
|
|
elseif script.onEnable and not prev_hook_config and enable then
|
|
-- Hook has been disabled for the user script
|
|
script.onEnable(hook, hook_config, configset_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.toggleScript(confid, script_key, subdir, enable)
|
|
local configsets = user_scripts.getConfigsets()
|
|
local configset = configsets[confid]
|
|
|
|
if not configset then
|
|
return false, i18n("configsets.unknown_id", {confid = confid})
|
|
end
|
|
|
|
-- Toggle the configuration (result is put in `configset`)
|
|
local res, err = toggleScriptConfigset(confid, configset, script_key, subdir, enable)
|
|
if not res then
|
|
return res, err
|
|
end
|
|
|
|
-- If the toggle has been successful, write the new configset and return
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.toggleAllScripts(confid, subdir, enable)
|
|
local configsets = user_scripts.getConfigsets()
|
|
local configset = configsets[confid]
|
|
|
|
if not configset then
|
|
return false, i18n("configsets.unknown_id", {confid = confid})
|
|
end
|
|
|
|
-- Toggle the configuration (result is put in `configset`)
|
|
local scripts = user_scripts.load(getSystemInterfaceId(), user_scripts.getScriptType(subdir), subdir)
|
|
|
|
for script_name, script in pairs(scripts.modules) do
|
|
-- Toggle each script individually
|
|
local res, err = toggleScriptConfigset(confid, configset, script.key, subdir, enable)
|
|
if not res then
|
|
return res, err
|
|
end
|
|
end
|
|
|
|
-- If the toggle has been successful for all scripts, write the new configset and return
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Returns the factory user scripts configuration
|
|
-- Any user-submitted conf param is ignored
|
|
function user_scripts.getFactoryConfig()
|
|
local ifid = getSystemInterfaceId()
|
|
local default_conf = {}
|
|
|
|
for type_id, script_type in pairs(user_scripts.script_types) do
|
|
for _, subdir in pairs(script_type.subdirs) do
|
|
local scripts = user_scripts.load(ifid, script_type, subdir, {return_all = true})
|
|
|
|
for key, usermod in pairs(scripts.modules) do
|
|
default_conf[subdir] = default_conf[subdir] or {}
|
|
default_conf[subdir][key] = default_conf[subdir][key] or {}
|
|
local script_config = default_conf[subdir][key]
|
|
local hooks = ternary(script_type.has_per_hook_config, usermod.hooks, {[ALL_HOOKS_CONFIG_KEY]=1})
|
|
|
|
for hook in pairs(hooks) do
|
|
script_config[hook] = {
|
|
enabled = usermod.default_enabled or false,
|
|
script_conf = usermod.default_value or {},
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local res = {
|
|
id = user_scripts.DEFAULT_CONFIGSET_ID,
|
|
name = i18n("policy_presets.default"),
|
|
config = default_conf,
|
|
}
|
|
|
|
return res
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.loadDefaultConfig()
|
|
local ifid = getSystemInterfaceId()
|
|
local configsets = user_scripts.getConfigsets()
|
|
-- The configset
|
|
local default_configset = configsets[user_scripts.DEFAULT_CONFIGSET_ID] or {}
|
|
-- Default per user-script configuration
|
|
local default_conf = default_configset["config"] or {}
|
|
-- Default per user-script filters
|
|
local default_filters = default_configset["filters"] or {}
|
|
|
|
if default_conf then
|
|
-- Drop possible nested values due to a previous bug
|
|
default_conf.config = nil
|
|
end
|
|
|
|
for type_id, script_type in pairs(user_scripts.script_types) do
|
|
for _, subdir in pairs(script_type.subdirs) do
|
|
local scripts = user_scripts.load(ifid, script_type, subdir, {return_all = true})
|
|
|
|
for key, usermod in pairs(scripts.modules) do
|
|
-- Cleanup exclusion counters
|
|
ntop.delCache(string.format(NUM_FILTERED_KEY, subdir, key))
|
|
|
|
if((usermod.default_enabled ~= nil) or (usermod.default_value ~= nil)) then
|
|
default_conf[subdir] = default_conf[subdir] or {}
|
|
default_conf[subdir][key] = default_conf[subdir][key] or {}
|
|
local script_config = default_conf[subdir][key]
|
|
local hooks = ternary(script_type.has_per_hook_config, usermod.hooks, {[ALL_HOOKS_CONFIG_KEY]=1})
|
|
|
|
for hook in pairs(hooks) do
|
|
-- Do not override an existing configuration
|
|
if(script_config[hook] == nil) then
|
|
script_config[hook] = {
|
|
enabled = usermod.default_enabled or false,
|
|
script_conf = usermod.default_value or {},
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
configsets[user_scripts.DEFAULT_CONFIGSET_ID] = {
|
|
id = user_scripts.DEFAULT_CONFIGSET_ID,
|
|
name = i18n("policy_presets.default"),
|
|
config = default_conf,
|
|
filters = default_filters,
|
|
}
|
|
|
|
saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.resetConfigsets()
|
|
for confset_id, _ in pairs(user_scripts.getConfigsets()) do
|
|
user_scripts.deleteConfigset(confset_id)
|
|
end
|
|
|
|
cached_config_sets = nil
|
|
ntop.delCache(CONFIGSETS_KEY)
|
|
user_scripts.loadDefaultConfig()
|
|
|
|
return(true)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Returns true if a system script is enabled for some hook
|
|
function user_scripts.isSystemScriptEnabled(script_key)
|
|
-- Verify that the script is currently available
|
|
local k = "ntonpng.cache.user_scripts.available_system_modules." .. script_key
|
|
local available = ntop.getCache(k)
|
|
|
|
if(isEmptyString(available)) then
|
|
local m = user_scripts.loadModule(getSystemInterfaceId(), user_scripts.script_types.system, "system", script_key)
|
|
available = (m ~= nil)
|
|
|
|
ntop.setCache(k, ternary(available, "1", "0"))
|
|
else
|
|
available = ternary(available == "1", true, false)
|
|
end
|
|
|
|
if(not available) then
|
|
return(false)
|
|
end
|
|
|
|
-- Here the configuration is update with the exclusion list for the alerts
|
|
local configsets = user_scripts.getConfigsets()
|
|
local default_config = user_scripts.getDefaultConfig(configsets, "system")
|
|
local script_config = default_config[script_key]
|
|
|
|
if(script_config) then
|
|
for _, hook in pairs(script_config) do
|
|
if(hook.enabled) then
|
|
return(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
return(false)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local default_config = {
|
|
enabled = false,
|
|
script_conf = {},
|
|
}
|
|
|
|
-- @brief Retrieves the configuration of a specific script
|
|
function user_scripts.getScriptConfig(configset, script, subdir)
|
|
local script_key = script.key
|
|
local config = configset.config[subdir]
|
|
|
|
if(config[script_key]) then
|
|
-- A configuration was found
|
|
return(config[script_key])
|
|
end
|
|
|
|
-- Default
|
|
local rv = {}
|
|
local script_type = user_scripts.getScriptType(subdir)
|
|
local hooks = ternary(script_type.has_per_hook_config, script.hooks, {[ALL_HOOKS_CONFIG_KEY]=1})
|
|
|
|
for hook in pairs(script.hooks) do
|
|
rv[hook] = default_config
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Retrieves the configuration of a specific hook of the target
|
|
-- @param target_config target configuration as returned by
|
|
-- user_scripts.getTargetConfig/user_scripts.getHostTargetConfigset
|
|
function user_scripts.getTargetHookConfig(target_config, script, hook)
|
|
local script_conf = target_config[script.key or script]
|
|
|
|
if not hook then
|
|
-- See has_per_hook_config
|
|
hook = ALL_HOOKS_CONFIG_KEY
|
|
end
|
|
|
|
if(not script_conf) then
|
|
return(default_config)
|
|
end
|
|
|
|
local conf = script_conf[hook] or default_config
|
|
local default_values = script.default_value or {}
|
|
|
|
-- Each new default value will be added to the conf.script_conf table
|
|
-- in this way if future values need to be added here there won't be problems
|
|
for key, value in pairs(default_values) do
|
|
if not conf.script_conf[key] then
|
|
conf.script_conf[key] = value
|
|
end
|
|
end
|
|
|
|
local default_filter_table = script.filter or {}
|
|
local default_filter_suppression = {}
|
|
|
|
-- Checking if filters are configured by default
|
|
if default_filter_table then
|
|
conf.script_conf["filter"] = {}
|
|
default_filter_suppression = default_filter_table.default_filter or {}
|
|
end
|
|
|
|
|
|
return conf
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Retrieve a `subdir` configuration from the configset identified with `configset_id` from all the available `configsets` passed
|
|
function user_scripts.getConfigById(configsets, configset_id, subdir)
|
|
configset_id = tonumber(configset_id) or user_scripts.DEFAULT_CONFIGSET_ID
|
|
local configset = configsets[configset_id] or configsets[user_scripts.DEFAULT_CONFIGSET_ID]
|
|
|
|
if configset and configset["config"] and configset["config"][subdir] then
|
|
return configset["config"][subdir], configset.id
|
|
end
|
|
|
|
return {}, nil
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Retrieve `subdir` filters from the configset identified with `configset_id` from all the available `configsets` passed
|
|
function user_scripts.getFiltersById(configsets, configset_id, subdir)
|
|
if configsets then
|
|
configset_id = tonumber(configset_id) or user_scripts.DEFAULT_CONFIGSET_ID
|
|
|
|
local configset = configsets[configset_id] or configsets[user_scripts.DEFAULT_CONFIGSET_ID]
|
|
|
|
if configset and configset["filters"] and configset["filters"][subdir] then
|
|
return configset["filters"][subdir], configset.id
|
|
end
|
|
end
|
|
|
|
return {}, nil
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getDefaultConfig(configsets, subdir)
|
|
local conf = configsets[user_scripts.DEFAULT_CONFIGSET_ID]
|
|
|
|
if(conf == nil) then
|
|
return({})
|
|
end
|
|
|
|
return conf.config[subdir] or {}, conf.id
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getScriptEditorUrl(script)
|
|
if(script.edition == "community") then
|
|
local plugin_file_path = string.sub(script.source_path, string.len(dirs.scriptdir) + 1)
|
|
local plugin_path = string.sub(script.plugin.path, string.len(dirs.scriptdir) + 1)
|
|
return(string.format("%s/lua/code_viewer.lua?plugin_file_path=%s&plugin_path=%s", ntop.getHttpPrefix(), plugin_file_path, plugin_path))
|
|
end
|
|
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Returns the list of the default filters of a specific alert
|
|
function user_scripts.getFilterPreset(alert, alert_info)
|
|
local alert_generation = alert_info["alert_generation"]
|
|
|
|
if not alert_generation then
|
|
return ''
|
|
end
|
|
|
|
local script_key = alert_generation["script_key"]
|
|
local subdir = alert_generation["subdir"]
|
|
local filter_string = ''
|
|
local script_type = user_scripts.getScriptType(subdir)
|
|
local script = user_scripts.loadModule(interface.getId(), script_type, subdir, script_key)
|
|
local filter_to_use = {}
|
|
local subdir_id = getSubdirId(subdir)
|
|
|
|
if not script then
|
|
return ''
|
|
end
|
|
|
|
if subdir_id == -1 then
|
|
return ''
|
|
end
|
|
|
|
if not available_subdirs[subdir_id]["filter"] then
|
|
return ''
|
|
end
|
|
|
|
-- Checking if the script has default filter fields or not
|
|
-- if not, getting the default for the subdir
|
|
if script["filter"] and script["filter"]["default_fields"] then
|
|
filter_to_use = script["filter"]["default_fields"]
|
|
elseif available_subdirs[subdir_id]["filter"]["default_fields"] then
|
|
filter_to_use = available_subdirs[subdir_id]["filter"]["default_fields"]
|
|
end
|
|
|
|
local filter_table = {}
|
|
local index = 1
|
|
|
|
for _, field in pairs(filter_to_use) do
|
|
-- Check for field existance in the alert
|
|
local field_val = alert[field]
|
|
|
|
-- If the filed does not exist, try and look it up inside `alert_info`, that is,
|
|
-- a decoded JSON table containing variable alert data.
|
|
if not field_val then
|
|
field_val = alert_info[field]
|
|
end
|
|
|
|
if field_val then
|
|
-- Forming the string e.g. srv_addr=1.1.1.1
|
|
filter_table[index] = field .. "=" .. field_val
|
|
index = index + 1
|
|
end
|
|
end
|
|
|
|
-- Creating the required string to print into the GUI
|
|
return table.concat(filter_table, ",")
|
|
end
|
|
|
|
-- #################################
|
|
|
|
-- @bief Given an already validated filter, returns a SQLite WHERE clause matching all filter fields
|
|
-- @param configset A user script configuration, i.e., one of the configurations obtained with user_scripts.getConfigsets()
|
|
-- @param subdir the modules subdir
|
|
-- @param user_script The string script identifier
|
|
-- @param filter An already validated user script filter
|
|
-- @return A string with the SQLite WHERE clause
|
|
function user_scripts.prepareFilterSQLiteWhere(confset_id, subdir, user_script, filter)
|
|
-- Access the alert_json using SQLite `json_` functions to properly filter with fields
|
|
local filters_where = {}
|
|
|
|
-- This is to match elements inside the alert_json
|
|
local script_where = {
|
|
string.format("json_extract(alert_json, '$.alert_generation.confset_id') = %u", confset_id),
|
|
string.format("json_extract(alert_json, '$.alert_generation.subdir') = '%s'", subdir),
|
|
string.format("json_extract(alert_json, '$.alert_generation.script_key') = '%s'", user_script),
|
|
}
|
|
|
|
|
|
-- Now prepare each SQLite statement for every field
|
|
local subdir_id = getSubdirId(subdir)
|
|
|
|
-- Retrieving the available filters for the subdir. e.g. flow subdir
|
|
local available_fields = available_subdirs[subdir_id]["filter"]["available_fields"]
|
|
|
|
for field_key, field_val in pairs(filter) do
|
|
if available_fields[field_key] and available_fields[field_key]["sqlite"] then
|
|
local sqlite = available_fields[field_key]["sqlite"](field_val)
|
|
filters_where[#filters_where + 1] = sqlite
|
|
end
|
|
end
|
|
|
|
-- Concatenate
|
|
local where = table.merge(filters_where, script_where)
|
|
-- And merge everything with ANDs
|
|
where = table.concat(where, " AND ")
|
|
|
|
return where
|
|
end
|
|
|
|
-- #################################
|
|
|
|
function user_scripts.parseFilterParams(additional_filters, subdir, reset_filters)
|
|
local separator = ";"
|
|
local filter_list = {}
|
|
local param_list = {}
|
|
|
|
-- Empty string given, error
|
|
if isEmptyString(additional_filters) then
|
|
return false, i18n("invalid_filters.empty")
|
|
end
|
|
|
|
-- Sanity Check, Sometimes js puts a "_" or a ";" at the end of the string so removes them
|
|
if additional_filters:match("(.*)_$") or additional_filters:match("(.*);$") then
|
|
additional_filters = additional_filters:sub(1, -2)
|
|
end
|
|
|
|
additional_filters = additional_filters:gsub(" ", "")
|
|
|
|
if reset_filters == true then
|
|
filter_list["reset_filters"] = "true"
|
|
end
|
|
|
|
filter_list["new_filters"] = {}
|
|
param_list = filter_list["new_filters"]
|
|
|
|
-- Splitting on the ";" - ";" is used to remove "\n" from js
|
|
local ex_list = split(additional_filters, separator)
|
|
local subdir_id = getSubdirId(subdir)
|
|
|
|
if subdir_id == -1 then
|
|
return false, i18n("invalid_filters.invalid_subdir")
|
|
end
|
|
|
|
-- Retrieving the available filters for the subdir. e.g. flow subdir
|
|
local available_fields = available_subdirs[subdir_id]["filter"]["available_fields"]
|
|
|
|
for filter_num, filter in pairs(ex_list) do
|
|
separator = ","
|
|
-- Splitting the filters
|
|
local parameters = split(filter, separator)
|
|
|
|
for _,field in pairs(parameters) do
|
|
if field ~= "" then
|
|
separator = "="
|
|
-- Splitting filter name and filter value
|
|
local field_key_value = split(field, separator)
|
|
|
|
-- Checking that for each filter a key and a value is given
|
|
if not table.len(field_key_value) == 2 then
|
|
return false, i18n("invalid_filters.few_args", {args=field})
|
|
end
|
|
|
|
local field_key = field_key_value[1]
|
|
local field_value = field_key_value[2]
|
|
|
|
-- Getting the http_lint for the selected param, if no param is found
|
|
-- then the filter is not correct
|
|
|
|
if not available_fields[field_key] or not available_fields[field_key]["lint"] or not available_fields[field_key]["lint"](field_value) then
|
|
return false, i18n("invalid_filters.incorrect_args", {args=field})
|
|
end
|
|
|
|
if not param_list[filter_num] then
|
|
param_list[filter_num] = {}
|
|
end
|
|
|
|
-- Already added this param before, so 2 identical arguments given
|
|
if param_list[filter_num][field_key] then
|
|
return false, i18n("invalid_filters.double_arg", {args=field})
|
|
end
|
|
|
|
param_list[filter_num][field_key] = field_value
|
|
end
|
|
end
|
|
end
|
|
|
|
return true, filter_list
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.matchExcludeFilter(filters_config, script, subdir, context)
|
|
local subdir_id = getSubdirId(subdir)
|
|
|
|
if subdir_id == -1 or not script or not script.key then
|
|
-- No script available
|
|
return false
|
|
end
|
|
|
|
if not filters_config or not filters_config[script.key] or not filters_config[script.key]["filter"] or not filters_config[script.key]["filter"]["current_filters"] then
|
|
-- No filter available for this script config
|
|
return false
|
|
end
|
|
|
|
-- Get the available fields for this given `subdir`
|
|
local available_fields = available_subdirs[subdir_id]["filter"]["available_fields"]
|
|
|
|
-- Iterate configured filters for this user script identified with `script.key`
|
|
for filter_num, filter in pairs(filters_config[script.key]["filter"]["current_filters"]) do
|
|
local filter_matches = true
|
|
|
|
for field_key, field_val in pairs(filter) do
|
|
if not available_fields[field_key] or not available_fields[field_key]["match"] then
|
|
-- field_key not present among available_fields, or no getter available: - field_key is unsupported
|
|
filter_matches = false
|
|
else
|
|
-- field_key is supported, let's evaluate the match function
|
|
filter_matches = available_fields[field_key]["match"](context, field_val --[[ the value --]])
|
|
end
|
|
|
|
if not filter_matches then
|
|
if filters_debug then traceError(TRACE_NORMAL, TRACE_CONSOLE, script.key..": field NOT matching "..field_val) end
|
|
-- There's no match. Just break, don't waste time evaluating other parts of the filter
|
|
break
|
|
else
|
|
if filters_debug then traceError(TRACE_NORMAL, TRACE_CONSOLE, script.key..": field IS matching "..field_val) end
|
|
-- Don't break, continue the evaluation of this filter!
|
|
end
|
|
end
|
|
|
|
if filter_matches then
|
|
-- There's a match with this filter! let's return
|
|
if filters_debug then traceError(TRACE_NORMAL, TRACE_CONSOLE, script.key..": filter IS matching") end
|
|
-- Increase the counter
|
|
ntop.incrCache(string.format(NUM_FILTERED_KEY, subdir, script.key))
|
|
-- Return
|
|
return true
|
|
else
|
|
if filters_debug then traceError(TRACE_NORMAL, TRACE_CONSOLE, script.key..": filter NOT matching") end
|
|
end
|
|
end
|
|
|
|
-- No filter matching
|
|
if filters_debug then traceError(TRACE_NORMAL, TRACE_CONSOLE, script.key..": no matching filter, returning...") end
|
|
return false
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief This function is going to check if the user script needs to be excluded
|
|
-- from the list, due to not having filters or not
|
|
function user_scripts.excludeScriptFilters(alert, alert_json, confid, script_key, subdir)
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false
|
|
end
|
|
|
|
-- Getting the configuration
|
|
local config = configsets[confid]["filters"]
|
|
|
|
if not config then
|
|
return false
|
|
end
|
|
|
|
-- Security checks
|
|
local conf = config[subdir]
|
|
|
|
if not conf then
|
|
return false
|
|
end
|
|
|
|
conf = conf[script_key]
|
|
|
|
if not conf then
|
|
return false
|
|
end
|
|
|
|
local applied_filter_config = {}
|
|
local subdir_id = getSubdirId(subdir)
|
|
|
|
-- Checking if the script has the field "filter.current_filters"
|
|
if conf["filter"] then
|
|
applied_filter_config = conf["filter"]["current_filters"]
|
|
end
|
|
|
|
if not applied_filter_config then
|
|
return false
|
|
end
|
|
|
|
-- Cycling through the filters
|
|
for _, values in pairs(applied_filter_config) do
|
|
local done = true
|
|
-- Getting the keys and values of the filters. e.g. filter=src_port, value=3900
|
|
for filter, value in pairs(values) do
|
|
-- Possible strange pattern, so using the function find,
|
|
-- defined into the available field to check the presence of the data
|
|
local find_value = available_subdirs[subdir_id]["filter"]["available_fields"][filter]["find"]
|
|
if not find_value(alert, alert_json, filter, value) then
|
|
-- The alert has a different value for that filter
|
|
done = false
|
|
goto continue2
|
|
end
|
|
::continue::
|
|
end
|
|
|
|
-- if
|
|
if done then
|
|
return true
|
|
end
|
|
|
|
::continue2::
|
|
end
|
|
|
|
-- all the filters are correct, exclude the alert
|
|
return false
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getDefaultFilters(ifid, subdir, script_key)
|
|
|
|
local script_type = user_scripts.getScriptType(subdir)
|
|
local script = user_scripts.loadModule(ifid, script_type, subdir, script_key)
|
|
local filters = {}
|
|
filters["current_filters"] = {}
|
|
|
|
if script["filter"] and script["filter"]["default_filters"] then
|
|
filters["current_filters"] = script["filter"]["default_filters"]
|
|
end
|
|
|
|
return filters
|
|
end
|
|
|
|
|
|
-- ##############################################
|
|
|
|
local function printUserScriptsTable()
|
|
local ifid = interface.getId()
|
|
|
|
for _, info in ipairs(user_scripts.listSubdirs()) do
|
|
|
|
local scripts = user_scripts.load(ifid, user_scripts.getScriptType(info.id), info.id, {return_all = true})
|
|
|
|
for name, script in pairsByKeys(scripts.modules) do
|
|
|
|
local available = ""
|
|
local filters = {}
|
|
local hooks = {}
|
|
|
|
-- Hooks
|
|
for hook in pairsByKeys(script.hooks) do
|
|
hooks[#hooks + 1] = hook
|
|
end
|
|
hooks = table.concat(hooks, ", ")
|
|
|
|
-- Filters
|
|
if(script.is_alert) then filters[#filters + 1] = "alerts" end
|
|
if(script.l4_proto) then filters[#filters + 1] = "l4_proto=" .. script.l4_proto end
|
|
if(script.l7_proto) then filters[#filters + 1] = "l7_proto=" .. script.l7_proto end
|
|
if(script.packet_interface_only) then filters[#filters + 1] = "packet_interface" end
|
|
if(script.three_way_handshake_ok) then filters[#filters + 1] = "3wh_completed" end
|
|
if(script.local_only) then filters[#filters + 1] = "local_only" end
|
|
if(script.nedge_only) then filters[#filters + 1] = "nedge=true" end
|
|
if(script.nedge_exclude) then filters[#filters + 1] = "nedge=false" end
|
|
filters = table.concat(filters, ", ")
|
|
|
|
if (name == "my_custom_script") then
|
|
goto skip
|
|
end
|
|
|
|
-- Availability
|
|
if(script.edition == "enterprise_m") then
|
|
available = "Enterprise M"
|
|
elseif(script.edition == "enterprise_l") then
|
|
available = "Enterprise L"
|
|
elseif(script.edition == "pro") then
|
|
available = "Pro"
|
|
else
|
|
available = "Community"
|
|
end
|
|
|
|
local edit_url = user_scripts.getScriptEditorUrl(script)
|
|
|
|
if(edit_url) then
|
|
edit_url = ' <a title="'.. i18n("plugins_overview.action_view") ..'" href="'.. edit_url ..'" class="btn btn-sm btn-secondary" ><i class="fas fa-eye"></i></a>'
|
|
end
|
|
|
|
print(string.format(([[
|
|
<tr>
|
|
<td>%s</td>
|
|
<td>%s</td>
|
|
<td>%s</td>
|
|
<td>%s</td>
|
|
<td>%s</td>
|
|
<td class="text-right">%u</td>
|
|
<td class="text-center">%s</td></tr>
|
|
]]), name, info.label, available, hooks, filters, script.num_filtered, edit_url or ""))
|
|
::skip::
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- #######################################################
|
|
|
|
function user_scripts.printUserScripts()
|
|
print([[
|
|
<div class='col-12 my-3'>
|
|
<table class='table table-bordered table-striped' id='user-scripts'>
|
|
<thead>
|
|
<tr>
|
|
<th>]].. i18n("plugins_overview.script") ..[[</th>
|
|
<th>]].. i18n("plugins_overview.type") ..[[</th>
|
|
<th>]].. i18n("availability") ..[[</th>
|
|
<th>]].. i18n("plugins_overview.hooks") ..[[</th>
|
|
<th>]].. i18n("plugins_overview.filters") ..[[</th>
|
|
<th>]].. i18n("plugins_overview.filtered") ..[[</th>
|
|
<th>]].. i18n("action") ..[[</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>]])
|
|
printUserScriptsTable()
|
|
print([[
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<link href="]].. ntop.getHttpPrefix() ..[[/datatables/datatables.min.css" rel="stylesheet"/>
|
|
<script type='text/javascript'>
|
|
|
|
$(document).ready(function() {
|
|
|
|
const addFilterDropdown = (title, values, column_index, datatableFilterId, tableApi) => {
|
|
|
|
const createEntry = (val, callback) => {
|
|
const $entry = $(`<li class='dropdown-item pointer'>${val}</li>`);
|
|
$entry.click(function(e) {
|
|
|
|
$dropdownTitle.html(`<i class='fas fa-filter'></i> ${val}`);
|
|
$menuContainer.find('li').removeClass(`active`);
|
|
$entry.addClass(`active`);
|
|
callback(e);
|
|
});
|
|
|
|
return $entry;
|
|
}
|
|
|
|
const dropdownId = `${title}-filter-menu`;
|
|
const $dropdownContainer = $(`<div id='${dropdownId}' class='dropdown d-inline'></div>`);
|
|
const $dropdownButton = $(`<button class='btn-link btn dropdown-toggle' data-toggle='dropdown' type='button'></button>`);
|
|
const $dropdownTitle = $(`<span>${title}</span>`);
|
|
$dropdownButton.append($dropdownTitle);
|
|
|
|
const $menuContainer = $(`<ul class='dropdown-menu' id='${title}-filter'></ul>`);
|
|
values.forEach((val) => {
|
|
const $entry = createEntry(val, (e) => {
|
|
tableApi.columns(column_index).search(val).draw(true);
|
|
});
|
|
$menuContainer.append($entry);
|
|
});
|
|
|
|
const $allEntry = createEntry(']].. i18n('all') ..[[', (e) => {
|
|
$dropdownTitle.html(`${title}`);
|
|
$menuContainer.find('li').removeClass(`active`);
|
|
tableApi.columns().search('').draw(true);
|
|
});
|
|
$menuContainer.prepend($allEntry);
|
|
|
|
$dropdownContainer.append($dropdownButton, $menuContainer);
|
|
$(datatableFilterId).prepend($dropdownContainer);
|
|
}
|
|
|
|
const $userScriptsTable = $('#user-scripts').DataTable({
|
|
pagingType: 'full_numbers',
|
|
initComplete: function(settings) {
|
|
|
|
const table = settings.oInstance.api();
|
|
const types = [... new Set(table.columns(1).data()[0].flat())];
|
|
const availability = [... new Set(table.columns(2).data()[0].flat())];
|
|
|
|
addFilterDropdown(']].. i18n("availability") ..[[', availability, 2, "#user-scripts_filter", table);
|
|
addFilterDropdown(']].. i18n("plugins_overview.type") ..[[', types, 1, "#user-scripts_filter", table);
|
|
},
|
|
pageLength: 25,
|
|
language: {
|
|
info: "]].. i18n('showing_x_to_y_rows', {x='_START_', y='_END_', tot='_TOTAL_'}) ..[[",
|
|
search: "]].. i18n('search') ..[[:",
|
|
infoFiltered: "",
|
|
paginate: {
|
|
previous: '<',
|
|
next: '>',
|
|
first: '«',
|
|
last: '»'
|
|
},
|
|
},
|
|
});
|
|
|
|
});
|
|
|
|
</script>
|
|
]])
|
|
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
return(user_scripts)
|