ntopng/scripts/lua/modules/inactive_hosts_utils.lua
2024-01-12 11:44:18 +01:00

637 lines
20 KiB
Lua

--
-- (C) 2013-24 - ntop.org
--
local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path
require("lua_utils")
local json = require("dkjson")
local discover = require "discover_utils"
local OFFLINE_LOCAL_HOSTS_KEY = "ntopng.hosts.offline.ifid_%s"
local inactive_hosts_utils = {}
-- ##########################################
-- Function used to check if the host has to be filtered out or not
-- return true in case the host is okey, false in case it has to be filtered out
local function check_filters(host_info, filters)
local mac_manufacturer = ntop.getMacManufacturer(host_info.mac) or {}
local filters_ok = true
for filter, value in pairs(filters or {}) do
if filter == "manufacturer" then
if mac_manufacturer.extended ~= value then
filters_ok = false
goto skip
end
elseif tostring(host_info[filter]) ~= tostring(value) then
filters_ok = false
goto skip
end
end
::skip::
return filters_ok
end
-- ##########################################
-- This function return a list of inactive hosts, with all the informations
function inactive_hosts_utils.getInactiveHosts(ifid, filters)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
local networks_stats = interface.getNetworksStats()
local host_list = {}
for redis_key, _ in pairs(available_keys) do
local host_info_json = ntop.getHashCache(redis_hash, redis_key)
local network_name = ""
if not isEmptyString(host_info_json) then
local host_info = json.decode(host_info_json)
local mac_manufacturer = ntop.getMacManufacturer(host_info.mac) or {}
local mac_manufacturer_label = ""
if not check_filters(host_info, filters) then
goto skip
end
for n, ns in pairs(networks_stats) do
if ns.network_id == tonumber(host_info.network) then
network_name = getFullLocalNetworkName(ns.network_key)
end
end
if mac_manufacturer and not isEmptyString(mac_manufacturer.extended) then
mac_manufacturer_label = mac_manufacturer.extended
end
host_list[#host_list + 1] = {
ip_address = host_info.ip,
mac_address = host_info.mac,
host = host_info.ip .. "@" .. host_info.vlan,
vlan = getFullVlanName(host_info.vlan),
vlan_id = host_info.vlan,
name = host_info.name,
last_seen = host_info.last_seen,
first_seen = host_info.first_seen,
epoch_end = host_info.last_seen,
epoch_begin = host_info.first_seen,
device_id = host_info.device_type,
device_type = discover.devtype2string(host_info.device_type),
network_id = host_info.network,
network = network_name,
serial_key = redis_key,
manufacturer = mac_manufacturer_label
}
end
::skip::
end
return host_list
end
-- ##########################################
-- This function return the info of a specific inactive host, given the serialization (redis) key
function inactive_hosts_utils.getInactiveHostInfo(ifid, serial_key)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local host_info_json = ntop.getHashCache(redis_hash, serial_key)
if not isEmptyString(host_info_json) then
local host_info = json.decode(host_info_json)
return host_info
end
return nil
end
-- ##########################################
-- Return the inactive host number
function inactive_hosts_utils.getInactiveHostsNumber(ifid, filters)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
local count = 0
if filters then
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
for redis_key, _ in pairs(available_keys) do
local host_info_json = ntop.getHashCache(redis_hash, redis_key)
if not isEmptyString(host_info_json) then
local host_info = json.decode(host_info_json)
local last_seen = host_info.last_seen
-- Exclude those that do not follow the filters
if not check_filters(host_info, filters) then
goto skip
end
count = count + 1
::skip::
end
end
else
count = table.len(available_keys)
end
return count
end
-- ##########################################
-- This function return a list of available VLAN filters
function inactive_hosts_utils.getVLANFilters(ifid, filters)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
local vlan_list = {}
local rsp = {}
for redis_key, _ in pairs(available_keys) do
local host_info_json = ntop.getHashCache(redis_hash, redis_key)
if not isEmptyString(host_info_json) then
local host_info = json.decode(host_info_json)
-- Exclude those that do not follow the filters
if not check_filters(host_info, filters) then
goto skip
end
if not (vlan_list[host_info.vlan]) then
vlan_list[host_info.vlan] = 1
else
vlan_list[host_info.vlan] = vlan_list[host_info.vlan] + 1
end
::skip::
end
end
-- Add the vlan names
for vlan, count in pairsByKeys(vlan_list) do
local vlan_name = ''
if vlan == 0 then
if table.len(vlan_list) == 1 then
break
end
vlan_name = i18n('no_vlan')
else
vlan_name = getFullVlanName(vlan)
end
rsp[#rsp + 1] = {
count = count,
key = "vlan_id",
value = vlan,
label = tostring(vlan_name)
}
end
-- Add the "all" entry
table.insert(rsp, 1, {
key = "vlan_id",
value = "",
label = i18n('all')
})
return rsp
end
-- ##########################################
-- This function return a list of available network filters
function inactive_hosts_utils.getNetworkFilters(ifid, filters)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
local networks_stats = interface.getNetworksStats()
local network_list = {}
local rsp = {}
for redis_key, _ in pairs(available_keys) do
local host_info_json = ntop.getHashCache(redis_hash, redis_key)
local network_name = ""
if not isEmptyString(host_info_json) then
local host_info = json.decode(host_info_json)
-- Exclude those that do not follow the filters
if not check_filters(host_info, filters) then
goto skip
end
if not (network_list[host_info.network]) then
network_list[host_info.network] = 1
else
network_list[host_info.network] = network_list[host_info.network] + 1
end
::skip::
end
end
-- Format the networks name
for network, count in pairsByKeys(network_list) do
local network_name
for n, ns in pairs(networks_stats) do
if ns.network_id == tonumber(network) then
network_name = getFullLocalNetworkName(ns.network_key)
end
end
rsp[#rsp + 1] = {
count = count,
key = "network",
value = network,
label = tostring(network_name or network)
}
end
-- Add the "all" entry
table.insert(rsp, 1, {
key = "network",
value = "",
label = i18n('all')
})
return rsp
end
-- ##########################################
-- This function return a list of available device filters
function inactive_hosts_utils.getDeviceFilters(ifid, filters)
local discover_utils = require "discover_utils"
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
local device_list = {}
local rsp = {}
for redis_key, _ in pairs(available_keys) do
local host_info_json = ntop.getHashCache(redis_hash, redis_key)
if not isEmptyString(host_info_json) then
local host_info = json.decode(host_info_json)
local dev_name = discover_utils.devtype2string(host_info.device_type)
-- Exclude those that do not follow the filters
if not check_filters(host_info, filters) then
goto skip
end
if not (device_list[dev_name]) then
device_list[dev_name] = {
count = 1,
type = host_info.device_type
}
else
device_list[dev_name].count = device_list[dev_name].count + 1
end
::skip::
end
end
for device, info in pairsByKeys(device_list) do
rsp[#rsp + 1] = {
count = info.count,
key = "device_type",
value = info.type,
label = device
}
end
-- Add "all" entry
table.insert(rsp, 1, {
key = "device_type",
value = "",
label = i18n('all')
})
return rsp
end
-- ##########################################
-- This function return a list of available manufacturer filters
function inactive_hosts_utils.getManufacturerFilters(ifid, filters)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
local manufacturer_list = {}
local rsp = {}
for redis_key, _ in pairs(available_keys) do
local host_info_json = ntop.getHashCache(redis_hash, redis_key)
if not isEmptyString(host_info_json) then
local host_info = json.decode(host_info_json)
local mac_manufacturer = ntop.getMacManufacturer(host_info.mac) or {}
local tmp = mac_manufacturer.extended
if isEmptyString(tmp) then
tmp = mac_manufacturer.short
end
-- Exclude those that do not follow the filters
if not check_filters(host_info, filters) then
goto skip
end
if tmp then
if not (manufacturer_list[tmp]) then
manufacturer_list[tmp] = 1
else
manufacturer_list[tmp] = manufacturer_list[tmp] + 1
end
end
::skip::
end
end
for manufacturer, count in pairsByKeys(manufacturer_list) do
rsp[#rsp + 1] = {
count = count,
key = "manufacturer",
value = manufacturer,
label = manufacturer
}
end
table.insert(rsp, 1, {
key = "manufacturer",
value = "",
label = i18n('all')
})
return rsp
end
-- ##########################################
function inactive_hosts_utils.deleteAllEntries(ifid)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
local num_hosts_deleted = table.len(available_keys)
for redis_key, _ in pairs(available_keys) do
ntop.delHashCache(redis_hash, redis_key)
end
return num_hosts_deleted
end
-- ##########################################
function inactive_hosts_utils.deleteAllEntriesSince(ifid, epoch)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
local num_hosts_deleted = 0
for redis_key, _ in pairs(available_keys) do
local host_info_json = ntop.getHashCache(redis_hash, redis_key)
if isEmptyString(host_info_json) then
ntop.delHashCache(redis_hash, redis_key)
num_hosts_deleted = num_hosts_deleted + 1
end
local host_info = json.decode(host_info_json)
if host_info.last_seen < epoch then
num_hosts_deleted = num_hosts_deleted + 1
ntop.delHashCache(redis_hash, redis_key)
end
end
return num_hosts_deleted
end
-- ##########################################
function inactive_hosts_utils.deleteSingleEntry(ifid, redis_key)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
ntop.delHashCache(redis_hash, redis_key)
return 1 -- Number of hosts deleted
end
-- ##########################################
function inactive_hosts_utils.formatInactiveHosts(hosts, no_html)
local format_utils = require("format_utils")
local discover_utils = require "discover_utils"
-- Format the values to be used by the front end application
for key, value in pairs(hosts) do
local url = nil
hosts[key]["last_seen"] = format_utils.formatPastEpochShort(value["last_seen"])
hosts[key]["first_seen"] = format_utils.formatPastEpochShort(value["first_seen"])
hosts[key]["manufacturer"] = value["manufacturer"]
-- If available, add url and extra info
local mac_info = interface.getMacInfo(value["mac_address"])
if mac_info then
url = mac2url(value["mac_address"])
if no_html then
url = nil
end
hosts[key]["mac_address"] = {
name = mac2label(value["mac_address"]),
value = value["mac_address"],
url = url
}
end
if interface.getNetworkStats(hosts[key]["network_id"]) then
url = '/lua/hosts_stats.lua?network=' .. hosts[key]["network_id"]
if no_html then
url = nil
end
hosts[key]["network"] = {
name = hosts[key]["network"],
value = hosts[key]["network_id"],
url = url
}
end
url = '/lua/inactive_host_details.lua?serial_key=' .. hosts[key]["serial_key"]
local device_type = discover_utils.devtype2icon(value["device_id"])
if no_html then
url = nil
device_type = nil
end
hosts[key]["host"] = {
ip_address = {
name = hosts[key]["ip_address"],
value = hosts[key]["ip_address"],
url = url
},
device_type = device_type,
device_name = discover_utils.devtype2string(value["device_id"])
}
if hosts[key]["vlan_id"] then
hosts[key]["host"]["vlan"] = {
name = hosts[key]["vlan"],
value = hosts[key]["vlan_id"]
}
if interface.getVLANInfo(hosts[key]["vlan_id"]) and not no_html then
hosts[key]["host"]["vlan"]["url"] = '/lua/hosts_stats.lua?vlan=' .. hosts[key]["vlan_id"]
end
end
hosts[key]["device_id"] = nil
hosts[key]["network_id"] = nil
hosts[key]["vlan_id"] = nil
if no_html then
hosts[key].epoch_end = nil
hosts[key].epoch_begin = nil
hosts[key].serial_key = nil
hosts[key].vlan = nil
hosts[key].name = nil
end
end
return hosts
end
-- ##########################################
function inactive_hosts_utils.formatInactiveHostsCSV(hosts)
local formatted_hosts = inactive_hosts_utils.formatInactiveHosts(hosts, true --[[ No HTML ]])
local column_names = "|"
local csv_formatted = ""
for key, value in pairs(formatted_hosts) do
local tmp_value
if value.mac_address then
tmp_value = value.mac_address.value
formatted_hosts[key].mac_address = tmp_value
end
tmp_value = value.network.name
formatted_hosts[key].network = tmp_value
tmp_value = value.host.ip_address.value
formatted_hosts[key].ip_address = tmp_value
tmp_value = value.host.ip_address.name
formatted_hosts[key].hostname = tmp_value
tmp_value = value.host.device_name
formatted_hosts[key].device_type = tmp_value
tmp_value = ""
if value.host.vlan then
tmp_value = value.host.vlan.name or value.host.vlan.value
end
formatted_hosts[key].vlan = tmp_value
value.host = nil
column_names = "|"
local concat = "|"
for column, data in pairsByKeys(value) do
column_names = column_names .. string.upper(column) .. "|"
concat = concat .. data .. "|"
end
csv_formatted = csv_formatted .. concat .. "\n"
end
return column_names .. "\n" .. csv_formatted
end
-- ##########################################
function inactive_hosts_utils.formatInactiveHostsJSON(hosts)
local json = require("dkjson")
local formatted_hosts = inactive_hosts_utils.formatInactiveHosts(hosts, true --[[ No HTML ]])
return json.encode(formatted_hosts or {})
end
-- ##########################################
-- Return the distribution of inactive hosts on epoch basis:
-- - hour
-- - day
-- - week
-- - older
function inactive_hosts_utils.getInactiveHostsEpochDistribution(ifid, filters)
local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
local available_keys = ntop.getHashKeysCache(redis_hash) or {}
local epoch_list = {
last_hour = 0,
last_day = 0,
last_week = 0,
older = 0,
}
local now = os.time() -- current epoch
local one_hour_epoch = now - 3600
local one_day_epoch = now - 86400
local one_week_epoch = now - 604800
for redis_key, _ in pairs(available_keys) do
local host_info_json = ntop.getHashCache(redis_hash, redis_key)
local network_name = ""
if not isEmptyString(host_info_json) then
local host_info = json.decode(host_info_json)
local last_seen = host_info.last_seen
-- Exclude those that do not follow the filters
if not check_filters(host_info, filters) then
goto skip
end
if last_seen >= one_hour_epoch then
-- newer then one hour
epoch_list.last_hour = epoch_list.last_hour + 1
elseif last_seen >= one_day_epoch then
-- newer then one day but older then one hour
epoch_list.last_day = epoch_list.last_day + 1
elseif last_seen >= one_week_epoch then
-- newer then one week but older then one day
epoch_list.last_week = epoch_list.last_week + 1
else
-- Older then one week
epoch_list.older = epoch_list.older + 1
end
::skip::
end
end
return epoch_list
end
-- ##########################################
function inactive_hosts_utils.getFilters()
local filters = {
vlan = _GET["vlan_id"],
network = _GET["network"],
device_type = _GET["device_type"],
manufacturer = _GET["manufacturer"],
}
-- Return the data
for filter, value in pairs(filters) do
if isEmptyString(value) then
filters[filter] = nil
end
end
return filters
end
-- ##########################################
return inactive_hosts_utils