--
-- (C) 2017-24 - ntop.org
--
local clock_start = os.clock()

local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path

local http_lint = {}

local json = require "dkjson"
local tracker = require "tracker"
local locales_utils = require "locales_utils"
local validation_utils = require "validation_utils"
require "ntop_utils"
require "label_utils"

-- #################################################################

-- TRACKER HOOK (ntop.*, interface.*)

tracker.track_ntop()
tracker.track_interface()

-- #################################################################

-- UTILITY FUNCTIONS

function starts(String, Start)
    if ((String == nil) or (Start == nil)) then
        return (false)
    end

    return string.sub(String, 1, string.len(Start)) == Start
end

-- Searches into the keys of the table
local function validateChoiceByKeys(defaults, v)
    if defaults[v] ~= nil then
        return true
    else
        return false
    end
end

-- Placeholder for pcap file validation
local function validatePcap(defaults, v)
    return true
end

-- Searches into the value of the table
-- Optional key can be used to access fields of the array element
local function validateChoice(defaults, v, key)
    for _, d in pairs(defaults) do
        if key ~= nil then
            if d[key] == v then
                return true
            end
        else
            if d == v then
                return true
            end
        end
    end

    return false
end

local function validateChoiceInline(choices)
    return function(choice)
        if (validateChoice(choices, choice)) then
            return true
        else
            return false
        end
    end
end
http_lint.validateChoiceInline = validateChoiceInline

local function validateListOfType(l, validate_callback, separator)
    local separator = separator or ","

    if (isEmptyString(l) or (l == "-1")) then
        return true
    end

    local items = split(l, separator)

    for _, item in pairs(items) do
        if item and item ~= "" then
            if not validate_callback(item) then
                return false
            end
        end
    end

    return true
end

local function validateEmpty(s)
    if s == "" then
        return true
    else
        return false
    end
end

local function validateEmptyOr(other_validation)
    return function(s)
        if (validateEmpty(s) or other_validation(s)) then
            return true
        else
            return false
        end
    end
end
http_lint.validateEmptyOr = validateEmptyOr

-- #################################################################

-- FRONT-END VALIDATORS

local function validateSNMPIfidNumber(p)
    -- integer SNMP Ifid validation

    if p == "*" then
        return true
    end

    local num = tonumber(p)

    if (num == nil) then
        return false
    end

    if math.floor(num) == num then
        return true
    else
        -- this is a float number
        return false
    end
end
http_lint.validateSNMPIfidNumber = validateSNMPIfidNumber

-- #################################################################

-- FRONT-END VALIDATORS

local function validateNumber(p)
    -- integer number validation
    local num = tonumber(p)

    if (num == nil) then
        return false
    end

    if math.floor(num) == num then
        return true
    else
        -- this is a float number
        return false
    end
end
http_lint.validateNumber = validateNumber

-- #################################################################

-- FRONT-END VALIDATORS

local function validateVlan(p)
    -- integer number validation
    if (validateNumber(p)) then
        local num = tonumber(p)
        if (-1 <= num and num < 4095) then
            return true
        end
    end

    return false
end
http_lint.validateVlan = validateVlan

-- ##############################################

local function validateFloat(p)
    -- integer number validation
    local num = tonumber(p)

    if (num == nil) then
        return false
    end

    return true
end

-- ##############################################

local function validateSyslogFormat(p)
    if p == "plaintext" or p == "plaintextrfc" or p == "json" or p == "ecs" or p == "checkmk" then
        return true
    end

    return false
end
http_lint.validateSyslogFormat = validateSyslogFormat

-- ##############################################

local function validateHostInfoType(p)
    if p == "minimal_one_host" or p == "minimal_all_host" or p == "more_one_host" or p == "more_all_host" then
        return true
    end

    return false
end

local function validateFlowType(p)
    if p == "peers" or p == "description" then
        return true
    end

    return false
end

local function validateInterfaceType(p)
    if p == "show" then
        return true
    end

    return false
end

local function validatePort(p)
    local items = split(p, "/")

    if (items == nil) then
        return false
    else
        p = items[1]

        if (items[2] ~= nil) then
            if ((items[2] == "tcp") or (items[2] == "udp")) then
                -- Good: example 80/tcp
            else
                return false
            end
        end
    end

    if not validateNumber(p) then
        return false
    end

    local n = tonumber(p)
    if ((n ~= nil) and (n >= 0) and (n <= 65535)) then
        return true
    else
        return false
    end
end
http_lint.validatePort = validatePort

-- ##############################################
local function validateASN(p)
    -- Unique case in which the asn has 'No ASN' as value
    return (validateNumber(p) or p == "No ASN")
end

local function validateUnquoted(p)
    -- This function only verifies that value does not contain single quotes, but
    -- does not perform any type validation, so it should be used with care.
    -- Double quotes are already handled by the C side.
    if (string.find(p, "'") ~= nil) then
        return false
    else
        return true
    end
end
http_lint.validateUnquoted = validateUnquoted

local function validateLuaScriptPath(p)
    local os_utils = require("os_utils")

    if (string.find(p, "'") ~= nil) then
        return false
    end
    return (starts(p, os_utils.getPathDivider() .. "scripts"))
end
http_lint.validateLuaScriptPath = validateLuaScriptPath

local function validateUploadedFile(p)
    local os_utils = require("os_utils")
    local dirs = ntop.getDirs()

    return (starts(p, dirs.workingdir .. os_utils.getPathDivider() .. "tmp" .. os_utils.getPathDivider() .. "upload"))
end
http_lint.validateLuaScriptPath = validateLuaScriptPath

local function validateUnchecked(p)
    -- This function does not perform any validation, so only the C side validation takes place.
    -- In particular, single quotes are allowed so they must be handled explicitly by the programmer in
    -- order to avoid injection.
    return true
end
http_lint.validateUnchecked = validateUnchecked

local function validateAuthProto(p)
    return (p == "pap" or p == "chap")
end
http_lint.validateAuthProto = validateAuthProto

local function validateSingleWord(w)
    if (string.find(w, "% ") ~= nil) then
        return false
    else
        return validateUnquoted(w)
    end
end
http_lint.validateSingleWord = validateSingleWord

-- ##############################################

-- @brief Returns true if inputstr is inside alert, function used to check
--        if the filter is right or not
local function validateScriptFilter(inputstr)
    inputstr = inputstr:gsub(" ", "")
    return validateListOfType(inputstr, validateSingleWord)
end
http_lint.validateScriptFilter = validateScriptFilter

-- ##############################################

local function validateMessage(w)
    return true
end
http_lint.validateSingleWord = validateMessage

local function validateSingleAlphanumericWord(w)
    if (w:match("%W")) then
        return false
    else
        return validateSingleWord(w)
    end
end

local function validateTrafficRecordingProvider(w)
    if w == "ntopng" or (w:starts("n2disk@") and validateSingleWord(w)) then
        return true
    end

    return false
end

local function validateUsername(p)
    -- A username (e.g. used in ntopng authentication)
    return (validateSingleWord(p))
end

local function licenseCleanup(p)
    return p -- don't touch passwords (checks against valid fs paths already performed)
end

local function passwordCleanup(p)
    return p -- don't touch passwords (checks against valid fs paths already performed)
end

local function webhookCleanup(p)
    local allowed_prefixes = {"https://", "http://"}

    for _, prefix in pairs(allowed_prefixes) do
        if p and p:match("^" .. prefix) then
            -- Only allow the prefix to go through unpurified
            local purified = prefix .. ntop.httpPurifyParam(p:gsub("^" .. prefix, ''))

            return purified
        end
    end

    -- If there's no matching prefix, purify everything
    return ntop.httpPurifyParam(p)
end
http_lint.webhookCleanup = webhookCleanup

local function jsonCleanup(json_payload)
    -- can't touch the json payload or it could be broken
    return json_payload
end

local function whereCleanup(p)
    -- SQL where
    -- A-Za-z0-9!=<>()
    return (p:gsub('%W><!()', '_'))
end

-- NOTE: keep in sync with getLicensePattern()
local function validateLicense(p)
    return string.match(p, "[%l%u%d/+=]+") == p or validateEmpty(p)
end

local function validatePassword(p)
    -- A password (e.g. used in ntopng authentication)
    return true
end

local function validateAbsolutePath(p)
    -- An absolute path. Let it pass for now
    return validateUnquoted(p)
end

local function validateNumMinutes(m)
    return (m == "custom") or validateNumber(m)
end

local function validateJSON(j)
    if (j == "") then
        return true
    end
    return (json.decode(j) ~= nil)
end

local function validateMeasurement(p)
    --[[ FIXX include loop check (pragma_once_checks)
      local am_utils = require "am_utils"

      if am_utils then
      local available_measurements = am_utils.getMeasurementsInfo()

      return(available_measurements[p] ~= nil)
      end

      return(false)
   --]]
    return validateSingleWord(p)
end

-- #################################################################

local function validateOnOff(mode)
    local modes = {"on", "off"}

    return validateChoice(modes, mode)
end

local function validateMode(mode)
    local modes = {"all", "local", "local_no_tx", "local_no_tcp_tx", "remote", "remote_no_tx", "remote_no_tcp_tx",
                   "broadcast_domain", "filtered", "blacklisted", "dhcp", "restore", "client_duration",
                   "server_duration", "client_frequency", "server_frequency", "broadcast_multicast"}

    return validateChoice(modes, mode)
end

local function validateDashboardMode(mode)
    local modes = {"community", "pro", "enterprise"}

    return validateChoice(modes, mode)
end

local function validateOperator(mode)
    local modes = {"gt", "eq", "lt"}

    return validateChoice(modes, mode)
end

http_lint.validateOperator = validateOperator

local function validateAlertValue(value)
    return validateEmpty(value) or validateNumber(value) or validateOnOff(value)
end

local function validateHttpMode(mode)
    local modes = {"responses", "queries"}

    return validateChoice(modes, mode)
end

local function validateEBPFData(mode)
    local modes = {"categories", "breeds", "applications", "processes"}

    return validateChoice(modes, mode)
end

local function validateErrorsFilter(mode)
    local modes = {"errors", "discards", "errors_or_discards"}

    return validateChoice(modes, mode)
end

local function validatePidMode(mode)
    local modes = {"l4", "l7", "host", "apps"}

    return validateChoice(modes, mode)
end

local function validateNdpiStatsMode(mode)
    local modes = {"sinceStartup", "count", "host"}

    return validateChoice(modes, mode)
end

local function validateDeviceResponsiveness(r)
    local modes = {"all", "responsive", "unresponsive"}

    return validateChoice(modes, r)
end

local function validateCounterSince(mode)
    local modes = {"actual", "absolute"}

    return validateChoice(modes, mode)
end

local function validateSflowDistroMode(mode)
    local modes = {"host", "process", "user"}

    return validateChoice(modes, mode)
end

local function validateSflowDistroType(mode)
    local modes = {"size", "memory", "bytes", "latency", "server", "ipver"}

    return validateChoice(modes, mode)
end

local function validateSflowFilter(mode)
    local modes = {"All", "Client", "Server", "recv", "sent"}

    return validateChoice(modes, mode)
end

local function validateIfaceLocalStatsMode(mode)
    local modes = {"distribution"}

    return validateChoice(modes, mode)
end

local function validateProcessesStatsMode(mode)
    local modes = {"table", "timeline"}

    return validateChoice(modes, mode)
end

local function validateDirection(mode)
    local modes = {"sent", "recv"}

    return validateChoice(modes, mode)
end

local function validateSendersReceivers(mode)
    local modes = {"senders", "receivers"}

    return validateChoice(modes, mode)
end

local function validateFingerprintType(ft)
    local fingerprint_types = {"ja4", "hassh"}

    return validateChoice(fingerprint_types, ft)
end

local function validateClientOrServer(mode)
    local modes = {"client", "server"}

    return validateChoice(modes, mode)
end

local function validateBroadcastUnicast(mode)
    local modes = {"unicast", "broadcast_multicast", "one_way", "one_way_unicast", "one_way_broadcast_multicast",
                   "bidirectional"}

    return validateChoice(modes, mode)
end

local function validateAlertTypeNumber(status)
    if not validateNumber(status) then
        return false
    end

    local num = tonumber(status)
    return ((num >= 0) and (num < 2 ^ 32))
end

local function validateAlertType(mode)
    local modes = {"normal", "alerted", "filtered", "periodic"}

    if validateAlertTypeNumber(mode) then
        return true
    end

    return validateChoice(modes, mode)
end

local function validateAlertFamily(family)
    local modes = {"active_monitoring", "flow", "host", "interface", "mac", "network", "snmp", "system", "user"}

    if validateAlertTypeNumber(family) then
        return true
    end

    return validateChoice(modes, family)
end

local function validateAlertTypeSeverity(mode)
    local modes = {"group_none", "notice_or_lower", "warning", "error", "critical", "emergency"}

    return validateChoice(modes, mode)
end

local function validateAlertTypeAndSeverity(mode)
    return validateAlertType(mode) or validateAlertTypeSeverity(mode)
end

local function validateTCPFlowState(mode)
    local modes = {"established", "connecting", "closed", "reset"}

    return validateChoice(modes, mode)
end

local function validatePolicyPreset(mode)
    local modes = {"children", "business", "no_obfuscation", "walled_garden"}

    return validateChoice(modes, mode)
end

local function validateAssetFamily(mode)
    local modes = {"gateway", "dns", "ntp", "imap", "smtp", "pop", "http", "ssh", "rdp"}

    return validateChoice(modes, mode)
end

local function validateStatsType(mode)
    local modes = {"severity_pie", "type_pie", "count_sparkline", "top_origins", "top_targets", "duration_pie",
                   "longest_engaged", "counts_pie", "counts_plain", "top_talkers", "top_applications"}

    return validateChoice(modes, mode)
end

local function validateAlertStatsType(mode)
    local modes = {"severity_pie", "type_pie", "count_sparkline", "top_origins", "top_targets", "duration_pie",
                   "longest_engaged", "counts_pie", "counts_plain"}

    return validateChoice(modes, mode)
end

local function validateFlowHostsType(mode)
    local modes = {"local_only", "remote_only", "local_origin_remote_target", "remote_origin_local_target", "all_hosts"}

    return validateChoice(modes, mode)
end

local function validateConsolidationFunction(mode)
    local modes = {"AVERAGE", "MAX", "MIN"}

    return validateChoice(modes, mode)
end

local function validateAlertStatus(mode)
    local modes = {"engaged", "historical", "historical-flows"}

    return validateChoice(modes, mode)
end

local function validateAggregation(mode)
    local modes = {"ndpi", "l4proto", "port"}

    return validateChoice(modes, mode)
end

local function validateReportMode(mode)
    local modes = {"daily", "weekly", "monthly"}

    return validateChoice(modes, mode)
end

local function validatenEdgeAction(mode)
    local modes = {"discard", "make_permanent"}

    return validateChoice(modes, mode)
end

local function validateFavouriteAction(mode)
    local modes = {"set", "get", "del", "del_all"}

    return validateChoice(modes, mode)
end

local function validateViewPreferences(view)
    local views = {"simple", "expert"}
    return validateChoice(views, view)
end

local function validateFavouriteType(mode)
    local modes = {"apps_per_host_pair", "top_applications", "talker", "app", "host_peers_by_app"}

    return validateChoice(modes, mode)
end

local function validateAjaxFormat(mode)
    local modes = {"d3", "pie"}

    return validateChoice(modes, mode)
end

local function validatePrintFormat(mode)
    local modes = {"txt", "json", "csv"}

    return validateChoice(modes, mode)
end

local function validateResetStatsMode(mode)
    local modes = {"reset_drops", "reset_all", "reset_blacklisted"}

    return validateChoice(modes, mode)
end

local function validateFlowMode(mode)
    local modes = {"topk", "flows"}

    return validateChoice(modes, mode)
end

local function validateDevicesMode(mode)
    local modes = {"source_macs_only", "dhcp_macs_only", "inactive_macs_only"}

    return validateChoice(modes, mode)
end

local function validateDeviceType(tp)
    local discover = require("discover_utils")

    return (discover.isValidDevtype(tp))
end

local function validateUnassignedDevicesMode(mode)
    local modes = {"active_only", "inactive_only"}

    return validateChoice(modes, mode)
end

local function validateSnmpAction(mode)
    local modes = {"delete", "delete_all", "add", "addNewDevice", "startPolling", "prune", "ping_all"}

    return validateChoice(modes, mode)
end

local function validateSnmpLevel(level)
    local levels = {"authPriv", "authNoPriv", "noAuthNoPriv"}
    return validateChoice(levels, level)
end

local function validateSnmpAuthProtocol(protocol)
    local protocols = {"md5", "sha","sha256","sha384","sha512"}
    return validateChoice(protocols, protocol)
end

local function validateSnmpPrivacyProtocol(protocol)
    local protocols = {"des", "aes", "aes128"}
    return validateChoice(protocols, protocol)
end

local function validateExtractionJobAction(mode)
    local modes = {"delete", "stop"}

    return validateChoice(modes, mode)
end

local function validateUserRole(mode)
    local modes = {"administrator", "unprivileged", "captive_portal"}

    return validateChoice(modes, mode)
end

local function validateUserLanguage(code)
    local codes = {}
    for _, c in ipairs(locales_utils.getAvailableLocales()) do
        codes[#codes + 1] = c["code"]
    end

    return validateChoice(codes, code)
end

local function validateTimeZoneName(tz)
    local tz_utils = require("tz_utils")

    local timezones = tz_utils.ListTimeZones()
    if timezones then
        for _, t in ipairs(timezones) do
            if tz == t then
                return true
            end
        end
        return false
    end

    -- never reached as timezones are listed in a text file
    return false
end

local function validateNotificationSeverity(tz)
    return validateChoiceInline({"error", "warning", "info"})
end

http_lint.validateNotificationSeverity = validateNotificationSeverity

local function validateIPV4(p)
    return isIPv4(p)
end

local function validateTransformation(t)
    return validateChoice({"none", "aggregate"}, t)
end

local function validateIpAddress(p)
    if (isIPv4(p) or isIPv6(p)) then
        return true
    else
        return false
    end
end
http_lint.validateIpAddress = validateIpAddress

local function validateIpRange(p)
    local range = string.split(p, "%-")

    if not range or #range ~= 2 then
        return false
    end

    return validateIpAddress(range[1]) and validateIpAddress(range[2])
end

local function validateHTTPHost(p)
    -- TODO maybe stricter check?
    if validateSingleWord(p) then
        return true
    else
        return false
    end
end

local function validateIpVersion(p)
    if ((p == "4") or (p == "6")) then
        return true
    else
        return false
    end
end

local function validateServer(v)
    -- thanks to https://stackoverflow.com/questions/35467680/lua-pattern-to-validate-a-dns-address
    if (isEmptyString(v)) then
        return false
    end

    return validateIpAddress(v) or validateSingleWord(v)
end

local function validateHostName(v)
    if (isEmptyString(v)) then
        return false
    end

    if (not validateSingleWord(v)) then
        for str in ipairs(split(v, " ")) do
            if (not validateSingleWord(str)) then
                return false
            end
        end
    end

    return true
end

local function validateColumnsIds(v)
    return validateUnchecked(v)
    -- An Array is composed by a series of string separated by commas
    -- e.g.     column1,column2,column3
    -- So split by comma and check each single column with validateSingleWord
    -- local columns = string.split(v, ",")

    -- if((column == nil) or (type(column) ~= table)) then return false end

    -- for _, column_id in pairs(columns) do
    --     if not validateSingleWord(column_id) then
    --         return false
    --     end
    -- end

    -- return true
end

local function validateDate(p)
    -- TODO this validation function should be removed and dates should always be passed as timestamp
    if string.find(p, ":") ~= nil then
        return true
    else
        return false
    end
end

local function validateMemberRelaxed(m)
    -- This does not actually check the semantic with isValidPoolMember
    -- as this is used in pool deletion to handle bad pool member values
    -- inserted by mistake)
    if validateUnquoted(m) then
        return true
    else
        return false
    end
end

local function validateMember(m)
    if isValidPoolMember(m, true) then
        return true
    else
        return false
    end
end

local function validateMembersFilter(m)
    if starts(m, "manuf:") then
        m = string.sub(m, string.len("manuf:") + 1)
        return validateUnquoted(m)
    end

    return validateMember(m)
end

local function validateIdToDelete(i)
    if ((i == "__all__") or validateNumber(i)) then
        return true
    else
        return false
    end
end

local function validateLocalGlobal(p)
    local values = {"local", "global"}

    return validateChoice(values, p)
end

local function validateBool(p)
    if (isEmptyString(p) or p == "false") or (p == "true") then
        return true
    else
        local n = tonumber(p)
        if ((n == 0) or (n == 1)) then
            return true
        else
            return false
        end
    end
end
http_lint.validateBool = validateBool

local function validateSortOrder(p)
    local defaults = {"asc", "desc"}

    return validateChoice(defaults, p)
end

local ndpi_protos = interface.getnDPIProtocols()
local ndpi_categories = interface.getnDPICategories()

local function validateApplication(app)
    local modes = {"TCP", "UDP"}
    if validateChoice(modes, app) then
        return true
    end

    local dot = string.find(app, "%.")

    if dot ~= nil then
        local master = string.sub(app, 1, dot - 1)
        if not validateChoiceByKeys(ndpi_protos, master) then
            -- try to see if app is just an app with a dot (e.g., Musical.ly)
            return validateChoiceByKeys(ndpi_protos, app)
        else
            -- master is a valid protocol, let's see if the application is valid as well
            local sub = string.sub(app, dot + 1)
            return validateChoiceByKeys(ndpi_protos, sub)
        end
    else
        return validateChoiceByKeys(ndpi_protos, app)
    end
end

local function validateTagsOperator(p)
    local tag_utils = require "tag_utils"
    return (tag_utils.tag_operators[p] ~= nil)
end

local function validateFilters(other_validation)
    return function(s)
        local param = split(s, ";") or split(s, "-")

        if param and #param == 1 then
            param = split(s, "-")
        end

        if param and #param == 2 then
            local tmp = split(param[1], "-")

            if tmp and #tmp == 2 then
                return (other_validation(tmp[1]) and (other_validation(tmp[2])) and (validateTagsOperator(param[2])))
            else
                if isEmptyString(param[1]) then
                    return validateTagsOperator(param[2])
                else
                    return (other_validation(param[1]) and (validateTagsOperator(param[2])))
                end
            end
        end

        -- Note: comma is deprecated, use ';'
        -- Checking comma for backward compatibility
        param = split(s, ",")
        if param and #param == 2 then
            return (other_validation(param[1]) and (validateTagsOperator(param[2])))
        end

        return other_validation(s)
    end
end
http_lint.validateFilters = validateFilters

local L4_PROTO_KEYS = {
    tcp = 6,
    udp = 17,
    icmp = 1,
    eigrp = 88,
    other_ip = -1
}

local function validateProtocolIdOrName(p)
    -- Lower used because TCP instead of tcp wasn't seen as a l4proto
    local tmp = string.lower(p)
    local param = string.split(p, "%.")

    if param and #param == 2 then
        -- Example 5.26
        return (validateChoice(ndpi_protos, param[1]) and validateChoice(ndpi_protos, param[2]))
    else
        return (validateChoice(ndpi_protos, p) or validateChoiceByKeys(L4_PROTO_KEYS, p) or
                   validateChoiceByKeys(ndpi_protos, p)) or
                   (validateChoice(ndpi_protos, tmp) or validateChoiceByKeys(L4_PROTO_KEYS, tmp) or
                       validateChoiceByKeys(ndpi_protos, tmp)) or (p == 'none')
    end
end

http_lint.validateProtocolIdOrName = validateProtocolIdOrName

local function validateDoubleProtocolIdOrName(p)
    local param = string.split(p, "%.")

    if param and #param == 2 then
        return (validateProtocolIdOrName(param[1]) and (validateProtocolIdOrName(param[2])))
    end

    return (validateProtocolIdOrName(p))
end
http_lint.validateProtocolIdOrName = validateProtocolIdOrName

function http_lint.validateTrafficProfile(p)
    return validateUnquoted(p)
end

local function validateSortColumn(p)
    -- Note: this is also used in some scripts to specify the ordering, so the "column_"
    -- prefix is not honored
    if ((validateSingleWord(p)) --[[and (string.starts(p, "column_"))]] ) then
        return true
    else
        return false
    end
end

local function validateCountry(p)
    if string.len(p) == 2 then
        return true
    else
        return validateNumber(p)
    end
end

-- #################################

local function validateStatus(p)
    return (p == 'allowed' or p == 'denied')
end

-- #################################

local function validateInterface(i)
    return interface.isValidIfId(tonumber(i))
end
http_lint.validateInterface = validateInterface

local function validateNetwork(i)
    if not string.find(i, "/") then
        return validateIpAddress(i)
    else
        -- Mask
        local ip_mask = split(i, "/")
        if #ip_mask ~= 2 then
            return false
        end
        local ip = ip_mask[1]
        local mask = ip_mask[2]

        if not validateNumber(mask) then
            return false
        end

        local prefix = tonumber(mask)
        if prefix >= 0 then
            -- IP
            local is_ipv6 = isIPv6(ip)
            local is_ipv4 = isIPv4(ip)

            if is_ipv6 and prefix <= 128 then
                return true
            elseif is_ipv4 and prefix <= 32 then
                return true
            end
        end

        return false
    end
end
http_lint.validateNetwork = validateNetwork

local function validateHost(p)
    local host = hostkey2hostinfo(p)

    if (host.host ~= nil) and (host.vlan ~= nil) and (isIPv4(host.host) or isIPv6(host.host) or isMacAddress(host.host)) then
        return true
    else
        return validateNetwork(p)
    end
end
http_lint.validateHost = validateHost

local function validateNetworkWithVLAN(i)
    if not string.find(i, "/") then
        return validateHost(i)
    else
        -- VLAN
        local net_vlan = split(i, "@")
        local net = net_vlan[1]

        if #net_vlan < 1 then
            return false
        end

        if #net_vlan == 2 then
            local vlan = net_vlan[2]
            if not validateNumber(vlan) then
                return false
            end
        end

        return validateNetwork(net)
    end
end
http_lint.validateNetworkWithVLAN = validateNetworkWithVLAN

local function validateMac(p)
    if isMacAddress(p) then
        return true
    else
        return false
    end
end

-- ##############################################

local function validatePolicy(p)
    local policies = {"accept", "deny"}

    if validateNumber(p) or validateChoice(policies, p) then
        return true
    end

    return false
end
http_lint.validatePolicy = validatePolicy

local function validatePolicyValue(p)
    if validateNetwork(p) or validateSingleWord(p) then
        return true
    end

    return false
end
http_lint.validatePolicyValue = validatePolicyValue

-- ##############################################

-- @brief Returns true if inputstr is a Mac or all string
local function validateDeviceOrAll(inputstr)
    return (validateMac(inputstr) or inputstr == 'all')
end
http_lint.validateDeviceOrAll = validateDeviceOrAll

-- ##############################################

local function validateZoom(zoom)
    if string.match(zoom, "%d+%a") == zoom then
        return true
    else
        return false
    end
end

-- ##############################################

-- @brief Returns true if inputstr is a Mac or all string
local function validateDevice(inputstr)
    return (validateDeviceOrAll(inputstr) or validateIPV4(inputstr))
end
http_lint.validateDevice = validateDevice

-- ##############################################

local function validateCategory(cat)
    if starts(cat, "cat_") then
        local id = split(cat, "cat_")[2]
        return validateNumber(id)
    elseif validateChoiceByKeys(ndpi_categories, cat) then
        return true
    else
        return validateNumber(cat)
    end

    return false
end
http_lint.validateCategory = validateCategory

local function validateProtocolOrCategory(p)
    return validateProtocolIdOrName(p) or validateCategory(p)
end

local function validateProtocolIdOrNameorCategory(p)
    if string.starts(p, "cat_") then
        local category = split(p, "cat_")
        return validateCategory(category[2])
    end
    return validateProtocolIdOrName(p)
end

local function validateFlowHostsTypeOrIpVersionOrIp(p)
    if string.starts(p, "ip_version_") then
        local version = split(p, "ip_version_")
        return validateIpVersion(version[2])
    else
        local is_host_type = validateFlowHostsType(p)
        local host = hostkey2hostinfo(p)
        if host and not is_host_type then
            return validateHost(p)
        else
            return is_host_type
        end
    end
end

local function validateShapedElement(elem_id)
    local id
    if starts(elem_id, "cat_") then
        id = split(elem_id, "cat_")[2]
    else
        id = elem_id
    end

    if ((elem_id == "default") or validateNumber(id)) then
        return true
    else
        return false
    end
end

local function validateAlertDescriptor(d)
    return (validateSingleWord(d))
end

local function validateInterfacesList(l)
    return validateListOfType(l, validateInterface)
end

local function validateNetworksList(l)
    return validateListOfType(l, validateNetwork)
end

local function validateNetworksWithVLANList(l)
    return validateListOfType(l, validateNetworkWithVLAN)
end

local function validateMacList(l)
    return validateListOfType(l, validateMac)
end

local function validateACLNetworksList(l)
    -- networks in the ACL are preceeded by a + or a - sign
    -- and are (currently) used for the mongoose webserver ACL
    -- Examples:
    -- +0.0.0.0/0,-192.168.0.0/16
    -- +127.0.0.0/8
    if isEmptyString(l) then
        return true
    end

    local items = split(l, ',')

    -- make sure each item has a leading + or -
    for _, i in pairs(items) do
        if not starts(i, '+') and not starts(i, '-') then
            return false
        end
    end

    -- now we can safely replace + and - and do a normal network validation
    l = l:gsub("+", ""):gsub("-", "")

    return validateListOfType(l, validateNetwork)
end

local function validateCategoriesList(mode)
    return validateListOfType(l, validateCategory)
end

local function validateApplicationsList(l)
    return validateListOfType(l, validateApplication)
end

local function validateHostsList(l)
    return validateListOfType(l, validateHost)
end

local function validateListOfTypeInline(t)
    return function(l)
        return validateListOfType(l, t)
    end
end

local function validateIfFilter(i)
    if validateNumber(i) or i == "all" then
        return true
    else
        return false
    end
end

local function validateCustomColumn(c)
    local custom_column_utils = require("custom_column_utils")

    if validateChoice(custom_column_utils.available_custom_columns, c, 1) then
        return true
    else
        return false
    end
end

local function validateTopModule(m)
    -- TODO check for existence?
    return validateSingleWord(m)
end

local function validateGatewayName(m)
    -- NOTE: no space allowed right now
    return validateSingleWord(m)
end

local function validateStaticRouteName(m)
    -- NOTE: no space allowed right now
    return validateSingleWord(m)
end

local function validateNetworkInterface(m)
    return validateSingleWord(m)
end

local function validateRoutingPolicyName(m)
    return validateUnquoted(m)
end

local function validateNumberOrUnquoted(p)
    return validateNumber(p) or validateUnquoted(p)
end

function validateRoutingPolicyGateway(m)
    -- this is in the form "policyid_gwid"
    local parts = string.split(m, "_")

    if parts and #parts == 2 then
        local policy_id = parts[1]
        local gw_id = parts[2]

        return validateNumber(policy_id) and validateNumber(gw_id)
    end

    return false
end

-- #################################################################

local function validateInterfaceConfMode(m)
    return validateChoice({"dhcp", "static", "vlan_trunk"}, m)
end

-- #################################################################

local function validateAssociations(associations)
    if not associations or not type(associations) == "table" then
        return false
    end

    for k, v in pairs(associations) do
        if not isValidPoolMember(k) then
            -- Try to fix the format (add prefix/vlan if missing)
            local fixedk = fixPoolMemberFormat(k)
            if fixedk and isValidPoolMember(fixedk) then
                associations[fixedk] = v
                associations[k] = nil
            else
                return false
            end
        end
    end

    return true
end

-- #################################################################

local function validateSNMPversion(m)
    -- 0 = SNMP v1
    -- 1 = SNMP v2c
    -- 2 - SNMP v3
    return validateChoice({"0", "1", "2", "3"}, m)
end
http_lint.validateSNMPversion = validateSNMPversion

-- #################################################################

local function validateSNMPstatus(m)
    return validateChoice({"up", "down"}, m) or validateNumber(m)
end

-- #################################################################

local function validatenIndexQueryType(qt)
    return validateChoice({"top_clients", "top_servers", "top_protocols", "top_contacts"}, qt)
end

-- #################################################################

local function validateCIDR(m)
    return validateChoice({"24", "32", "128"}, m)
end

local function validateVsCIDR(m)
    return validateChoice({"24", "25", "26", "27", "28", "29", "30", "32", "128"}, m)
end

local function validateOperatingMode(m)
    return validateChoice({"single_port_router", "routing", "bridging", "passive"}, m)
end

-- #################################################################

function http_lint.parseConfsetTargets(subdir, param)
    local values = string.split(param, ",") or {param}
    local validator = nil

    if ((subdir == "host") or (subdir == "snmp_device") or (subdir == "network")) then
        -- IP addresses/CIDR
        validator = validateNetwork
    elseif ((subdir == "interface") or (subdir == "flow") or (subdir == "syslog")) then
        -- interface ID
        validator = validateInterface
    else
        traceError(TRACE_ERROR, TRACE_CONSOLE, "Unsupported subdir: " .. subdir)
        return nil, "Unsupported subdir"
    end

    for _, v in pairs(values) do
        if (not validator(v)) then
            return nil, i18n("configsets.bad_target", {
                target = v
            })
        end
    end

    return (values)
end

-- #################################################################

function validateSankeyCriteria(criteria)
    return (criteria == 'flow_volume_criteria' or criteria == 'flow_drops_criteria')
end

-- #################################################################

function validateToken(token)
    if (not token) then
        return false
    end
    if (string.len(token) ~= 32) then
        return false
    end
    -- iterate over each byte of the string
    if (token:match("^%x+$") == nil) then
        return false
    end

    return true
end

-- #################################################################

local function validateMattermostBearerToken(token)
    if (not token) then
        return false
    end
    if (string.len(token) ~= 26) then
        return false
    end
    -- iterate over each byte of the string

    return true
end

-- #################################################################

local function validateFieldAlias(key_value_pair)
    -- Validates parameters such as:
    -- packets.sent=tpd
    -- bytes.rcvd=rbd

    local kv = key_value_pair:split("=") or {key_value_pair}

    if #kv == 1 then
        -- Field without alias
        return validateSingleWord(kv[1])
    elseif #kv == 2 then
        -- Field and alias
        return validateSingleWord(kv[1]) and validateSingleWord(kv[2])
    end

    return false
end

-- #################################################################

local function validateListItems(script, conf, key)
    local item_type = script.gui.item_list_type or ""
    local item_validator = validateUnchecked
    local existing_items = {}
    local validated_items = {}
    key = key or "items"
    local conf_items = conf[key]

    if (item_type == "country") then
        item_validator = validateCountry
        err_label = "Bad country"
    elseif (item_type == "proto_or_category") then
        item_validator = validateProtocolOrCategory
        err_label = "Bad protocol/category"
    elseif (item_type == "string") then
        item_validator = validateSingleWord
        err_label = "Bad string"
    elseif (item_type == "device_type") then
        item_validator = validateDeviceType
        err_label = "Bad device type"
    elseif (item_type == "ip_address") then
        item_validator = validateIpAddress
        err_label = "Bad address"
    elseif (item_type == "mac_address") then
        item_validator = validateMac
        err_label = "Bad address"
    elseif (item_type == "server") then
        item_validator = validateServer
        err_label = "Bad server IP or name"
    elseif (item_type == "vlan") then
        item_validator = validateVlan
        err_label = "Bad VLAN ID (must be a value beetwen 0 and 4094)"
    end

    if (type(conf_items) == "table") then
        for _, item in ipairs(conf_items) do
            if existing_items[item] then
                -- Ignore duplicated items
                goto next_item
            end

            if not item_validator(item) then
                return false, err_label .. ": " .. string.format("%s", item)
            end

            existing_items[item] = true
            validated_items[#validated_items + 1] = item

            ::next_item::
        end

        conf[key] = validated_items
    end

    return true, conf
end

http_lint.validateListItems = validateListItems

-- #################################################################

-- NOTE: Put here all the parameters to validate

local known_parameters = {
    -- UNCHECKED (Potentially Dangerous)
    ["custom_name"] = validateUnchecked, -- A custom interface/host name
    ["old_name"] = validateUnchecked, -- Old custom interface/host name
    ["custom_notes"] = validateUnchecked, -- Host custom notes
    ["pool_name"] = validateUnchecked,
    ["pool_name_only"] = validateBool,
    ["query"] = validateUnchecked, -- This field should be used to perform partial queries.
    -- It up to the script to implement proper validation.
    -- In NO case query should be executed directly without validation.
    -- UNQUOTED (Not Generally dangerous)
    ["referer"] = validateUnquoted, -- An URL referer
    ["url"] = {webhookCleanup, validateUnquoted}, -- An URL
    ["label"] = validateUnquoted, -- A device label
    ["os"] = validateNumber, -- An Operating System id
    ["info"] = validateUnquoted, -- An information message
    ["entity_val"] = validateUnquoted, -- An alert entity value
    ["full_name"] = validateUnquoted, -- A user full name
    ["manufacturer"] = validateUnquoted, -- A MAC manufacturer
    ["slack_sender_username"] = validateUnquoted,
    ["slack_webhook"] = {webhookCleanup, validateUnquoted},
    ["bind_dn"] = validateUnquoted,
    ["bind_pwd"] = {passwordCleanup, validatePassword}, -- LDAP Bind Authentication Password
    ["search_path"] = validateUnquoted,
    ["user_group"] = validateUnquoted,
    ["admin_group"] = validateUnquoted,
    ["radius_admin_group"] = validateUnquoted,
    ["radius_unpriv_capabilties_group"] = validateUnquoted,
    ["ts_post_data_url"] = validateUnquoted, -- URL for influxdb
    ["connectivity_check_url"] = validateUnquoted, -- URL for connectivity check

    ["teams_url"] = {webhookCleanup, validateUnquoted},
    ["webhook_url"] = {webhookCleanup, validateUnquoted},
    ["webhook_sharedsecret"] = validateEmptyOr(validateSingleWord),
    ["webhook_username"] = validateEmptyOr(validateSingleWord),
    ["webhook_password"] = validateEmptyOr(validateSingleWord),

    ["mattermost_url"] = {http_lint.webhookCleanup, http_lint.validateUnquoted},
    ["mattermost_token"] = validateMattermostBearerToken,
    ["mattermost_channelname"] = http_lint.validateSingleWord,

    -- DB
    ["select_clause"] = validateUnquoted,
    ["select_keys_clause"] = validateUnquoted,
    ["select_values_clause"] = validateUnquoted,
    ["approx_search"] = validateBool,
    ["group_by_clause"] = validateUnquoted,
    ["order_by_clause"] = validateUnquoted,
    ["alert_family"] = validateAlertFamily, -- Alert family validation
    ["where_clause"] = {whereCleanup, validateUnchecked}, -- previously defined as validateUnquoted
    ["begin_time_clause"] = validateUnquoted,
    ["end_time_clause"] = validateUnquoted,
    ["flow_clause"] = validateSingleWord, -- deprecated (keeping for backward compatibility)
    ["flow_info"] = validateSingleWord,
    ["topk_clause"] = validateSingleWord,
    ["maxhits_clause"] = validateNumber,
    ["ni_query_type"] = validatenIndexQueryType,
    ["ni_query_filter"] = validateListOfTypeInline(validateSingleWord),
    ["aggregated"] = validateBool,

    -- HOST SPECIFICATION
    ["host"] = validateUnquoted, -- an IPv4 (optional @vlan), IPv6 (optional @vlan), or MAC address
    ["versus_host"] = validateHost, -- an host for comparison
    ["filters_to_display"] = validateListOfTypeInline(validateSingleWord),
    ["mac"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateMac))), -- a MAC address
    ["cli_mac"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateMac))), -- a MAC address
    ["srv_mac"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateMac))), -- a MAC address
    ["apn_mac"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateMac))), -- a MAC address
    ["tskey"] = validateUnquoted, -- timeseries name
    ["peer1"] = validateHost, -- a Peer in a connection
    ["peer2"] = validateHost, -- another Peer in a connection
    ["origin"] = validateHost, -- the source of the alert
    ["target"] = validateHost, -- the target of the alert
    ["member"] = validateMember, -- an IPv4 (optional @vlan, optional /suffix), IPv6 (optional @vlan, optional /suffix), or MAC address
    ["old_member"] = validateMember,
    ["network"] = validateListOfTypeInline(validateFilters(validateNumber)), -- A network ID/name
    ["network_name"] = validateFilters(validateNetwork),
    ["network_cidr"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateNetworkWithVLAN))), -- An IPv4 or IPv6 subnet in CIDR format
    ["cli_network_cidr"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateNetworkWithVLAN))), -- An IPv4 or IPv6 subnet in CIDR format
    ["srv_network_cidr"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateNetworkWithVLAN))), -- An IPv4 or IPv6 subnet in CIDR format
    ["cli_network"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateNumber))), -- A network ID
    ["srv_network"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateNumber))), -- A network ID
    ["ip"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateServer))), -- An IPv4 or IPv6 address or an Hostname
    ["cli_ip"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateServer))), -- An IPv4 or IPv6 address or an Hostname
    ["srv_ip"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateServer))), -- An IPv4 or IPv6 address or an Hostname
    ["name"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateServer))), -- An IPv4 or IPv6 address or an Hostname
    ["cli_name"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateHostName))), -- An IPv4 or IPv6 address or an Hostname
    ["srv_name"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateHostName))), -- An IPv4 or IPv6 address or an Hostname
    ["domain_name"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateUnquoted))),
    ["wlan_ssid"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateUnquoted))),
    ["cli_port"] = validateListOfTypeInline(validateFilters(validatePort)), -- Client port
    ["srv_port"] = validateListOfTypeInline(validateFilters(validatePort)), -- Server port
    ["cli_asn"] = validateListOfTypeInline(validateFilters(validateASN)),
    ["srv_asn"] = validateListOfTypeInline(validateFilters(validateASN)),
    ["subtype"] = validateListOfTypeInline(validateFilters(validateUnquoted)),
    ["min_incoming"] = validateEmptyOr(validateNumber), -- Minimum number of incoming edges (alerts) for a host (page-alerts-graph.vue and pro/scripts/lua/rest/v2/get/alert/graph/alerts.lua)
    ["min_outgoing"] = validateEmptyOr(validateNumber), -- Minimum number of outgoing edges (alerts) for a host (page-alerts-graph.vue and pro/scripts/lua/rest/v2/get/alert/graph/alerts.lua)
    ["traffic_direction"] = validateFilters(validateNumber), -- Local or Remote
    ["observation_point_id"] = validateFilters(validateNumber), -- Observation point ID, used by nindex query
    ["probe_ip"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateHost))), -- Probe IP, used by nindex query
    ["tot_bytes"] = validateFilters(validateNumber), -- Total bytes, used by nindex query
    ["src2dst_dscp"] = validateEmptyOr(validateFilters(validateUnquoted)), -- Client DSCP, used by nindex query
    ["dst2src_dscp"] = validateEmptyOr(validateFilters(validateUnquoted)), -- Client DSCP, used by nindex query
    ["cli2srv_bytes"] = validateEmptyOr(validateFilters(validateNumber)), -- Client DSCP, used by nindex query
    ["srv2cli_bytes"] = validateEmptyOr(validateFilters(validateNumber)), -- Client DSCP, used by nindex query
    ["cli_nw_latency"] = validateEmptyOr(validateFilters(validateFloat)), -- Client network latency
    ["srv_nw_latency"] = validateEmptyOr(validateFilters(validateFloat)), -- Server network latency
    ["flow_status_num"] = validateEmptyOr(validateFilters(validateUnquoted)), -- Flow Status, used by nindex query
    ["vhost"] = validateHTTPHost, -- HTTP server name or IP address
    ["version"] = validateIpVersion, -- To specify an IPv4 or IPv6
    ["ip_version"] = validateListOfTypeInline(validateFilters(validateIpVersion)), -- To specify an IPv4 or IPv6
    ["vlan"] = validateEmptyOr(validateNumber), -- A VLAN id
    ["vlan_id"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateNumber))), -- A VLAN id
    ["vlan_label"] = validateEmptyOr(validateUnquoted),
    ["input_snmp"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateSingleWord))),
    ["output_snmp"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateSingleWord))),
    ["snmp_interface"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateSingleWord))),
    ["hosts"] = validateHostsList, -- A list of hosts
    ["src2dst_tcp_flags"] = validateListOfTypeInline(validateFilters(validateNumber)), -- Client to Server TCP flags
    ["dst2src_tcp_flags"] = validateListOfTypeInline(validateFilters(validateNumber)), -- Server to Client TCP flags
    ["score"] = validateListOfTypeInline(validateFilters(validateNumber)), -- Score
    ["qoe_score"] = validateListOfTypeInline(validateFilters(validateNumber)), -- QoE Score
    ["qoe"] = validateNumber, -- QoE Type, e.g. 0, 1 , 2...
    ["chart_id"] = validateUnquoted, -- Chart id
    ["cli_location"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["srv_location"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["host_location"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["major_connection_state"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["minor_connection_state"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["pre_nat_ipv4_src_addr"] = validateListOfTypeInline(validateFilters(validateHost)),
    ["pre_nat_ipv4_dst_addr"] = validateListOfTypeInline(validateFilters(validateHost)),
    ["post_nat_ipv4_src_addr"] = validateListOfTypeInline(validateFilters(validateHost)),
    ["post_nat_ipv4_dst_addr"] = validateListOfTypeInline(validateFilters(validateHost)),
    ["pre_nat_src_port"] = validateListOfTypeInline(validateFilters(validatePort)),
    ["pre_nat_dst_port"] = validateListOfTypeInline(validateFilters(validatePort)),
    ["post_nat_src_port"] = validateListOfTypeInline(validateFilters(validatePort)),
    ["post_nat_dst_port"] = validateListOfTypeInline(validateFilters(validatePort)),

    -- AUTHENTICATION
    ["username"] = validateUsername, -- A ntopng user name, new or existing
    ["password"] = {passwordCleanup, validatePassword}, -- User password
    ["new_password"] = {passwordCleanup, validatePassword}, -- The new user password
    ["old_password"] = {passwordCleanup, validatePassword}, -- The old user password
    ["confirm_password"] = {passwordCleanup, validatePassword}, -- Confirm user password
    ["user_role"] = validateUserRole, -- User role
    ["user_language"] = validateUserLanguage, -- User language
    ["add_user_language"] = validateUserLanguage, -- User language in add modal
    ["allow_pcap_download"] = validateEmptyOr(validateBool),
    ["allow_historical_flows"] = validateBool,
    ["allow_alerts"] = validateBool,
    ["is_range_picker"] = validateBool,

    -- NDPI
    ["application"] = validateProtocolIdOrNameorCategory, -- An nDPI application protocol name
    ["category"] = validateCategory, -- An nDPI protocol category name
    ["category_alias"] = validateUnquoted, -- An nDPI protocol category alias given by the user
    ["protocol_alias"] = validateUnquoted, -- An nDPI protocol category alias given by the user
    ["breed"] = validateBool, -- True if nDPI breed should be shown
    ["ndpi_category"] = validateBool, -- True if nDPI category should be shown
    ["ndpistats_mode"] = validateNdpiStatsMode, -- A mode for rest/v2/get/interface/l7/stats.lua
    ["l4_proto_id"] = validateProtocolIdOrName, -- get_historical_data.lua
    ["l7_proto_id"] = validateProtocolIdOrName,
    ["proto"] = validateSingleWord, -- An nDPI application protocol ID or name
    -- get_historical_data.lua
    ["l4proto"] = validateListOfTypeInline(validateFilters(validateProtocolIdOrName)), -- An nDPI application protocol ID, layer 4
    ["l7proto"] = validateListOfTypeInline(validateFilters(validateDoubleProtocolIdOrName)), -- An nDPI application protocol ID, layer 7
    ["l7proto_master"] = validateListOfTypeInline(validateFilters(validateDoubleProtocolIdOrName)), -- An nDPI application protocol ID, layer 7
    ["l7_proto"] = validateListOfTypeInline(validateFilters(validateProtocolIdOrName)), -- An nDPI application protocol ID, layer 7
    ["filtered_query"] = validateBool, -- Parameter used to download nindex flows
    ["l7cat"] = validateListOfTypeInline(validateFilters(validateCategory)), -- An nDPI category, layer 7
    ["flow_risk"] = validateListOfTypeInline(validateFilters(validateUnquoted)), -- Flow risk
    ["protocol"] = validateProtocolIdOrName, -- An nDPI application protocol ID or name
    ["ndpi"] = validateApplicationsList, -- a list applications
    ["ndpi_new_cat_id"] = validateNumber, -- An ndpi category id after change
    ["ndpi_old_cat_id"] = validateNumber, -- An ndpi category id before change
    ["new_application"] = validateSingleWord, -- A new nDPI application protocol name

    ["subnet_2"] = validateUnquoted,

    -- Wireless
    ["wifi_enabled"] = validateBool,
    ["wifi_ssid"] = validateSingleWord,
    ["wpa_passphrase"] = {passwordCleanup, validatePassword}, -- Wifi password
    ["interface_filter"] = validateInterface,

    -- Remote probe
    ["ifIdx"] = validateNumber, -- A generic switch/router port id
    ["inIfIdx"] = validateNumber, -- A switch/router INPUT port id (%INPUT_SNMP)
    ["outIfIdx"] = validateNumber, -- A switch/router OUTPUT port id (%OUTPUT_SNMP)
    ["deviceIP"] = validateIPV4, -- The switch/router probe ip address (%EXPORTER_IPV4_ADDRESS)
    ["ebpf_data"] = validateEBPFData, -- mode for get_username_data.lua and get_process_data.lua
    ["uid"] = validateNumber, -- user id
    ["pid_mode"] = validatePidMode, -- pid mode for pid_stats.lua
    ["pid_name"] = validateSingleWord, -- A process name
    ["pid"] = validateNumber, -- A process ID
    ["procstats_mode"] = validateProcessesStatsMode, -- A mode for processes_stats.lua
    ["sflowdistro_mode"] = validateSflowDistroMode, -- A mode for host_sflow_distro
    ["distr"] = validateSflowDistroType, -- A type for host_sflow_distro
    ["sflow_filter"] = validateSflowFilter, -- sflow host filter
    ["exporter_ifname"] = validateSingleWord, -- an interface name on the probe system

    -- TIME SPECIFICATION
    ["tstamp"] = validateNumber, -- A timestamp value
    ["epoch"] = validateNumber, -- A timestamp value
    ["epoch_begin"] = validateNumber, -- A timestamp value to indicate start time
    ["epoch_end"] = validateNumber, -- A timestamp value to indicate end time
    ["period_begin_str"] = validateDate, -- Specifies a start date in JS format
    ["period_end_str"] = validateDate, -- Specifies an end date in JS format
    ["timezone"] = validateNumber, -- The timezone of the browser
    ["days"] = validateNumber,

    -- PAGINATION
    ["perPage"] = validateNumber, -- Number of results per page (used for pagination)
    ["sortOrder"] = validateSortOrder, -- A sort order
    ["sortColumn"] = validateSortColumn, -- A sort column
    ["currentPage"] = validateNumber, -- The currently displayed page number (used for pagination)
    ["totalRows"] = validateNumber, -- The total number of rows

    -- AGGREGATION
    ["grouped_by"] = validateSingleWord, -- A group criteria
    ["count"] = validateSingleWord, -- Sql count parameter
    ["aggregation"] = validateAggregation, -- A mode for graphs aggregation
    ["limit"] = validateNumber, -- To limit results
    ["all"] = validateBool, -- To remove limit on results

    -- NAVIGATION
    ["gui"] = validateBool, -- Return data in html format (backward compatibility)
    ["page"] = validateSingleWord, -- Currently active subpage tab
    ["tab"] = validateSingleWord, -- Currently active tab, handled by javascript
    ["template"] = validateSingleWord, -- Currently selected template
    ["template_name"] = validateUnquoted,
    ["endpoint"] = validateSingleWord,
    ["component"] = validateSingleWord,
    ["component_title"] = validateUnquoted,
    ["component_width"] = validateNumber,
    ["component_height"] = validateNumber,
    ["component_time_offset"] = validateSingleWord,
    ["component_time_window"] = validateSingleWord,
    ["component_params"] = {jsonCleanup, validateJSON},
    ["components"] = validateColumnsIds, -- Comma separated list of component ids
    ["top_field"] = validateSingleWord,
    ["system_interface"] = validateBool,

    -- CONFIGSETS
    ["filters"] = validateScriptFilter, -- Currently active exclusion list for the alert
    ["delete_alerts"] = validateBool,
    ["alert_generation"] = {jsonCleanup, validateJSON},

    -- EXCLUDE DEVICES
    ["device"] = validateDevice,
    ["mac_list"] = validateUnquoted,
    ["mac_alias"] = validateSingleWord,
    ["mac_status"] = validateStatus,
    ["devices_status_during_learning"] = validateBool,
    ["devices_status_post_learning"] = validateBool,

    -- UI TOASTS
    ["toast_id"] = validateSingleWord,

    -- NOTIFICATIONS ENDPOINT
    ["recipient_name"] = validateUnquoted,
    ["bind_to_all_pools"] = validateBool,
    ["recipient_id"] = validateNumber,
    ["recipient_check_categories"] = validateEmptyOr(validateListOfTypeInline(validateNumber)),
    ["recipient_check_entities"] = validateEmptyOr(validateListOfTypeInline(validateNumber)),
    ["recipient_host_pools"] = validateEmptyOr(validateListOfTypeInline(validateNumber)),
    ["recipient_am_hosts"] = validateEmptyOr(validateListOfTypeInline(validateUnquoted)),
    ["recipient_interface_pools"] = validateEmptyOr(validateListOfTypeInline(validateNumber)),
    ["recipient_minimum_severity"] = validateNumber,
    ["recipient_silence_multiple_alerts"] = validateBool,
    ["recipient_checks"] = validateEmptyOr(validateListOfTypeInline(validateUnquoted)),
    ["recipient_notifications_type"] = validateEmptyOr(validateSingleWord),
    ["endpoint_conf_name"] = validateUnquoted,
    ["endpoint_id"] = validateNumberOrUnquoted,
    ["endpoint_conf_type"] = validateUnquoted,
    ["cc"] = validateEmptyOr(validateSingleWord),

    -- NOTIFICATIONS ENDPOINT/RECIPIENT
    ["discord_url"] = {http_lint.webhookCleanup, http_lint.validateUnquoted},
    ["discord_sender"] = http_lint.validateUnquoted,
    ["discord_username"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),

    ["shell_script"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),
    ["shell_script_execution"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),
    ["shell_script_options"] = http_lint.validateEmptyOr(http_lint.validateUnquoted),

    ["syslog_alert_format"] = http_lint.validateEmptyOr(http_lint.validateSyslogFormat),
    ["syslog_protocol"] = http_lint.validateEmptyOr(http_lint.validateChoiceInline({"tcp", "udp", ""})),
    ["syslog_host"] = http_lint.validateEmptyOr(validateServer),
    ["syslog_port"] = http_lint.validateEmptyOr(http_lint.validatePort),

    ["telegram_channel"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),
    ["telegram_token"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),

    ["thehive_token"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),
    ["thehive_url"] = {http_lint.webhookCleanup, http_lint.validateUnquoted},
    ["thehive_org_name"] = http_lint.validateSingleWord,
    ["thehive_obs_mail"] = http_lint.validateSingleWord,
    ["thehive_obs_url"] = {http_lint.webhookCleanup, http_lint.validateUnquoted},

    ["pagerduty_integration_key"] = http_lint.validateSingleWord,

    ["fail2ban_jail"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),

    ["elasticsearch_url"] = {http_lint.webhookCleanup, http_lint.validateUnquoted},
    ["elasticsearch_username"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),
    ["elasticsearch_password"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),
    ["elasticsearch_index"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),

    ["wechat_url"] = {http_lint.webhookCleanup, http_lint.validateUnquoted},
    ["wechat_index"] = http_lint.validateEmptyOr(http_lint.validateSingleWord),

    -- POOLS
    ["pool_members"] = validateEmptyOr(validateListOfTypeInline(validateSingleWord)),
    ["recipients"] = validateEmptyOr(validateListOfTypeInline(validateNumber)),

    -- OTHER
    ["rsp_format"] = validateBool,
    ["interface"] = validateNetworkInterface,
    ["showjson"] = validateBool,
    ["hostinfotype"] = validateHostInfoType,
    ["interfacetype"] = validateHostInfoType,
    ["flowtype"] = validateFlowType,
    ["all_values"] = validateBool, -- Used by scripts/lua/rest/v2/get/interface/l7/stats.lua to know if all the values have to be returned
    ["collapse_stats"] = validateBool, -- Used by scripts/lua/rest/v2/get/interface/l7/stats.lua to know if stats need to be collapsed
    ["max_values"] = validateNumber, -- Used by scripts/lua/rest/v2/get/interface/l7/stats.lua to know the max number of stats to be returned
    ["_"] = validateEmptyOr(validateNumber), -- jQuery nonce in ajax requests used to prevent browser caching
    ["__"] = validateUnquoted, -- see LDAP prefs page
    ["ifid"] = validateInterface, -- An ntopng interface ID
    ["rule_ifid"] = validateInterface, -- An ntopng interface ID
    ["observationPointId"] = validateNumber,
    ["ifname"] = validateSingleWord,
    ["iffilter"] = validateIfFilter, -- An interface ID or 'all'
    ["mode"] = validateMode, -- Remote or Local users
    ["dashboard_mode"] = validateDashboardMode, -- Dashboard mode
    ["device_responsiveness"] = validateDeviceResponsiveness, -- Device responsiveness
    ["counters_since"] = validateCounterSince, -- Select actual or absolute counters
    ["timeframe"] = validateUnquoted, -- alerts_analysis_utils.lua
    ["err_counters_filter"] = validateErrorsFilter, -- Filter by errrrs, discards, both
    ["country"] = validateListOfTypeInline(validateFilters(validateCountry)), -- Country code
    ["cli_country"] = validateListOfTypeInline(validateFilters(validateCountry)), -- Country code
    ["srv_country"] = validateListOfTypeInline(validateFilters(validateCountry)), -- Country code
    ["flow_key"] = validateNumber, -- The key of the flow
    ["flow_hash_id"] = validateNumber, -- The ID uniquely identifying the flow in the hash table
    ["user"] = validateSingleWord, -- The user ID
    ["snapshot_name"] = validateSingleWord, -- The user ID
    ["command"] = validateSingleWord, -- The user ID
    ["report_name"] = validateSingleWord, -- The report name
    ["report_template"] = validateSingleWord, -- The report template
    ["report_title"] = validateSingleWord, -- The report title (used in vs reports)
    ["pool"] = validateNumber, -- A pool ID
    ["pool_id"] = validateNumber, -- A pool ID
    ["direction"] = validateDirection, -- Sent or Received direction
    ["download"] = validateBool,
    ["item"] = validateJSON, -- Used by the Import/Export page to select the item to import/export
    ["config"] = validateUnquoted, -- Used by the Import/Export page to select the item to import/export
    ["asset_key"] = validateSingleWord, -- Used by network configuration
    ["stats_type"] = validateStatsType, -- A mode for historical stats queries
    ["alertstats_type"] = validateAlertStatsType, -- A mode for alerts stats queries
    ["flowhosts_type"] = validateFlowHostsTypeOrIpVersionOrIp, -- A filter for local/remote hosts in each of the two directions
    ["hosts_type"] = validateFlowHostsType, -- A filter for local/remote hosts in each of the two directions
    ["talking_with"] = validateHost, -- A filter for hosts in each of the two directions
    ["status"] = validateUnquoted, -- An alert type to filter
    ["enabled_status"] = validateSingleWord, -- Can be: All, Enabled, Disabled
    ["hash_table"] = validateSingleWord, -- An internal ntopng hash_table
    ["periodic_script"] = validateSingleWord, -- A script under callbacks/interface executed by ntopng
    ["periodic_script_issue"] = validateSingleWord, -- Script issues under callbacks/interface executed by ntopng
    ["check"] = validateSingleWord, -- A user script key
    ["check_target"] = validateSingleWord, -- A user script target, e.g., Flow, Host, Interface
    ["subdir"] = validateSingleWord, -- A user script subdir
    ["profile"] = http_lint.validateTrafficProfile, -- Traffic Profile name
    ["delete_profile"] = http_lint.validateTrafficProfile, -- A Traffic Profile to delete
    ["delete_vlan"] = validateSingleWord, -- A Traffic Profile to delete
    ["alert_id"] = validateListOfTypeInline(validateFilters(validateNumber)), -- An alert type enum
    ["alert_category"] = validateListOfTypeInline(validateFilters(validateNumber)), -- An alert category enum
    ["mitre_id"] = validateListOfTypeInline(validateFilters(validateSingleWord)),
    ["mitre_tactic"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["mitre_technique"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["mitre_subtechnique"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["alert_map"] = validateSingleWord,
    ["alert_type"] = validateNumber,
    ["mitre_subtechnique"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["description"] = validateUnquoted,
    ["alert_l7_proto"] = validateNumber, -- An alert l7 protocol
    ["alert_subtype"] = validateSingleWord, -- An alert subtype string
    ["alert_severity"] = validateNumber, -- An alert severity enum
    ["severity"] = validateListOfTypeInline(validateFilters(validateNumber)), -- Same as alert_severity
    ["alert_status"] = validateListOfTypeInline(validateFilters(validateNumber)), -- An alert status enum
    ["alert_granularity"] = validateNumber, -- An alert granularity
    ["entity"] = validateNumber, -- An alert entity type
    ["by_24h"] = validateBool, -- Used to know if the new or the old timeseries format is requested
    ["dns_answer"] = validateListOfTypeInline(validateFilters(validateSingleWord)),
    ["dns_query"] = validateListOfTypeInline(validateFilters(validateSingleWord)),
    ["role"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- attacker/victim
    ["role_cli_srv"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- client/server
    ["l7_error_id"] = validateListOfTypeInline(validateFilters(validateNumber)), -- client/server
    ["http_method"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- client/server
    ["http_return"] = validateListOfTypeInline(validateFilters(validateNumber)), -- client/server
    ["http_url"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- client/server
    ["user_agent"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- client/server
    ["netbios_name"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- client/server
    ["mdns_answer"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- client/server
    ["mdns_name"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- client/server
    ["mdns_name_txt"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- client/server
    ["mdns_ssid"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- client/server
    ["confidence"] = validateListOfTypeInline(validateFilters(validateNumber)), -- client/server
    ["acknowledged"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- acknowledged
    ["community_id"] = validateListOfTypeInline(validateFilters(validateSingleWord)), -- Community ID
    ["cli_fingerprint"] = validateListOfTypeInline(validateFilters(validateSingleWord)),
    ["ja4_client"] = validateListOfTypeInline(validateFilters(validateSingleWord)),
    ["issuer_dn"] = validateListOfTypeInline(validateFilters(validateSingleWord)),
    ["last_server"] = validateListOfTypeInline(validateFilters(validateSingleWord)),
    ["retransmissions"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["out_of_order"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["lost"] = validateListOfTypeInline(validateFilters(validateNumber)),

    ["asn"] = validateNumber, -- An ASN number
    ["country_id"] = validateNumber, -- A Country Code
    ["module"] = validateTopModule, -- A top script module
    ["step"] = validateNumber, -- A step value
    ["cf"] = validateConsolidationFunction, -- An RRD consolidation function
    ["verbose"] = validateBool, -- True if script should be verbose
    ["num_minutes"] = validateNumMinutes, -- number of minutes
    ["zoom"] = validateZoom, -- a graph zoom specifier
    ["column_key"] = validateSingleWord, -- SNMP Column Key
    ["community"] = validateSingleWord, -- SNMP community
    ["snmp_read_community"] = validateSingleWord, -- SNMP Read community
    ["snmp_write_community"] = validateSingleWord, -- SNMP Write community
    ["snmp_level"] = validateSnmpLevel, -- SNMP Level
    ["snmp_auth_protocol"] = validateSnmpAuthProtocol,
    ["snmp_auth_passphrase"] = validateSingleWord,
    ["snmp_privacy_protocol"] = validateSnmpPrivacyProtocol,
    ["observation_point"] = validateNumber,
    ["obs_point"] = validateNumber,
    ["snmp_privacy_passphrase"] = validateSingleWord,
    ["snmp_context"] = validateSingleWord,
    ["lldp_cdp_mode"] = validateBool, -- LLDP/CDP mode
    ["default_snmp_community"] = validateSingleWord, -- Default SNMP community for non-SNMP-configured local hosts
    ["snmp_host"] = validateServer, -- Either an IPv4/v6 or a hostname
    ["default_snmp_version"] = validateSNMPversion, -- Default SNMP protocol version
    ["snmp_version"] = validateSNMPversion, -- 0:v1 1:v2c 2:v3
    ["snmp_username"] = validateSingleWord, -- SNMP Username
    ["cidr"] = validateCIDR, -- /32 or /24
    ["vs_cidr"] = validateVsCIDR, -- /24 /25 /26 /27 /28 /29 /30 /32 or /128
    ["snmp_port_idx"] = validateNumber, -- SNMP port index
    ["port_index"] = validateNumber, -- SNMP port index
    ["uplink_speed"] = validateFloat, -- Flow Exporter uplink speed
    ["downlink_speed"] = validateFloat, -- Flow Exporter downlink speed
    ["if_index"] = validateNumber, -- SNMP port index
    ["snmp_recache"] = validateBool, -- forces SNMP queries to be re-executed and cached
    ["request_discovery"] = validateBool, -- forces device discovery to be re-cached
    ["intfs"] = validateInterfacesList, -- a list of network interfaces ids
    ["search"] = validateBool, -- When set, a search should be performed
    ["search_flows"] = validateBool, -- When set, a flow search should be performed
    ["custom_column"] = validateCustomColumn,
    ["criteria"] = validateCustomColumn,
    ["aggregation_criteria"] = validateSingleWord,
    ["row_id"] = validateUnquoted, -- A number used to identify a record in a database
    ["rrd_file"] = validateUnquoted, -- A path or special identifier to read an RRD file
    ["port"] = validatePort, -- An application port
    ["ntopng_license"] = {licenseCleanup, validateLicense}, -- ntopng licence string
    ["syn_attacker_threshold"] = validateEmptyOr(validateNumber),
    ["global_syn_attacker_threshold"] = validateEmptyOr(validateNumber),
    ["syn_victim_threshold"] = validateEmptyOr(validateNumber),
    ["global_syn_victim_threshold"] = validateEmptyOr(validateNumber),
    ["flow_attacker_threshold"] = validateEmptyOr(validateNumber),
    ["global_flow_attacker_threshold"] = validateEmptyOr(validateNumber),
    ["flow_victim_threshold"] = validateEmptyOr(validateNumber),
    ["global_flow_victim_threshold"] = validateEmptyOr(validateNumber),
    ["re_arm_minutes"] = validateEmptyOr(validateNumber), -- Number of minute before alert re-arm check
    ["device_type"] = validateNumber,
    ["ewma_alpha_percent"] = validateNumber,
    ["sidebar_collapsed"] = validateNumber,
    ["senders_receivers"] = validateSendersReceivers, -- Used in top scripts
    ["fingerprint_type"] = validateFingerprintType,
    ["granularity"] = validateSingleWord,
    ["old_granularity"] = validateSingleWord,
    ["script_type"] = validateSingleWord,
    ["check_subdir"] = validateSingleWord,
    ["script_key"] = validateSingleWord,
    ["alert_key"] = validateNumber,
    ["flow_alert_key"] = validateNumber,
    ["host_alert_key"] = validateNumber,
    ["alert_addr"] = validateSingleWord,
    ["alert_domain"] = validateSingleWord,
    ["alert_certificate"] = validateUnchecked,
    ["old_type"] = validateSingleWord,
    ["old_subdir"] = validateSingleWord,
    ["old_alert_domain"] = validateSingleWord,
    ["old_alert_certificate"] = validateSingleWord,
    ["old_alert_addr"] = validateSingleWord,
    ["old_flow_alert_key"] = validateSingleWord,
    ["old_host_alert_key"] = validateSingleWord,
    ["search_script"] = validateSingleWord,
    ["field_alias"] = validateListOfTypeInline(validateFieldAlias),
    ["dscp_class"] = validateSingleWord,
    ["host_pool_members"] = validateUnquoted,
    ["type"] = validateSingleWord,
    ["frequency"] = validateSingleWord,
    ["metric_type"] = validateSingleWord,
    ["rule_id"] = validateSingleWord,
    ["new_rule_id"] = validateSingleWord,
    ["extra_metric"] = validateSingleWord,
    ["rule_type"] = validateSingleWord,
    ["rule_threshold_sign"] = validateNumber,
    ["is_ifname"] = validateBool,
    ["metric_label"] = validateUnquoted,
    ["flows_page_type"] = validateSingleWord,
    ["view"] = validateSingleWord,
    ["enabled"] = validateBool,

    -- VULNERABILITY SCAN
    ["scan_type"] = validateSingleWord,
    ["discovered_host_scan_type"] = validateListOfTypeInline(validateSingleWord),
    ["scan_ports"] = validateSingleWord,
    ["scan_params"] = validateUnquoted,
    ["scan_single_host"] = validateBool,
    ["delete_all_scan_hosts"] = validateBool,
    ["scan_return_result"] = validateBool,
    ["scan_date"] = validateSingleWord,
    ["scan_frequency"] = validateSingleWord,
    ["scan_id"] = validateSingleWord,
    ["is_va"] = validateBool,
    ["is_edit"] = validateBool,
    ["was_down"] = validateBool,
    ["netscan_report"] = validateBool,

    ["snmp_device"] = validateDevice,
    ["snmp_device_port"] = validateSNMPIfidNumber,
    ["snmp_device_port_label"] = validateUnquoted,
    ["snmp_device_label"] = validateUnquoted,
    ["snmp_threshold_value"] = validateNumber,
    ["snmp_threshold_unit"] = validateUnquoted,
    ["snmp_metric_type_label"] = validateUnquoted,

    ["bytes"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["packets"] = validateListOfTypeInline(validateFilters(validateNumber)),

    ["host_pool"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["cli_host_pool_id"] = validateListOfTypeInline(validateFilters(validateNumber)),
    ["srv_host_pool_id"] = validateListOfTypeInline(validateFilters(validateNumber)),

    -- Widget and Datasources
    ["ds_type"] = validateSingleWord,
    ["ds_name"] = validateSingleWord,
    ["transformation"] = validateTransformation,

    -- Topology SNMP Devices
    ["topology_host"] = validateIPV4,
    ["toggle_snmp_trap"] = validateBool,
    ["toggle_snmp_excluded_from_usage"] = validateBool,

    -- Active monitoring
    ["am_host"] = {http_lint.webhookCleanup, http_lint.validateUnquoted},
    ["old_am_host"] = {http_lint.webhookCleanup, http_lint.validateUnquoted},
    ["old_host"] = {http_lint.webhookCleanup, http_lint.validateUnquoted},
    ["threshold"] = http_lint.validateEmptyOr(http_lint.validateNumber),
    ["measurement"] = validateMeasurement,
    ["old_measurement"] = validateMeasurement,

    -- Infrastructure Dashboard
    ["alias"] = validateUnquoted,
    ["token"] = validateToken,
    ["instance_id"] = validateSingleWord,
    ["stats"] = validateBool,
    ["rtt_threshold"] = validateNumber,
    ["bandwidth_threshold"] = validateNumber,
    ["toggle_netbox"] = validateBool,
    ["toggle_ntopng_assets_inventory"] = validateBool,
    ["netbox_activation_url"] = validateSingleWord,
    ["netbox_default_site"] = validateSingleWord,
    ["netbox_personal_access_token"] = validateSingleWord,
    ["is_infrastructure"] = validateBool,

    -- Service Map
    ["service_id"] = validateNumber,
    ["service_status"] = validateNumber,
    ["set_service_state"] = validateNumber,
    ["service_l7proto"] = validateSingleWord,
    ["map"] = validateUnquoted,
    ["age"] = validateSingleWord,
    ["first_seen"] = validateNumber,
    ["only_memory"] = validateBool,
    ["only_alerted_hosts"] = validateBool,
    ["map_search"] = validateUnquoted,

    -- Script editor
    ["script_file_path"] = validateLuaScriptPath,

    -- PREFERENCES - see prefs.lua for details
    -- Toggle Buttons
    ["toggle_flow_begin"] = validateBool,
    ["flow_table_time"] = validateBool,
    ["flow_table_probe_order"] = validateBool,
    ["interface_rrd_creation"] = validateBool,
    ["interface_one_way_hosts_rrd_creation"] = validateBool,
    ["interface_top_talkers_creation"] = validateBool,
    ["automatic_reports_creation"] = validateBool,
    ["interface_flow_dump"] = validateBool,
    ["is_mirrored_traffic"] = validateBool,
    ["discard_probing_traffic"] = validateBool,
    ["show_dyn_iface_traffic"] = validateBool,
    ["push_host_filters"] = validateBool,
    ["interface_network_discovery"] = validateBool,
    ["dynamic_iface_vlan_creation"] = validateBool,
    ["toggle_mysql_check_open_files_limit"] = validateBool,
    ["disable_alerts_generation"] = validateBool,
    ["alert_page_refresh_rate_enabled"] = validateBool,
    ["enable_score"] = validateBool,
    ["toggle_alert_probing"] = validateBool,
    ["toggle_geo_map_score"] = validateBool,
    ["toggle_geo_map_asname"] = validateBool,
    ["toggle_geo_map_alerted_flows"] = validateBool,
    ["toggle_geo_map_blacklisted_flows"] = validateBool,
    ["toggle_geo_map_host_name"] = validateBool,
    ["toggle_geo_map_rxtx_data"] = validateBool,
    ["toggle_geo_map_num_flows"] = validateBool,
    ["toggle_flow_alerts_iface"] = validateBool,
    ["toggle_tls_alerts"] = validateBool,
    ["toggle_dns_alerts"] = validateBool,
    ["toggle_mining_alerts"] = validateBool,
    ["toggle_remote_to_remote_alerts"] = validateBool,
    ["toggle_dropped_flows_alerts"] = validateBool,
    ["toggle_malware_probing"] = validateBool,
    ["toggle_device_protocols_alerts"] = validateBool,
    ["toggle_elephant_flows_alerts"] = validateBool,
    ["toggle_ip_reassignment_alerts"] = validateBool,
    ["toggle_longlived_flows_alerts"] = validateBool,
    ["toggle_data_exfiltration"] = validateBool,
    ["toggle_enable_automatic_reports"] = validateBool,
    ["toggle_enable_runtime_flows_dump"] = validateBool,
    ["toggle_tiny_flows_dump"] = validateBool,
    ["toggle_alert_syslog"] = validateBool,
    ["toggle_slack_notification"] = validateBool,
    ["toggle_email_notification"] = validateBool,
    ["toggle_top_sites"] = validateBool,
    ["toggle_sites_collection"] = validateBool,
    ["toggle_dns_cache"] = validateBool,
    ["toggle_http_enable_access_log"] = validateBool,
    ["toggle_asset_inventory_log"] = validateBool,
    ["toggle_captive_portal"] = validateBool,
    ["toggle_mdns_repeater"] = validateBool,
    ["toggle_informative_captive_portal"] = validateBool,
    ["toggle_external_auth_captive_portal"] = validateBool,
    ["toggle_autologout"] = validateBool,
    ["toggle_interface_name_only"] = validateBool,
    ["toggle_autoupdates"] = validateBool,
    ["toggle_ldap_anonymous_bind"] = validateBool,
    ["toggle_local"] = validateBool,
    ["toggle_local_host_cache_enabled"] = validateBool,
    ["toggle_active_local_host_cache_enabled"] = validateBool,
    ["toggle_network_discovery"] = validateBool,
    ["toggle_periodic_network_discovery"] = validateBool,
    ["toggle_network_discovery_debug"] = validateBool,
    ["toggle_interface_traffic_rrd_creation"] = validateBool,
    ["toggle_local_hosts_traffic_rrd_creation"] = validateBool,
    ["toggle_local_hosts_stats_rrd_creation"] = validateBool,
    ["toggle_l2_devices_traffic_rrd_creation"] = validateBool,
    ["toggle_observation_points_rrd_creation"] = validateBool,
    ["toggle_intranet_traffic_rrd_creation"] = validateBool,
    ["toggle_system_probes_timeseries"] = validateBool,
    ["toggle_interface_usage_probes_timeseries"] = validateBool,
    ["toggle_flow_rrds"] = validateBool,
    ["toggle_pools_rrds"] = validateBool,
    ["toggle_flow_snmp_ports_rrds"] = validateBool,
    ["toggle_access_log"] = validateBool,
    ["toggle_host_pools_log"] = validateBool,
    ["toggle_log_to_file"] = validateBool,
    ["toggle_snmp_rrds"] = validateBool,
    ["toggle_snmp_polling"] = validateBool,
    ["toggle_active_monitoring"] = validateBool,
    ["toggle_tiny_flows_export"] = validateBool,
    ["toggle_vlan_rrds"] = validateBool,
    ["toggle_asn_rrds"] = validateBool,
    ["toggle_country_rrds"] = validateBool,
    ["toggle_os_rrds"] = validateBool,
    ["toggle_shaping_directions"] = validateBool,
    ["toggle_dst_with_post_nat_dst"] = validateBool,
    ["toggle_src_with_post_nat_src"] = validateBool,
    ["toggle_behaviour_analysis"] = validateBool,
    ["toggle_use_mac_in_flow_key"] = validateBool,
    ["toggle_fingerprint_stats"] = validateBool,
    ["toggle_starttls"] = validateBool,
    ["toggle_dump_pcap_to_clickhouse"] = validateBool,
    ["behaviour_analysis_learning_period"] = validateNumber,
    ["behaviour_analysis_learning_status_during_learning"] = validateNumber,
    ["behaviour_analysis_learning_status_post_learning"] = validateNumber,
    ["iec60870_learning_period"] = validateNumber,
    ["modbus_learning_period"] = validateNumber,
    ["devices_learning_period"] = validateNumber,
    ["host_port_learning_period"] = validateNumber,
    ["toggle_src_and_dst_using_ports"] = validateBool,
    ["toggle_device_activation_alert"] = validateBool,
    ["toggle_device_first_seen_alert"] = validateBool,
    ["toggle_pool_activation_alert"] = validateBool,
    ["toggle_quota_exceeded_alert"] = validateBool,
    ["toggle_external_alerts"] = validateBool,
    ["toggle_influx_auth"] = validateBool,
    ["toggle_ldap_auth"] = validateBool,
    ["toggle_ldap_ext_user_cap"] = validateBool,
    ["toggle_local_auth"] = validateBool,
    ["toggle_radius_accounting"] = validateBool,
    ["toggle_radius_external_auth_for_local_users"] = validateBool,
    ["toggle_radius_auth"] = validateBool,
    ["toggle_http_auth"] = validateBool,
    ["toggle_ldap_referrals"] = validateBool,
    ["toggle_ldap_debug"] = validateBool,
    ["toggle_webhook_notification"] = validateBool,
    ["toggle_elasticsearch_notification"] = validateBool,
    ["toggle_auth_session_midnight_expiration"] = validateBool,
    ["toggle_client_x509_auth"] = validateBool,
    ["toggle_snmp_debug"] = validateBool,
    ["toggle_snmp_port_admin_status"] = validateBool,
    ["toggle_snmp_alerts_port_duplexstatus_change"] = validateBool,
    ["toggle_snmp_qos_mib_polling"] = validateBool,
    ["toggle_snmp_alerts_port_status_change"] = validateBool,
    ["toggle_snmp_alerts_port_errors"] = validateBool,
    ["snmp_port_load_threshold"] = validateNumber,
    ["toggle_midnight_stats_reset"] = validateBool,
    ["toggle_ndpi_flows_rrds"] = validateBool,
    ["toggle_internals_rrds"] = validateBool,
    ["toggle_local_hosts_one_way_ts"] = validateBool,
    ["toggle_dark_theme"] = validateBool,
    ["toggle_menu_entry_help"] = validateBool,
    ["toggle_menu_entry_developer"] = validateBool,
    ["toggle_flow_aggregated_alerted_flows"] = validateBool,
    ["toggle_slow_mode"] = validateBool,
    ["toggle_message_broker"] = validateBool,
    ["toggle_tls_quic_hostnaming"] = validateBool,
    ["toggle_exclude_from_usage_page"] = validateBool,

    -- Input fields
    ["companion_interface"] = validateEmptyOr(validateInterface),
    ["flows_and_alerts_data_retention_days"] = validateNumber,
    ["aggregated_flows_data_retention_days"] = validateNumber,
    ["reports_data_retention_days"] = validateNumber,
    ["ts_and_stats_data_retention_days"] = validateNumber,
    ["max_entity_alerts"] = validateNumber,
    ["max_num_secs_before_delete_alert"] = validateNumber,
    ["alert_page_refresh_rate"] = validateNumber,
    ["elephant_flow_remote_to_local_bytes"] = validateNumber,
    ["elephant_flow_local_to_remote_bytes"] = validateNumber,
    ["max_num_packets_per_tiny_flow"] = validateNumber,
    ["dump_frequency"] = validateNumber,
    ["max_num_bytes_per_tiny_flow"] = validateNumber,
    ["max_aggregated_flows_traffic_upperbound"] = validateNumber,
    ["max_aggregated_flows_upperbound"] = validateNumber,
    ["google_apis_browser_key"] = validateSingleWord,
    ["ldap_server_address"] = validateSingleWord,
    ["radius_server_address"] = validateSingleWord,
    ["radius_acct_server_address"] = validateSingleWord,
    ["os_type"] = validateSingleWord,
    ["server_type"] = validateNumber,
    ["http_auth_url"] = validateSingleWord,
    ["radius_secret"] = validateUnquoted,
    ["radius_auth_proto"] = validateAuthProto,
    ["local_host_max_idle"] = validateNumber,
    ["longlived_flow_duration"] = validateNumber,
    ["non_local_host_max_idle"] = validateNumber,
    ["flow_max_idle"] = validateNumber,
    ["active_local_host_cache_interval"] = validateNumber,
    ["mac_address_cache_duration"] = validateNumber,
    ["toggle_assets_collection"] = validateBool,
    ["auth_session_duration"] = validateNumber,
    ["local_host_cache_duration"] = validateNumber,
    ["intf_rrd_raw_days"] = validateNumber,
    ["intf_rrd_1min_days"] = validateNumber,
    ["intf_rrd_1h_days"] = validateNumber,
    ["intf_rrd_1d_days"] = validateNumber,
    ["other_rrd_raw_days"] = validateNumber,
    ["other_rrd_1min_days"] = validateNumber,
    ["other_rrd_1h_days"] = validateNumber,
    ["other_rrd_1d_days"] = validateNumber,
    ["host_activity_rrd_1h_days"] = validateNumber,
    ["host_activity_rrd_1d_days"] = validateNumber,
    ["host_activity_rrd_raw_hours"] = validateNumber,
    ["max_ui_strlen"] = validateNumber,
    ["ntopng_host_address"] = validateSingleWord,
    ["http_acl_management_port"] = validateACLNetworksList,
    ["http_index_page"] = validateUnquoted,
    ["safe_search_dns"] = validateIPV4,
    ["global_dns"] = validateEmptyOr(validateIPV4),
    ["secondary_dns"] = validateEmptyOr(validateIPV4),
    ["informative_captive_portal_url"] = validateEmptyOr(validateSingleWord),
    ["redirection_url"] = validateEmptyOr(validateSingleWord),
    ["email_sender"] = validateSingleWord,
    ["email_recipient"] = validateSingleWord,
    ["smtp_server"] = validateServer,
    ["smtp_port"] = validation_utils.validatePortRange,
    ["smtp_username"] = validateEmptyOr(validateSingleWord),
    ["smtp_password"] = validateEmptyOr(validatePassword),
    ["use_proxy"] = validateBool,
    ["influx_dbname"] = validateSingleWord,
    ["influx_username"] = validateEmptyOr(validateSingleWord),
    ["influx_password"] = validateEmptyOr(validateSingleWord),
    ["influx_query_timeout"] = validateNumber,
    ["host_to_scan_max_num_scans"] = validateNumber,
    ["serial_key"] = validateSingleWord,
    ["message_broker_username"] = validateEmptyOr(validateSingleWord),
    ["message_broker_password"] = validateEmptyOr(validateSingleWord),
    ["message_broker_url"] = validateEmptyOr(validateSingleWord),
    ["message_broker_topics_list"] = validateEmptyOr(validateUnquoted),

    -- Multiple Choice
    ["disaggregation_criterion"] = validateChoiceInline({"none", "vlan", "probe_ip", "iface_idx", "ingress_iface_idx",
                                                         "ingress_vrf_id", "probe_ip_and_ingress_iface_idx"}),
    ["ignored_interfaces"] = validateEmptyOr(validateListOfTypeInline(validateNumber)),
    ["hosts_ndpi_timeseries_creation"] = validateChoiceInline({"none", "per_protocol", "per_category", "both"}),
    ["interfaces_ndpi_timeseries_creation"] = validateChoiceInline({"none", "per_protocol", "per_category", "both"}),
    ["l2_devices_ndpi_timeseries_creation"] = validateChoiceInline({"none", "per_category"}),
    ["exporters_ndpi_ts_creation"] = validateChoiceInline({"none", "per_protocol"}),
    ["slack_notification_severity_preference"] = validateNotificationSeverity,
    ["email_notification_severity_preference"] = validateNotificationSeverity,
    ["webhook_notification_severity_preference"] = validateNotificationSeverity,
    ["notification_severity_preference"] = validateNotificationSeverity,
    ["multiple_ldap_authentication"] = validateChoiceInline({"local", "ldap", "ldap_local"}),
    ["multiple_ldap_account_type"] = validateChoiceInline({"posix", "samaccount"}),
    ["toggle_logging_level"] = validateChoiceInline({"trace", "debug", "info", "normal", "warning", "error"}),
    ["toggle_thpt_content"] = validateChoiceInline({"bps", "pps"}),
    ["toggle_theme"] = validateChoiceInline({"default", "white", "dark"}),
    ["toggle_host_mask"] = validateChoiceInline({"0", "1", "2"}),
    ["topk_heuristic_precision"] = validateChoiceInline({"disabled", "more_accurate", "accurate", "aggressive"}),
    ["timeseries_driver"] = validateChoiceInline({"rrd", "influxdb", "prometheus"}),
    ["message_broker"] = validateChoiceInline({"nats", "mqtt"}),
    ["edition"] = validateEmptyOr(validateChoiceInline(
        {"community", "pro", "enterprise", "enterprise_m", "enterprise_l"})),
    ["hosts_ts_creation"] = validateChoiceInline({"off", "light", "full"}),
    ["ts_high_resolution"] = validateNumber,
    ["lbd_hosts_as_macs"] = validateBool,
    ["toggle_emit_flow_alerts"] = validateBool,
    ["toggle_emit_host_alerts"] = validateBool,
    ["toggle_date_type"] = validateSingleWord,

    ["exporters_ts_resolution"] = validateNumber,

    -- Other
    ["send_test_email"] = validateEmpty,
    ["send_test_slack"] = validateEmpty,
    ["send_test_webhook"] = validateEmpty,
    ["send_test_elasticsearch"] = validateEmpty,
    ["network_discovery_interval"] = validateNumber,
    ["blog_notification_id"] = validateNumber,
    ["captive_portal_id_method"] = validateChoiceInline({"mac", "ip"}),

    -- Save columns config new tables
    ["visible_columns_ids"] = validateColumnsIds,
    ["table_id"] = validateSingleWord,

    -- Error report
    ["message"] = validateSingleWord,
    ["script_path"] = validateLuaScriptPath,
    ["error_message"] = validateMessage,
    ["reason"] = validateSingleWord,

    --
    ["assets"] = validateMessage,

    -- LIVE CAPTURE
    ["capture_id"] = validateNumber, -- Live capture id
    ["duration"] = validateEmptyOr(validateListOfTypeInline(validateFilters(validateNumber))),
    ["bpf_filter"] = validateEmptyOr(validateUnquoted), --

    -- TRAFFIC RECORDING
    ["snaplen"] = validateNumber,
    ["disk_space"] = validateNumber,
    ["smart_disk_space"] = validateNumber,
    ["file_id"] = validateNumber,
    ["job_action"] = validateExtractionJobAction,
    ["job_id"] = validateNumber,
    ["n2disk_license"] = {licenseCleanup, validateLicense},
    ["record_traffic"] = validateBool,
    ["flow_export"] = validateBool,
    ["smart_record_traffic"] = validateBool,
    ["max_extracted_pcap_bytes"] = validateNumber,
    ["traffic_recording_provider"] = validateTrafficRecordingProvider,
    ["dismiss_external_providers_reminder"] = validateBool,
    ["dismiss_missing_geoip_reminder"] = validateBool,
    --

    -- TOP STATS
    ["only_data"] = validateEmptyOr(validateBool), -- Used by top talkers, if true then only important data is returned
    --

    -- PAGE SPECIFIC
    ["hosts_category"] = validateNumber,
    ["hour"] = validateEmptyOr(validateSingleWord),
    ["unicast_only"] = validateBool,
    ["iflocalstat_mode"] = validateIfaceLocalStatsMode, -- A mode for iface_local_stats.lua
    ["clisrv"] = validateClientOrServer, -- Client or Server filter
    ["client"] = validateHost,
    ["server"] = validateHost,
    ["report"] = validateReportMode, -- A mode for traffic report
    ["use_server_timezone"] = validateBool,
    ["report_zoom"] = validateBool, -- True if we are zooming in the report
    ["format"] = validatePrintFormat, -- a print format
    ["nedge_config_action"] = validatenEdgeAction, -- system_setup_ui_utils.lua
    ["fav_action"] = validateFavouriteAction, -- get_historical_favourites.lua
    ["favourite_type"] = validateFavouriteType, -- get_historical_favourites.lua
    ["locale"] = validateCountry, -- locale used in test_locale.lua
    ["render"] = validateBool, -- True if report should be rendered
    ["printable"] = validateBool, -- True if report should be printable
    ["daily"] = validateBool, -- used by report.lua
    ["json"] = validateBool, -- True if json output should be generated
    ["extended"] = validateBool, -- Flag for extended report
    ["tracked"] = validateNumber, --
    ["ajax_format"] = validateAjaxFormat, -- iface_hosts_list
    ["host_stats_flows"] = validateBool, -- True if host_get_json should return statistics regarding host flows
    ["showall"] = validateBool, -- report.lua
    ["addvlan"] = validateBool, -- True if VLAN must be added to the result
    ["http_mode"] = validateHttpMode, -- HTTP mode for host_http_breakdown.lua
    ["refresh"] = validateNumber, -- top flow refresh in seconds, index.lua
    ["always_show_hist"] = validateBool, -- host_details.lua
    ["host_stats"] = validateBool, -- host_get_json
    ["captive_portal_users"] = validateBool, -- to show or hide captive portal users
    ["long_names"] = validateBool, -- get_hosts_data
    ["id_to_delete"] = validateIdToDelete, -- alert_utils.lua, alert ID to delete
    ["to_delete"] = validateLocalGlobal, -- alert_utils.lua, set if alert configuration should be dropped
    ["SaveAlerts"] = validateEmpty, -- alert_utils.lua, set if alert configuration should change
    ["host_pool_id"] = validateNumber, -- change_user_prefs, new pool id for host
    ["host_pool_label"] = validateSingleWord,
    ["old_host_pool_id"] = validateNumber, -- change_user_prefs, old pool id for host
    ["del_l7_proto"] = validateShapedElement, -- if_stats.lua, ID of the protocol to delete from rule
    ["target_pool"] = validateNumber, -- if_stats.lua, ID of the pool to perform the action on
    ["add_shapers"] = validateEmpty, -- if_stats.lua, set when adding shapers
    ["delete_shaper"] = validateNumber, -- shaper ID to delete
    ["empty_pool"] = validateNumber, -- host_pools.lua, action to empty a pool by ID
    ["pool_to_delete"] = validateNumber, -- host_pools.lua, pool ID to delete
    ["edit_pools"] = validateEmpty, -- host_pools.lua, set if pools are being edited
    ["member_to_delete"] = validateMemberRelaxed, -- host_pools.lua, member to delete from pool
    ["sampling_rate"] = validateEmptyOr(validateNumber), -- if_stats.lua
    ["resetstats_mode"] = validateResetStatsMode, -- reset_stats.lua
    ["snmp_action"] = validateSnmpAction, -- snmp specific
    ["snmp_status"] = validateSNMPstatus, -- snmp specific status (up: 1, down: 2, testing: 3)
    ["snmp_admin_status"] = validateSNMPstatus, -- same as snmp_status but for the admin status
    ["snmp_if_type"] = validateNumber, -- snmp interface type (see snmp_utils.lua fnmp_iftype)
    ["iftype_filter"] = validateSingleWord, -- SNMP iftype filter name
    ["host_quota"] = validateEmptyOr(validateNumber), -- max traffi quota for host
    ["allowed_interface"] = validateEmptyOr(validateInterface), -- the interface an user is allowed to configure
    ["allowed_networks"] = validateNetworksList, -- a list of networks the user is allowed to monitor
    ["switch_interface"] = validateInterface, -- a new active ntopng interface
    ["switch_instance"] = validateInterface, -- a new active ntopng infrastructure instance
    ["qos_class_id"] = validateNumber,
    ["edit_members"] = validateEmpty, -- set if we are editing pool members
    ["trigger_alerts"] = validateBool, -- true if alerts should be active for this entity
    ["show_advanced_prefs"] = validateBool, -- true if advanced preferences should be shown
    ["ifSpeed"] = validateEmptyOr(validateNumber), -- interface speed
    ["scaling_factor"] = validateEmptyOr(validateNumber), -- interface scaling factor
    ["drop_host_traffic"] = validateBool, -- to drop an host traffic
    ["lifetime_limited"] = validateEmptyOr(validateOnOff), -- set if user should have a limited lifetime
    ["lifetime_unlimited"] = validateEmptyOr(validateOnOff), -- set if user should have an unlimited lifetime
    ["edit_profiles"] = validateEmpty, -- set when editing traffic profiles
    ["edit_policy"] = validateEmpty, -- set when editing policy
    ["edit_device_policy"] = validateEmpty, -- set when editing device policy
    ["delete_user"] = validateSingleWord,
    ["drop_flow_policy"] = validateBool, -- true if target flow should be dropped
    ["traffic_type"] = validateBroadcastUnicast, -- flows_stats.lua
    ["alert_type"] = validateAlertTypeAndSeverity, -- flows_stats.lua
    ["flow_status"] = validateAlertType, -- flows_stats.lua
    ["alert_type_severity"] = validateAlertTypeSeverity, -- flows_stats.lua
    ["alert_type_num"] = validateAlertTypeNumber, -- charts
    ["tcp_flow_state"] = validateTCPFlowState, -- flows_stats.lua
    ["traffic_profile"] = http_lint.validateTrafficProfile, -- flows_stats.lua
    ["include_unlimited"] = validateBool, -- pool_details_ndpi.lua
    ["policy_preset"] = validateEmptyOr(validatePolicyPreset), -- a traffic bridge policy set
    ["members_filter"] = validateMembersFilter, -- host_pools.lua
    ["devices_mode"] = validateDevicesMode, -- macs_stats.lua
    ["flow_mode"] = validateFlowMode, -- if_stats.lua
    ["unassigned_devices"] = validateUnassignedDevicesMode, -- unknown_device.lua
    ["delete_all_policies"] = validateEmpty, -- traffic policies
    ["safe_search"] = validateBool, -- users
    ["device_protocols_policing"] = validateBool, -- users
    ["forge_global_dns"] = validateBool, -- users
    ["asset_family"] = validateAssetFamily, -- network_maps.lua
    ["lan_interfaces"] = validateListOfTypeInline(validateNetworkInterface),
    ["wan_interfaces"] = validateListOfTypeInline(validateNetworkInterface),
    ["static_route_name"] = validateStaticRouteName,
    ["old_static_route_name"] = validateStaticRouteName,
    ["delete_static_route"] = validateStaticRouteName,
    ["gateway_name"] = validateGatewayName,
    ["old_gateway_name"] = validateGatewayName,
    ["interface_name"] = validateGatewayName,
    ["old_interface_name"] = validateGatewayName,
    ["delete_gateway"] = validateGatewayName,
    ["ping_address"] = validateIPV4,
    ["flowdev_ip"] = validateIPV4,
    ["device_ip"] = validateIPV4,
    ["policy_name"] = validateRoutingPolicyName,
    ["old_policy_name"] = validateRoutingPolicyName,
    ["delete_policy"] = validateRoutingPolicyName,
    ["policy_id"] = validateNumber,
    ["timezone_name"] = validateTimeZoneName,
    ["custom_date_str"] = validateDate,
    ["custom_date_str_orig"] = validateDate,
    ["global_dns_preset"] = validateSingleWord,
    ["child_dns_preset"] = validateSingleWord,
    ["global_primary_dns"] = validateIPV4,
    ["global_secondary_dns"] = validateEmptyOr(validateIPV4),
    ["child_primary_dns"] = validateIPV4,
    ["child_secondary_dns"] = validateEmptyOr(validateIPV4),
    ["lan_recovery_ip"] = validateIPV4,
    ["lan_recovery_netmask"] = validateIPV4,
    ["dhcp_server_enabled"] = validateBool,
    ["ntp_sync_enabled"] = validateBool,
    ["activate_remote_assist"] = validateBool,
    ["dhcp_first_ip"] = validateIPV4,
    ["dhcp_last_ip"] = validateIPV4,
    ["factory"] = validateBool,
    ["factory_reset"] = validateEmpty,
    ["data_reset"] = validateEmpty,
    ["policy_filter"] = validateEmptyOr(validateNumber),
    ["hostname"] = validateSingleWord,
    ["delete"] = validateEmpty,
    ["delete_active_if_data"] = validateEmpty,
    ["delete_inactive_if_data"] = validateEmpty,
    ["delete_inactive_if_data_system"] = validateEmpty,
    ["delete_active_if_data_system"] = validateEmpty,
    ["reset_quotas"] = validateEmpty,
    ["bandwidth_allocation"] = validateChoiceInline({"min_guaranteed", "max_enforced"}),
    ["quotas_control"] = validateChoiceInline({"daily", "weekly", "monthly"}),
    ["bind_to"] = validateChoiceInline({"lan", "any"}),
    ["slow_pass_shaper_perc"] = validateNumber,
    ["slower_pass_shaper_perc"] = validateNumber,
    ["skip_critical"] = validateBool,
    ["reboot"] = validateEmpty,
    ["poweroff"] = validateEmpty,
    ["operating_mode"] = validateOperatingMode,
    ["per_ip_pass_rate"] = validateNumber,
    ["per_ip_slow_rate"] = validateNumber,
    ["per_ip_slower_rate"] = validateNumber,
    ["user_policy"] = validateNumber,
    ["hide_from_top"] = validateNetworksWithVLANList,
    ["gw_macs"] = validateMacList,
    ["top_hidden"] = validateBool,
    ["packets_drops_perc"] = validateEmptyOr(validateNumber),
    ["operating_system"] = validateNumber,
    ["action"] = validateSingleWord, -- generic
    ["table"] = validateSingleWord,
    ["columns"] = validateListOfTypeInline(validateNumber),
    ["devices"] = {jsonCleanup, validateJSON},
    ["devices_csv"] = validateUnquoted,
    ["ts_schema"] = validateSingleWord,
    ["metric"] = validateSingleWord,
    ["ts_query"] = validateListOfTypeInline(validateUnquoted),
    ["ts_compare"] = validateZoom,
    ["no_fill"] = validateBool,
    ["detail_view"] = validateSingleWord,
    ["initial_point"] = validateBool,
    ["extract_now"] = validateBool,
    ["custom_hosts"] = validateListOfTypeInline(validateSingleWord),
    ["custom_rules"] = validateListOfTypeInline(validateSingleWord),
    ["assistance_key"] = validateUnquoted,
    ["assistance_community"] = validateUnquoted,
    ["allow_admin_access"] = validateBool,
    ["accept_tos"] = validateBool,
    ["no_timeout"] = validateBool,
    ["bubble_mode"] = validateNumber,
    ["supernode"] = validateSingleWord,
    ["ts_aggregation"] = validateChoiceInline({"raw", "1h", "1d"}),
    ["fw_rule_id"] = validateSingleWord,
    ["external_port"] = validation_utils.validatePortRange,
    ["internal_port"] = validation_utils.validatePortRange,
    ["internal_ip"] = validateIPV4,
    ["fw_proto"] = validateChoiceInline({"tcp", "udp", "both"}),
    ["wan_interface"] = validateNetworkInterface,
    ["list_name"] = validateUnquoted,
    ["list_enabled"] = validateOnOff,
    ["list_update"] = validateNumber,
    ["dhcp_ranges"] = validateListOfTypeInline(validateIpRange),
    ["old_dhcp_ranges"] = validateListOfTypeInline(validateIpRange),
    ["icmp_type"] = validateNumber,
    ["icmp_cod"] = validateNumber,
    ["dscp"] = validateNumber,
    ["snmp_timeout_sec"] = validateNumber,
    ["hosts_only"] = validateBool,
    ["referal_url"] = validateUnquoted,
    ["disabled_status"] = validateListOfTypeInline(validateNumber),
    ["redis_command"] = validateSingleWord,
    ["flow_calls_drops"] = validateOnOff,
    ["global_flow_calls_drops"] = validateOnOff,
    ["syslog_producer"] = validateSingleWord,
    ["syslog_producer_host"] = validateSingleWord,
    ["old_syslog_producer"] = validateSingleWord,
    ["old_syslog_producer_host"] = validateSingleWord,
    ["query_preset"] = validateEmptyOr(validateSingleWord),
    ["new_charts"] = validateBool,
    ["sankey_version"] = validateNumber,
    ["blacklist_name"] = validateUnquoted,
    ["reset_url"] = validateBool,
    ["as"] = validateNumber,
    ["exporter_uuid"] = validateNumber,
    ["probe_uuid"] = validateNumber,
    ["telemetry"] = validateBool,
    ["preset"] = validateSingleWord,
    ["rules"] = validateUnquoted,

    -- nEdge policy
    ["default_policy"] = validatePolicy, -- users, nedge policies
    ["policy"] = validatePolicy, -- nedge policies
    ["src_type"] = validateSingleWord,
    ["dst_type"] = validateSingleWord,
    ["src_value"] = validatePolicyValue,
    ["dst_value"] = validatePolicyValue,
    ["bidirectional"] = validateBool,

    -- nEdge repeaters
    ["repeater_type"] = validateSingleWord,
    ["repeater_id"] = validateNumber,
    ["interfaces"] = validateSingleWord,
    ["restricted_interfaces"] = validateSingleWord,
    ["interface_details"] = validateUnquoted,
    ["restricted_interface_details"] = validateUnquoted,
    ["keep_src_address"] = validateBool,

    -- Containers
    ["pod"] = validateSingleWord,
    ["container"] = validateSingleWord,

    -- Host Pools / users associations
    ["associations"] = {jsonCleanup, validateAssociations},
    ["host_visibility"] = validateSingleWord,
    ["hide"] = validateBool,

    -- json POST DATA
    ["payload"] = {jsonCleanup, validateJSON},
    ["JSON"] = {jsonCleanup, validateJSON},
    ["host_threshold_rules"] = {jsonCleanup, validateJSON},
    ["content"] = {jsonCleanup, validateJSON},

    -- POST pcap
    ["pcap"] = validatePcap,
    ["uploaded_file"] = validateUploadedFile,
    ["create_new_iface"] = validateBool,

    -- See https://github.com/ntop/ntopng/issues/4275
    ["csrf"] = validateSingleWord,

    -- cloud
    ["params"] = {jsonCleanup, validateJSON},
    ["msg"] = {jsonCleanup, validateJSON},

    -- server side Datatables
    ["start"] = validateNumber,
    ["sort"] = validateSingleWord,
    ["order"] = validateSortOrder,
    ["length"] = validateNumber,
    ["draw"] = validateNumber,
    ["visible_columns"] = validateEmptyOr(validateListOfTypeInline(validateSingleWord)),
    ["instance_name"] = validateSingleWord, -- used in flow explorer
    ["hide_exporters_name"] = validateBool,
    ["dns_list"] = validateSingleWord,
    ["ntp_list"] = validateSingleWord,
    ["dhcp_list"] = validateSingleWord,
    ["smtp_list"] = validateSingleWord,
    ["gateway_list"] = validateSingleWord,
    ["whitelisted_networks"] = validateSingleWord,
    ["corporate_devices"] = validateSingleWord,
    ["local_devices"] = validateSingleWord

}

-- A special parameter is formed by a prefix, followed by a variable suffix
local special_parameters = { --[[Suffix validator]]
    --[[Value Validator]]
    -- The following parameter is *not* used inside ntopng
    -- It allows third-party users to write their own scripts with custom
    -- (unverified) parameters
    ["p_"] = {validateUnquoted, validateUnquoted},

    -- SHAPING
    ["shaper_"] = {validateNumber, validateNumber}, -- key: a shaper ID, value: max rate
    ["ishaper_"] = {validateShapedElement, validateNumber}, -- key: category or protocol ID, value: ingress shaper ID
    ["eshaper_"] = {validateShapedElement, validateNumber}, -- key: category or protocol ID, value: egress shaper ID
    ["qtraffic_"] = {validateShapedElement, validateNumber}, -- key: category or protocol ID, value: traffic quota
    ["qtime_"] = {validateShapedElement, validateNumber}, -- key: category or protocol ID, value: time quota
    ["oldrule_"] = {validateShapedElement, validateEmpty}, -- key: category or protocol ID, value: empty
    ["policy_"] = {validateShapedElement, validateListOfTypeInline(validateNumber)}, -- key: category or protocol ID, value: shaper,bytes_quota,secs_quota
    ["client_policy_"] = {validateShapedElement, validateListOfTypeInline(validateNumber)}, -- key: category or protocol ID, value: shaper,bytes_quota,secs_quota
    ["server_policy_"] = {validateShapedElement, validateListOfTypeInline(validateNumber)}, -- key: category or protocol ID, value: shaper,bytes_quota,secs_quota

    -- ALERTS (see alert_utils.lua)
    ["op_"] = {validateAlertDescriptor, validateOperator}, -- key: an alert descriptor, value: alert operator
    ["value_"] = {validateAlertDescriptor, validateAlertValue}, -- key: an alert descriptor, value: alert value
    ["slack_ch_"] = {validateNumber, validateSingleWord}, -- slack channel name
    ["enabled_"] = {validateAlertDescriptor, validateAlertValue}, -- key: a check module key, value: alert value

    -- Protocol to categories match
    ["proto_"] = {validateProtocolIdOrName, validateCategory},
    ["protocol"] = validateNumber,
    --
    ["static_route_address_"] = {validateStaticRouteName, validateIPV4},
    ["static_route_netmask_"] = {validateStaticRouteName, validateIPV4},
    ["static_route_via_"] = {validateStaticRouteName, validateIPV4},
    ["static_route_is_local_"] = {validateStaticRouteName, validateBool},

    -- Gateways
    ["gateway_address_"] = {validateGatewayName, validateIPV4},
    ["gateway_ping_"] = {validateGatewayName, validateIPV4},
    ["gw_rtt_"] = {validateGatewayName, validateNumber},
    ["gw_loss_thresh_"] = {validateGatewayName, validateNumber},
    ["gw_num_attempts_"] = {validateGatewayName, validateNumber},
    ["gw_id_"] = {validateNumber, validateGatewayName},
    ["pol_id_"] = {validateNumber, validateRoutingPolicyName},
    ["routing_"] = {validateRoutingPolicyGateway, validateEmptyOr(validateNumber)}, -- a routing policy

    -- DHCP Configuration
    ["dhcp_server_enabled_"] = {validateNetworkInterface, validateBool},
    ["dhcp_first_ip_"] = {validateNetworkInterface, validateIPV4},
    ["dhcp_last_ip_"] = {validateNetworkInterface, validateIPV4},
    ["dhcp_option_114_"] = {validateNetworkInterface, validateUnquoted},
    ["dhcp_option_160_"] = {validateNetworkInterface, validateUnquoted},

    -- Network Configuration
    ["iface_mode_"] = {validateNetworkInterface, validateInterfaceConfMode},
    ["iface_ip_"] = {validateNetworkInterface, validateIPV4},
    ["iface_on_"] = {validateNetworkInterface, validateBool},
    ["iface_gw_"] = {validateNetworkInterface, validateIPV4},
    ["iface_netmask_"] = {validateNetworkInterface, validateIPV4},
    ["iface_primary_dns_"] = {validateNetworkInterface, validateIPV4},
    ["iface_secondary_dns_"] = {validateNetworkInterface, validateIPV4},
    ["iface_nat_"] = {validateNetworkInterface, validateBool},
    ["iface_id_"] = {validateNumber, validateNetworkInterface},
    ["iface_up_"] = {validateNumber, validateNumber},
    ["iface_down_"] = {validateNumber, validateNumber},

    -- paramsPairsDecode: NOTE NOTE NOTE the "val_" value must explicitly be checked by the end application
    ["key_"] = {validateNumber, validateUnchecked}, -- key: an index, value: the pair key
    ["val_"] = {validateNumber, validateUnchecked}, -- key: an index, value: the pair value

    -- server side Datatables
    ["columns_"] = {validateNumber, validateBool},
    ["search_"] = {validateNumber, validateSingleWord},
    ["order_"] = {validateNumber, validateSingleWord},

    -- only client parameters
    ["timeseries_groups"] = {validateUnchecked, validateUnchecked},
    ["timeseries_groups_mode"] = {validateUnchecked, validateUnchecked},
    ["subnet"] = {validateUnchecked, validateUnchecked}
}

-- #################################################################

local function validateParameter(k, v)
    local debug = false
    local trace_failures = true

    if (debug) then
        io.write("[LINT] validateParameter [" .. k .. "] " .. type(v) .. "\n")
    end

    if (known_parameters[k] == nil) then
        -- Attempt at recursively validate tables
        if (type(v) == "table") then
            for table_key, table_value in pairs(v) do
                local success, message, purified = validateParameter(table_key, table_value)
                -- Stop, if any of the table value fails the validation against
                -- the expected table key
                if not success then
                    if (trace_failures and table_key ~= 'ifid') then
                        io.write(
                            "[LINT] validateParameter failed for table entry [" .. table_key .. "][" .. table_value ..
                                "]\n")
                    end

                    return success, message, nil
                end
            end

            -- Success, all the table keys have been validated successfully
            return true, "OK", v
        elseif k:starts('custom_fields') then
            -- Possible error, try the last check for historical custom_fields
            local tmp_1 = k:split("_")
            local i = 3
            while (tmp_1[i]) do
                if not tonumber(tmp_1[i]) then
                    return false, nil
                end
                i = i + 1
            end
            local success = validateListOfTypeInline(validateFilters(validateSingleWord))(v)
            return success, nil, v
        else
            if (trace_failures) then
                error("[LINT] Validation error: Unknown key '" .. k .. "' [" .. v .. "]: missing validation perhaps?\n")
            end
            return false, nil
        end
    else
        local ret
        local f = known_parameters[k]

        if (type(f) == "function") then
            -- We apply the default parameter cleanup
            v = ntop.httpPurifyParam(tostring(v))
            ret = known_parameters[k](v)
        else
            -- We apply the custom cleanup
            v = known_parameters[k][1](v)
            ret = known_parameters[k][2](v)
        end

        if ret then
            return true, "OK", v
        else
            if (trace_failures and k ~= 'ifid') then
                io.write("[LINT] validateParameter failed for parameter [" .. k .. "][" .. type(f) .. "]\n")
                tprint(v)
            end

            -- io.write(debug.traceback())
            return false, "Validation error", nil
        end
    end
end

local function validateSpecialParameter(param, value)
    -- These parameters are made up of one string prefix plus a string suffix
    for k, v in pairs(special_parameters) do
        if starts(param, k) and not known_parameters[param] --[[ make sure this is not a known, non-special param --]] then
            local suffix = split(param, k)[2]
            value = ntop.httpPurifyParam(value)

            if not v[1](suffix) then
                return false, "Special Validation, parameter key", nil
            elseif not v[2](value) then
                return false, "Special Validation, parameter value", nil
            else
                return true, "OK", value
            end
        end
    end

    return false
end

-- ##############################################

function http_lint.validationError(t, param, value, message)
    -- TODO graceful exit
    local s_id
    if t == _GET then
        s_id = "_GET"
    else
        s_id = "_POST"
    end

    -- Remove the param which failed the validation
    t[param] = nil
end

-- #################################################################

local function lintParams()
    local script_manager = require("script_manager")
    local params_to_validate = {_GET, _POST}
    local id, _, k, v

    -- VALIDATION SETTINGS
    local enableValidation = true --[[ To enable validation ]]
    local relaxGetValidation = true --[[ To consider empty fields as valid in _GET parameters ]]
    local relaxPostValidation = false --[[ To consider empty fields as valid in _POST parameters ]]
    local debug = false --[[ To enable validation debug messages ]]

    -- Extend the parameters with validators from the scripts
    local additional_params = script_manager.extendLintParams(http_lint, known_parameters)
    for _, id in pairs(params_to_validate) do
        for k, v in pairs(id) do
            if (false) then
                io.write("[LINT] Validating [" .. k .. "]\n")
            end

            if enableValidation then
                if ((v == "") and (((id == _GET) and relaxGetValidation) or ((id == _POST) and relaxPostValidation))) then
                    if (debug) then
                        io.write("[LINT] Parameter " .. k .. " is empty but we are in relax mode, so it can pass\n")
                    end
                else
                    local success, message, purified = validateSpecialParameter(k, v)
                    if success then
                        id[k] = purified
                    else
                        if message ~= nil then
                            -- tprint("k: "..k.. " v: "..v.. " success: "..tostring(success).. " message: "..message)

                            if (debug) then
                                io.write("[LINT] Failure validating parameter '" .. k .. "' (1)\n")
                            end

                            http_lint.validationError(id, k, v, message)
                        else
                            success, message, purified = validateParameter(k, v)

                            if success then
                                id[k] = purified
                            else
                                if (debug) then
                                    io.write("[LINT] Failure validating parameter '" .. k .. "' (2)\n")
                                end

                                if message ~= nil then
                                    http_lint.validationError(id, k, v, message)
                                else
                                    -- Here we have an unhandled parameter
                                    http_lint.validationError(id, k, v, "Missing validation")
                                end
                            end
                        end
                    end
                end
            end
        end
    end
end

-- #################################################################

--
-- In case of forms submitted as "application/x-www-form-urlencoded" the received JSON is
-- stored in _POST["payload"] unverified. The function below, parses JSON and puts it
-- in the corresponding _GET/_POST parameters
-- See also https://github.com/ntop/ntopng/issues/4113
--
local function parsePOSTpayload()
    if ((_POST ~= nil) and (_POST["payload"] ~= nil)) then
        local info, pos, err = json.decode(_POST["payload"], 1, nil)

        if (info ~= nil) then
            for k, v in pairs(info) do
                _GET[k] = v -- TODO: remove as soon as REST API is clean (https://github.com/ntop/ntopng/issues/4113)
                _POST[k] = v
            end
        end
    end
end

-- #################################################################

local function clearNotAllowedParams()
    if ntop.isnEdge() then
        -- Captive portal urls that can be clobbered with unrecognized
        -- and unvalid params as devices could have http requests open that are redirected
        -- to the captive portal.
        -- This function removes all the params except a minimum allowed set.
        local not_allowed_uris = {"/lua/info_portal.lua", "/lua/captive_portal.lua"}
        -- the referer must go through or the captive portal won't be able to do
        -- a proper redirect
        local allowed_params = {
            referer = 1
        }

        if ((_GET and table.len(_GET) > 0) or (_POST and table.len(_POST) > 0)) and _SERVER["URI"] then
            for _, uri in pairs(not_allowed_uris) do
                if string.ends(uri, _SERVER["URI"]) then
                    local param_tables = {_GET or {}, _POST or {}}

                    for _, param_table in pairs(param_tables) do
                        for param_key, param_value in pairs(param_table) do
                            if not allowed_params[param_key] then
                                param_table[param_key] = nil
                            end
                        end
                    end

                    break
                end
            end
        end
    end
end

-- #################################################################

if (not pragma_once_http_lint) then
    if (ignore_post_payload_parse == nil) then
        parsePOSTpayload()
    end

    clearNotAllowedParams()
    lintParams()
    pragma_once_http_lint = true
end

if (trace_script_duration ~= nil) then
    io.write(debug.getinfo(1, 'S').source .. " executed in " .. (os.clock() - clock_start) * 1000 .. " ms\n")
end

return http_lint
