ntopng/scripts/lua/modules/user_scripts.lua
Simone Mainardi c7082a4647 Implements new alerts database and drilldown
alert store skeleton

Alert database type changes

Implement alert store for host alerts. All alert store skeletons.

Fix class method access

Enable tracing

Implements simple queries for host alerts

Implement flow alert store

Fixes escaping of INSERT queries

Flow alerts database schema fixes

Adds escaping for alert JSON in flows and hosts

Implements queries .select() for alerts store

Adds limit and offset to perform paginated queries

Adds new REST getter for flow alerts

Name changes alert_severity to severity, alert_json to json

Fixes alert message not shown

Implement active monitoring alerts store

Implements sort of queries in the new dataabase

Changes alert_type to alert_id

Implement mac alerts store

Fixes flow alert messages

Implement system alerts store

Implement snmp alert store

Add missing items to the flow alerts

Add missing items to the host alerts

Add missing items to the mac alerts

Implements COUNT aplerts api

Add device_name to snmp alerts

add flow alerts templates

updated gitignore

fix for missing order field

add families defined in `alert_store_schema.sql`

Reworks and simplifies alert store subclasses

Implements REST API to fetch alert timeseries

Fixes date in flow REST api

Host alert json fix

Add get/host/alert/list.lua

Move alert/list.lua to alert/past/list.lua

Add alert/past/list.lua for all alert families

Add entity_id to system table to identify the alert type based on <alert_id, entity_id>

Add missing field

Implements facilities to query engaged alerts via REST

Handle both historical and past alerts in alert/list.lua

Fix count

Update params of select_historical

Implement method to add family-specific filters

Add alert/ts.lua for all alert families

Implements facilities and REST endpoints to delete alerts

Implements ordering of alert queries

Fix add_order_by group_by

Rest API tests update

Tests output update

Remove debug trace

Use alert_id instead of type. Add more flow alert info.

Update http lint

Format obsolete tlv version alerts. Add more host info.

Add row_id to list of alerts

Fix selection of engaged alerts

Add test for mac alerts (bcast domains)

Removes attempt to format alerts as flow alerts

Fixes interface selection for active monitoring

Update test output with rest changes

Add more fields to be ignores

Set alert count to 1 for the time being

add bar timeseries chart

add apexcharts

improvements on timeseries bar chart

registered chart callbakcs

working on alert page

fix for date format

Fixes acrive monitoring REST API

Fixes alignment of grouped alert data

Additional fix for alert histogram

remove useless if

formatting alerts page

fixes on flows alert stats table

rename local networks to device

format host pagie in alert_stats

add tag support for hosts and flow

implemented single delete action

add release modal

Fixes format of threshold cross interface alerts

Fixes wrong increase of dropped alerts

Implements exclusion list for invalid dns queries

Reworks exclusions lists for hosts and flows

Addresses #5212
Addresses #5113

Adds host alert keys in host callbacks definitions

Adds alert ids to flow callbacks

fix for not working button (#5215)

Fixes reported timeseries name

removed any additional button inside chart's toolbar (#5200)

Add tables for interfaces, networks, users to the schema. Skeleton alert_store classes

fixed broken range picker layout in firefox (#5199)

Alert insert fixes

Add more info to network alerts

Add rest endpoint for interface, network, user alerts

Fix endpoint selection in alerts_stats

Unifies columns between engaged and past alerts

Fixes Missing mandatory 'alert_granularity'

Minor fixes for missing alert_severity

Fixes arithmetic on a nil value (field 'last_seen')

Fixes get/system/alert/list.lua

use tstamp for column names (#5221)

Implements host alert formatter

Add alerts_store format_record_common

Use common format_record for am, system alerts

Use common format_record for all alerts

Fixes formatting of alerts of all types

Fixes nil in function 'hostinfo2label'

fixes on disable modal

add pages for network, user and interface endpoint (#5224)

Set alert_entity in all classes

Unifies influxdb alerts into system alerts

Addresses #5224

Unifies process alerts into system alerts

Addresses #5224

Cleanup unused periodicActivityEntity

Unifies category lists alerts into system alerts

Addresses #5224

Aligns new alert enums

Addresses #5224

Fixes alert page links

Fixes insertion of interface alerts

Implement filters for Host alerts

Fixes active monitoring alerts not triggering

Implement filters on flow alerts

Fixes for internal alerts timestamp and subtype

implements disable for the alerts

formatted alert disable label

Add address and device type to mac alert records

fix for delete alert toggle

Fix access to entity_val in alert_unexpected_new_device

add mac address and device type inside table

Fixes for new alert fields not handled

Fixes alert_definitions to handle new fields

Add ip/port to snmp alert records

Implements deletion of stored flow alerts

Add alert_name to all alert records via rest. Fix duration.

fixes for snmp tab

Implements delete of past host alerts

Add name to snmp alert records

fixes on system tab

Fix Date column

fixes link

Update menu

Fixes bad argument #3 to 'format' in snmp alerts

updated interface link

new alerts url for host (#5228)

Fixes sort of engaged alert

Minor cleanup

Fixes data returned for local network alerts

Fix duration for one shot. Note.

Fix duration override

Fixes interface selection for system alerts

Move host alert page

fixes for local network tab

Minor fix

Fix engaged host alerts

fixes on user tab

Fix alert_user_activity message
2021-04-26 19:41:34 +02:00

2060 lines
68 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.
-- Hack to avoid include loops
if(pragma_once_user_scripts == true) then
-- avoid multiple inclusions
return
end
pragma_once_user_scripts = true
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 alert_exclusions = require "alert_exclusions"
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 CONFIGSET_KEY = "ntopng.prefs.user_scripts.configset_v3" -- Keep in sync with ntop_defines.h FLOW_CALLBACKS_CONFIG
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 populated automatically when creating filters
default_fields = { "ip", },
-- All possible filter fields
available_fields = {
ip = {
lint = http_lint.validateIpAddress,
},
},
},
}, {
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 = { "ip", },
-- All possible filter fields
available_fields = {
ip = {
lint = http_lint.validateIpAddress,
},
},
},
-- 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 subdir == "host" then
path = string.format("%s/scripts/lua/modules/callback_definitions/%s", dirs.installdir, subdir)
elseif 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
local res = os_utils.fixPath(path)
return res
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
if user_script.plugin then
template_require = plugins_utils.loadTemplate(user_script.plugin.key, user_script_template)
end
-- 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 and plugin.edition
user_script.category = checkCategory(user_script.category)
-- A user script is assumed to be able to generate alerts if it has a flag or an alert id specified
user_script.is_alert = (user_script.is_alert == true or user_script.alert_id ~= nil)
user_script.num_filtered = tonumber(ntop.getCache(string.format(NUM_FILTERED_KEY, subdir, mod_fname))) or 0 -- math.random(1000,2000)
if subdir == "host" then
user_script.hooks = {min = true}
end
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 and 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
if not user_script.hooks then
-- Flow user scripts no longer have hooks. They have callbacks in C++ that have replaced hooks
user_script.hooks = {}
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 then
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
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(subdir ~= "flow" and subdir ~= "host" and 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) and subdir ~= "host" --[[ TODO: remove check when migration done --]] 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 or {}) 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) 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"])
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 configset
local configset = user_scripts.getConfigset()
-- 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
-- Call user script callbacks for
-- each available configuration existing for the user script
if not configset.config then
traceError(TRACE_ERROR,TRACE_CONSOLE, string.format("Configuration is missing"))
return
end
if not configset.config[subdir.id] then
traceError(TRACE_ERROR,TRACE_CONSOLE, string.format("Missing subdir '%s' from config", subdir.id))
return
end
if not configset.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 = configset.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)
elseif not is_load and script.onUnload then
-- This is an unload operation
script.onUnload(hook, hook_config)
end
end
end
end
end
end
end
end
-- ##############################################
local function saveConfigset(configset)
local v = json.encode(configset)
ntop.setCache(CONFIGSET_KEY, v)
-- Reload the periodic scripts as the configuration has changed
ntop.reloadPeriodicScripts()
-- Reload flow and host callbacks executed in C++
ntop.reloadFlowCallbacks()
ntop.reloadHostCallbacks()
return true
end
-- ##############################################
local cached_config_set = nil
-- Return the default config set
-- Note: Other config sets are deprecated
function user_scripts.getConfigset()
if not cached_config_set then
cached_config_set = json.decode(ntop.getCache(CONFIGSET_KEY))
end
return cached_config_set
end
-- ##############################################
function user_scripts.createOrReplaceConfigset(configset)
-- Skip configurations other then the only one supported (others are deprecated)
if configset.id and configset.id ~= user_scripts.DEFAULT_CONFIGSET_ID then
return false
end
-- Unbind recipients
local existing = user_scripts.getConfigset()
if existing then
pools_lua_utils.unbind_all_recipient_id(existing.id)
end
-- Clone config
configset = table.clone(configset)
configset.id = user_scripts.DEFAULT_CONFIGSET_ID
-- Save config
local rv = saveConfigset(configset)
if not rv then
return rv
end
return true
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(script_key, subdir, new_config, additional_params, additional_filters)
local configset = user_scripts.getConfigset()
local script_type = user_scripts.getScriptType("flow")
-- 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 = {}
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 = configset.config
-- Creating the filters conf if necessary
if not configset["filters"] then
configset["filters"] = {}
end
if not configset["filters"][subdir] then
configset["filters"][subdir] = {}
end
if not configset["filters"][subdir][script_key] then
configset["filters"][subdir][script_key] = {}
end
local filter_conf = configset["filters"][subdir][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 script.alert_id and additional_filters and (subdir == "host" or subdir == "flow") then
if subdir == "host" then
local current_filters = alert_exclusions.host_alerts_get_excluded_hosts(script.alert_id)
-- Add new filters
for _, new_filter in pairs(additional_filters["new_filters"] or {}) do
local new_ip = new_filter["ip"]
if not current_filters[new_ip] then
-- Not an already-added filter
alert_exclusions.disable_host_alert(new_ip, script.alert_id)
else
-- Filter was already added, nothing to do
current_filters[new_ip] = nil
end
end
-- Here we have leftovers, that is, previously added filters that should
-- be removed as they don't appear in the new filters
for current_ip, _ in pairs(current_filters or {}) do
alert_exclusions.enable_host_alert(current_ip, script.alert_id)
end
elseif subdir == "flow" then
local current_filters = alert_exclusions.flow_alerts_get_excluded_hosts(script.alert_id)
-- Add new filters (see above for hosts)
for _, new_filter in pairs(additional_filters["new_filters"] or {}) do
local new_ip = new_filter["ip"]
if not current_filters[new_ip] then
-- Not an already-added filter
alert_exclusions.disable_flow_alert(new_ip, script.alert_id)
else
-- Filter was already added, nothing to do
current_filters[new_ip] = nil
end
end
-- See above for flows
for current_ip, _ in pairs(current_filters or {}) do
alert_exclusions.enable_flow_alert(current_ip, script.alert_id)
end
end
elseif 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
configset["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 saveConfigset(configset)
end
-- ##############################################
-- @brief Toggles script `script_key` configuration on or off depending on `enable` for configuration `configset`
-- Hooks onDisable and onEnable are called.
-- @param configset A user script configuration, obtained with user_scripts.getConfigset()
-- @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, 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)
elseif script.onEnable and not prev_hook_config and enable then
-- Hook has been disabled for the user script
script.onEnable(hook, hook_config)
end
end
end
if not configset["config"][subdir][script_key] then
configset["config"][subdir][script_key] = {}
configset["config"][subdir][script_key] = config
end
return true
end
-- ##############################################
function user_scripts.toggleScript(script_key, subdir, enable)
local configset = user_scripts.getConfigset()
-- Toggle the configuration (result is put in `configset`)
local res, err = toggleScriptConfigset(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 saveConfigset(configset)
end
-- ##############################################
function user_scripts.toggleAllScripts(subdir, enable)
local configset = user_scripts.getConfigset()
-- 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(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 saveConfigset(configset)
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
-- ##############################################
-- @brief Migrate old configurations, if any
function user_scripts.migrateOldConfig()
-- Check if there is a v3 already
local configset_v3 = ntop.getCache(CONFIGSET_KEY)
if isEmptyString(configset_v3) then
-- Check if there is a v2
local CONFIGSETS_KEY_V2 = "ntopng.prefs.user_scripts.configsets_v2"
local configsets_v2 = ntop.getHashAllCache(CONFIGSETS_KEY_V2)
if configsets_v2 then
-- Migrate v2 to v3
local default_confset_json = configsets_v2["0"]
if default_confset_json then
ntop.setCache(CONFIGSET_KEY, default_confset_json)
end
-- Remove v2
ntop.delCache(CONFIGSETS_KEY_V2)
end
end
end
-- ##############################################
-- @brief Initializes a default configuration for user scripts
-- @param overwrite If true, a possibly existing configuration is overwritten with default values
function user_scripts.initDefaultConfig()
local ifid = getSystemInterfaceId()
-- Current (possibly not-existing, not yet created configset)
local configset = user_scripts.getConfigset() or {}
-- Default per user-script configuration
local default_conf = configset.config or {}
-- Default per user-script filters
local default_filters = configset.filters or {}
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
if usermod.filter and usermod.filter.default_filters then
default_filters[subdir] = default_filters[subdir] or {}
if not default_filters[subdir][key] then
-- Do not override filter of an existing configuration
default_filters[subdir][key] = usermod.filter.default_filters
end
end
end
end
end
-- This is the new configset with all defaults
local configset = {
config = default_conf,
filters = default_filters,
}
saveConfigset(configset)
end
-- ##############################################
function user_scripts.resetConfigset()
cached_config_set = nil
ntop.delCache(CONFIGSET_KEY)
user_scripts.initDefaultConfig()
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 configset = user_scripts.getConfigset()
local default_config = user_scripts.getConfig(configset, "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 or {}) 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 `confset_id` from all the available `configsets` passed
function user_scripts.getConfig(configset, subdir)
if configset and configset["config"] and configset["config"][subdir] then
return configset["config"][subdir]
end
return {}, nil
end
-- ##############################################
-- @brief Retrieve `subdir` filters from the configset
function user_scripts.getFilters(configset, subdir)
if configset and configset["filters"] and configset["filters"][subdir] then
return configset["filters"][subdir]
end
return {}, nil
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 subdir = alert_generation["subdir"]
local subdir_id = getSubdirId(subdir)
if subdir_id == -1 then
return ''
end
if not available_subdirs[subdir_id]["filter"] then
return ''
end
local filter_to_use = {}
if 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, obtained with user_scripts.getConfigset()
-- @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(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.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, script_key, subdir)
local configset = user_scripts.getConfigset()
-- Getting the configuration
local config = configset["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: '&lt;',
next: '&gt;',
first: '«',
last: '»'
},
},
});
});
</script>
]])
end
-- ##############################################
return(user_scripts)