--
-- (C) 2013-24 - ntop.org
--

--
-- This file implements some utility functions used by the REST API
-- in the vulnerability pages
--
--
-- https://geekflare.com/nmap-vulnerability-scan/
-- cd /usr/share/nmap/scripts/
-- git clone https://github.com/scipag/vulscan.git
-- ln -s `pwd`/scipag_vulscan /usr/share/nmap/scripts/vulscan
-- cd vulscan/utilities/updater/
-- chmod +x updateFiles.sh
-- ./updateFiles.sh
--
-- Example:
-- nmap -sV --script vulscan --script-args vulscandb=openvas.csv <target> -p 80,233
--
--
-- exploitdb.csv
-- osvdb.csv
-- securitytracker.csv
-- openvas.csv
-- scipvuldb.csv
-- xforce.csv
-- securityfocus.csv
-- cve.csv
--

-- **********************************************************

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

require "lua_utils" -- used by tprint (debug)

-- redis key with info found with scans
local host_to_scan_key                    = "ntopng.vs.hosts.scanned_values"
-- redis key with the current configuration
local prefs_host_values_key               = "ntopng.prefs.vs.hosts_conf"

-- redis key for periodic scan in progress
local periodic_scan_key                   = "ntopng.vs.periodic_scan"
local periodic_scan_type_key              = "ntopng.vs.periodic_scan_type"

-- redis key for scann all in progress
local ondemand_scan_key                   = "ntopng.vs.scan_all"
-- redis key for single scan in progress 
local single_scan_key                     = "ntopng.vs.single_scan"

-- redis keys to handle scan in progress or scheduled
local scanned_hosts_count_key             = "ntopng.prefs.host_to_scan.count_scanned"
local scanned_hosts_changes_queue_key     = "ntopng.alerts.scanned_hosts_changes"
local host_in_scanning_hash_key           = "ntopng.vs.hosts.in_scanning"
local host_scan_queue_key                 = "ntopng.vs.scan_queue"

-- redis key for last scan report dates
local hosts_scan_last_report_dates        = "ntopng.vs.report_dates"

-- redis keys for periodic scan info
local periodic_scan_host_info_key         = "ntopng.vs.periodic_scan.info"

-- redis keys for scan all info
local ondemand_scan_host_info_key         = "ntopng.vs.scan_all.info"

-- redis key for single scan info
local single_scan_info_key                = "ntopng.vs.single_scan.info"

-- imports
local json = require("dkjson")
local format_utils = require("format_utils")
local recipients = require("recipients")
local cve_utils = require("cve_utils")
local vs_db_utils = require("vs_db_utils")
local vs_rest_utils = require("vs_rest_utils")

-- Enable debug with:
-- redis-cli set "ntopng.prefs.vs.debug_enabled" "1"
-- systemctl restart ntopng
local debug_me = ntop.getCache("ntopng.prefs.vs.debug_enabled") == "1"
local verbose = false

local vs_utils = {}

local use_slow_scan;

-- **********************************************************
-- Function to retrieve host hash key used on redis

function vs_utils.get_host_hash_key(host, scan_type)
   return string.format("%s-%s", host, scan_type)
end

-- **********************************************************

-- Scan status
vs_utils.scan_status = {
   error       = 0,
   ok          = 1,
   scheduled   = 2,
   not_scanned = 3,
   scanning    = 4,
   failed      = 5,
}

-- Scan execution type
vs_utils.scan_in_exec_type = {
   single_scan = 1,
   scan_all = 2,
   periodic_scan = 3
}

-- **********************************************************
-- Function to retrieve nmap path on OS
function vs_utils.get_nmap_path()
   local path = {
      "/usr/bin/nmap",
      "/usr/local/bin/nmap",
      "/opt/homebrew/bin/nmap"
   }

   for _,p in pairs(path) do
      if(ntop.exists(p)) then
         return(p..use_slow_scan)
      end
   end

   return(nil)
end

-- **********************************************************
-- Function to check if nmap is installed
function vs_utils.is_nmap_installed()
   local module_path = {
      "/usr/share/nmap/scripts/",
      "/opt/homebrew/share/nmap/scripts/vulscan/",
      "/usr/local/share/nmap/scripts/vulscan",
   }
   local path = vs_utils.get_nmap_path()

   if(path ~= nil) then
      for _,m in pairs(module_path) do
         if(ntop.exists(m)) then
            return true
         end
      end
   end

   return false
end

-- **********************************************************
-- Function to get old report path (now the reports are
-- saved on the DB)
local function get_report_path(scan_type, ip, all)
   local base_dir
   local ret = ""

   base_dir = dirs.workingdir .. "/" .. getSystemInterfaceId() .. "/vulnerability_scan"
   ntop.mkdir(base_dir)

   if (not all or all == nil) then
      ret = base_dir .. "/"..ip.."_"..scan_type..".txt"
   else
      ret = base_dir .. "/*.txt"
   end

   return(ret)
end

-- ##############################################
-- Function to split string on \n
local function lines(str)
   local result = {}

   for line in str:gmatch '[^\n]+' do
      table.insert(result, line)
   end
   return result
end


-- ##############################################
-- Function to concatenate string item list into
-- comma separated list
local function format_port_list_to_string(ports)
   local scan_ports = ""

   if (ports ~= nil and #ports > 0) then
      for index,port in ipairs(ports) do
         if (index == 1) then
            scan_ports = ""..port
         else
            scan_ports = scan_ports .. ","..port
         end
      end
   end

   return scan_ports
end

-- ##############################################
-- Function to search for an item in a list
local function find_port(port, port_list)
   local found = false

   for _,item in ipairs(port_list) do
      if (item == port) then
         found = true
         break
      end
   end
   return found
end

-- ##############################################
-- Function to check ports differences between two
-- different scans on same host
local function check_ports_diffences(num_old_ports, old_ports, num_new_ports, new_ports)
   -- num_old_ports: number of old open ports (on the host before the last scan)
   -- old_ports: list of old ports
   -- num_new_ports: number of new open ports found (with the last host scan)
   -- new_ports: list of new open ports
   local rsp = {
      trigger = true
   }
   if (num_old_ports == 0 and num_new_ports ~= 0) then
      -- new ports found no old ports closed
      rsp.open_ports = new_ports
      rsp.open_ports_num = num_new_ports
      rsp.closed_ports_num = 0
      rsp.case = 'new_ports'
   elseif(num_old_ports ~= 0 and num_new_ports == 0) then
      -- no new ports found but old ports closed found
      rsp.open_ports_num = 0
      rsp.closed_ports_num = num_old_ports
      rsp.closed_ports = old_ports
      rsp.case = 'ports_closed'
   elseif(num_old_ports ~= 0 and num_new_ports ~= 0) then
      -- case new ports and old ports closed found
      local closed_ports = {}
      local open_ports = {}

      local diff = false
      for _, item in ipairs(old_ports) do
         local is_open = find_port(item, new_ports)
         if (not is_open) then
            closed_ports[#closed_ports+1] = item
            diff = true
         end
      end

      for _, item in ipairs(new_ports) do
         local is_open = find_port(item, old_ports)
         if (not is_open) then
            open_ports[#open_ports+1] = item
            diff = true
         end
      end


      if((not diff) and (num_old_ports == num_new_ports)) then
         -- case no differences and the old ports num is equals to
         -- new ports open 
         rsp.trigger = false
      else
         rsp.open_ports = open_ports
         rsp.open_ports_num = #open_ports
         rsp.closed_ports = closed_ports
         rsp.closed_ports_num = #closed_ports

         if (#open_ports ~= 0 and #closed_ports == 0) then
            rsp.case = 'new_ports'
         elseif (#open_ports == 0 and #closed_ports ~= 0) then
            rsp.case = 'ports_closed'
         else
            rsp.case = 'ports_open_and_closed'
         end
      end
   else
      rsp.trigger = false
   end
   return rsp
end

-- ##############################################
-- Function to split ports string comma separated
-- list into array
local function split_port_list(data, is_tcp)

   if (is_tcp) then
      if (data.tcp_ports and data.tcp_ports.num_ports ~= 0) then
         return split(data.tcp_ports.ports,",")
      end
   else
      if(data.udp_ports and data.udp_ports.num_ports ~= 0) then
         return split(data.udp_ports.ports, ",")
      end
   end

   return {}
end

-- ##############################################
-- Function to save last scan result
-- in the old case it saves on the FS now with CH
-- it saves on DB
local function save_last_result(scan_result, scan_type, host, epoch, last_port_scanned_label)

   if(scan_result ~= nil and not ntop.isClickHouseEnabled()) then
      local handle = io.open(get_report_path(scan_type, host), "a")
      local result = handle:write("\n\n"..i18n("hosts_stats.page_scan_hosts.inconsistency_state", {port = last_port_scanned_label}).."\n"..scan_result)
      handle:close()
   end

   if (scan_result ~= nil and ntop.isClickHouseEnabled()) then
      vs_db_utils.update_last_result(scan_result, scan_type, host, epoch, last_port_scanned_label)
   end

end

-- ##############################################
-- Function to verify if a port is open or is a 
-- false positive
local function verify_status_ports(possible_changed_ports, host, scan_type, open_ports_case, epoch)
   local real_ports = {}
   local scan_module = vs_utils.load_module(scan_type)

   for _,possible_changed_port in ipairs(possible_changed_ports) do
      local now,result,duration,scan_result,num_open_ports,num_vulnerabilities_found, cve, udp_ports, tcp_ports = scan_module:scan_host(host, possible_changed_port)

      local scan_type_label = ternary(scan_type == "tcp_portscan", "tcp", "udp")
      local port_label_for_vs_result = string.format("%u/%s",possible_changed_port,scan_type_label)
      save_last_result(result, scan_type, host, epoch, port_label_for_vs_result)
      if debug_me then
         tprint("SCANNED AGAIN HOST: "..host.." ON PORT: "..possible_changed_port)
      end
      if (open_ports_case and num_open_ports > 0) then
         -- case open port and num_open_ports is > 0 so is a real open port
         real_ports[#real_ports+1] = possible_changed_port
         if debug_me then
            tprint("IS A REAL OPEN")
         end
      elseif (open_ports_case == false and num_open_ports == 0) then
         -- case closed port and num_open_ports is 0 so is a real closed port
         real_ports[#real_ports+1] = possible_changed_port
         if debug_me then
            tprint("IS A REAL CLOSED")
         end
      end
   end
   return real_ports
end

-- ##############################################
-- Function to analyze ports differences and build
-- an object with the info for the alert
local function analyze_ports_diff(ports_difference, host, scan_type, epoch)
   local rsp = {}
   local need_to_trigger_alert_after_changes_verification = ports_difference.trigger
   if (ports_difference.trigger) then
      if (debug_me) then
         tprint("found ports differences")
         tprint(ports_difference)
      end
      local open_ports_case =  true

      local real_open_ports = verify_status_ports(ports_difference.open_ports or {}, host, scan_type, open_ports_case, epoch )
      local real_open_port_num = ternary(real_open_ports ~= nil and next(real_open_ports), #real_open_ports, 0)
      rsp["open_ports"] = {
         num = real_open_port_num,
         ports = format_port_list_to_string(real_open_ports)
      }
      open_ports_case = false
      local real_closed_ports = verify_status_ports(ports_difference.closed_ports or {}, host, scan_type, open_ports_case, epoch)
      local real_closed_ports_num = ternary(real_closed_ports ~= nil and next(real_closed_ports), #real_closed_ports, 0)

      rsp["closed_ports"] =  {
         num = real_closed_ports_num,
         ports = format_port_list_to_string(real_closed_ports)
      }
      rsp["ports_case"] = ports_difference.case

      if ((real_closed_ports_num == ports_difference.closed_ports_num) and (real_open_port_num == ports_difference.open_ports_num)) then

         -- case ok we can trigger alert
      else
         -- false positive detected
         if (debug_me) then
            tprint("IT'S A FALSE POSITIVE!!!")
         end

         need_to_trigger_alert_after_changes_verification = false
      end
      if (debug_me) then
         tprint(ports_difference.case)
      end
   elseif (debug_me) then
      tprint("IS IT TRIGGERED: ")
      tprint(ports_difference.trigger)
   end

   rsp["triggered"] = need_to_trigger_alert_after_changes_verification

   return rsp
end

-- ##############################################
-- Function to get ports changes
local function get_ports_changes(host, scan_type, old_data, new_data)

   local is_tcp = false
   local scan_type_log_label = "UDP"

   local rsp = {}

   if(scan_type == 'tcp_portscan' or scan_type == 'tcp_openports') then
      is_tcp = true
      scan_type_log_label = "TCP"
   end

   local old_ports = split_port_list(old_data, is_tcp)
   local new_ports = split_port_list(new_data, is_tcp)

   if (debug_me) then
      tprint(scan_type_log_label.." OLD PORTS: ")
      tprint(old_ports)
      tprint(scan_type_log_label.." NEW PORTS: ")
      tprint(new_ports)
   end

   local ports_differences = check_ports_diffences(#old_ports, old_ports,
						   #new_ports, new_ports)
   local rsp_diff = analyze_ports_diff(ports_differences, host, scan_type, new_data.last_scan_time)
   if (rsp_diff.triggered) then
      rsp["open_ports"] = rsp_diff.open_ports
      rsp["closed_ports"] = rsp_diff.closed_ports
      rsp["ports_case"] = rsp_diff.ports_case

   end
   rsp["triggered"] = rsp_diff.triggered

   rsp["measurement"] = "ports_changes_detected"

   return rsp
end

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

-- This function checks the differences between an old and a new host scan
-- and return a table containing those differences
local function check_differences(host, host_name, scan_type, old_data, new_data)
   local rsp = {}

   -- security checks
   if host == nil or scan_type == nil then
      return nil
   end

   -- retrocompatibility check
   if (scan_type == 'tcp_openports') then
      scan_type = 'tcp_portscan'
   end

   local num_cve_solved = 0
   local num_new_cve_issues = 0
   local cve_solved = {}
   local new_cve = {}

   -- Checking the solved vulnerabilities
   for _, cve in ipairs(old_data.cve or {}) do
      -- the old cves have the score
      cve = split(cve,"|")[1]
      -- If the new table does not contains the cve it means that it is solved
      if not (table.contains(new_data.cve or {}, cve)) then
         num_cve_solved = num_cve_solved + 1
         -- Add at most 5 cve
         if num_cve_solved <= 5 then
            cve_solved[#cve_solved + 1] = cve
         end
      end
   end

   -- Checking the new vulnerabilities
   for _, cve in ipairs(new_data.cve or {}) do
      -- If the new table does not contains the cve it means that it is solved
      if not (table.contains(old_data.cve or {}, cve)) then
         num_new_cve_issues = num_new_cve_issues + 1
         -- Add at most 5 cve
         if num_new_cve_issues <= 5 then
            new_cve[#new_cve + 1] = cve
         end
      end
   end

   -- ***************************************************************

   -- Checking differences beetwen old and new open ports and old and new closed ports
   local differences = get_ports_changes(host, scan_type, old_data, new_data)
   rsp["triggered"] = differences.triggered
   rsp["measurement"] = "ports_changes_detected"
   local prefix_key = "udp_"
   if (scan_type == "tcp_portscan") then
      prefix_key = "tcp_"
   end
   if (differences.triggered) then
      rsp[prefix_key.."open_ports"] = differences.open_ports
      rsp[prefix_key.."closed_ports"] = differences.closed_ports
      rsp[prefix_key.."ports_case"] = differences.ports_case
   end

   -- ***************************************************************

   if num_cve_solved > 0 then
      rsp["num_cve_solved"] = num_cve_solved
      rsp["cve_solved"] = cve_solved
      rsp["measurement"] = "cve_changes_detected"
   end

   if num_new_cve_issues > 0 then
      rsp["num_new_cve_issues"] = num_new_cve_issues
      rsp["new_cve"] = new_cve
      rsp["measurement"] = "cve_changes_detected"
   end


   if table.empty(rsp) or rsp.triggered == false  then
      rsp = nil
   else
      rsp["host"] = host
      rsp["host_name"] = host_name
      rsp["scan_type"] = scan_type

      if (new_data.last_scan_time) then
         local date_string = format_utils.formatPastEpochShort(new_data.last_scan_time)
         date_string = string.gsub(date_string," ","_")
         rsp["date"] = date_string
         rsp["epoch"] = new_data.last_scan_time
      end
   end

   return rsp
end

-- ##############################################
-- Function to obtain the port from the scan result
-- line
function vs_utils.cleanup_port(is_tcp, line)
   local splitted_line = {}

   local regex =  "([^/udp]+)"

   if (is_tcp) then
      regex =  "([^/tcp]+)"
   end
   for str in string.gmatch(line, regex) do
      table.insert(splitted_line, str)
   end

   return splitted_line[1]

end

-- ##############################################
-- Function to cleanup and get info from the scan
-- result.
-- Remove the first/last few lines that contain nmap information that change at each scan
function vs_utils.cleanup_nmap_result(scan_result, scan_type)
   if(scan_result ~= nil) then
      scan_result = scan_result:gsub("|", "")
      scan_result = scan_result:gsub("_", "")

      scan_result = lines(scan_result)

      for i=1,4 do
         table.remove(scan_result, 1)
      end

      table.remove(scan_result, #scan_result)

      local num_open_ports = 0
      local num_vulnerabilities = 0
      local cve = {}
      local scan_out = {}
      local tcp_ports = {}
      local udp_ports = {}

      for _,l in pairs(scan_result) do
         -- Ignore "open|filtered" ports
         if((string.find(l, "open") ~= nil) and (string.find(l, "filtered") == nil)) then
            local t = string.find(l, "/tcp ") or 0
            local u = string.find(l, "/udp ") or 0

            if (t > 0) then
               num_open_ports = num_open_ports + 1
               tcp_ports[#tcp_ports+1] = vs_utils.cleanup_port(true, l)
            end

            if(u > 0) then
               num_open_ports = num_open_ports + 1
               udp_ports[#udp_ports+1] = vs_utils.cleanup_port(false, l)
            end
         end

         -- Escape XML/HTML code that might be present in the output
         l = l:gsub("<", "&lt;")
         l = l:gsub(">", "&gt;")

         if(string.sub(l, 1, 2) == " [") then
            local c = string.split(string.sub(l,3), "]")
            local url = cve_utils.getDocURL(c[1], scan_type)

            if(scan_type == "cve") then
	       l = '[<A HREF="'..url..'">'..c[1]..'</A>]'..c[2]
            elseif(scan_type == "openvas") then
               l = '[<A HREF="'..url..'">'..c[1]..'</A>]'..c[2]
            end

            table.insert(cve, c[1])
            num_vulnerabilities = num_vulnerabilities + 1
         end
         
         if (string.find(l, "filtered") == nil) then
            table.insert(scan_out, l)
         end
      end

      scan_result = table.concat(scan_out, "\n")
      return scan_result, num_open_ports, num_vulnerabilities, cve, udp_ports, tcp_ports
   else
      return "", 0, 0, 0, 0, 0
   end
end

-- ##############################################
-- Remove the first/last few lines that contain nmap information that change at each scan
function vs_utils.cleanup_nmap_check_host_result(scan_result)
   if(scan_result ~= nil) then

      if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "Result: "..scan_result.."\n") end

      scan_result = scan_result:gsub("|", "")
      scan_result = scan_result:gsub("_", "")

      scan_result = lines(scan_result)

      -- remove the first line and the last one
      table.remove(scan_result, 1)
      table.remove(scan_result, #scan_result)

      local is_up_and_run = false
      for _,l in pairs(scan_result) do
         -- searching for "Host is up" nmap string
         if (string.find(l, "Host is up") ~= nil) then
            is_up_and_run = true
            goto continue
         end
      end

      ::continue::
      return is_up_and_run
   else
      return false
   end
end

-- **********************************************************

-- Remove the first/last few lines that contain nmap information that change at each scan
function vs_utils.cleanup_nmap_vulners_result(scan_result, scan_type)
   scan_result = scan_result:gsub("|_", "")
   scan_result = scan_result:gsub("|", "")

   scan_result = lines(scan_result)

   for i=1,4 do
      table.remove(scan_result, 1)
   end

   table.remove(scan_result, #scan_result)

   local num_open_ports = 0
   local num_vulnerabilities = 0
   local cve = {}
   local scan_out = {}

   for _,l in pairs(scan_result) do
      if(string.find(l, "open") ~= nil) then
         local t = string.find(l, "/tcp ") or 0
         local u = string.find(l, "/udp ") or 0

         if((t > 0) or (u > 0)) then
            num_open_ports = num_open_ports + 1
         end
      end

      if(string.find(l, "https://vulners.com/") ~= nil) then
         local c = string.split(l, "\t")
         table.insert(cve, c[2])
         num_vulnerabilities = num_vulnerabilities + 1
      end

      table.insert(scan_out, l)
   end

   scan_result = table.concat(scan_out, "\n")

   return scan_result, num_open_ports, num_vulnerabilities, cve
end

-- **********************************************************

-- Function to save host configuration
local function isAlreadyPresent(item)
   local host_hash_key = vs_utils.get_host_hash_key(item.host, item.scan_type)
   local hash_prefs_string = ntop.getHashCache(prefs_host_values_key,host_hash_key)

   if (not isEmptyString(hash_prefs_string)) then
      local host_pref_value = json.decode(hash_prefs_string) 
      -- the value must not be empty
      if (host_pref_value and host_pref_value.host == item.host and host_pref_value.scan_type == host_pref_value.scan_type) then
         return true
      end
   end
   return false
end

-- **********************************************************
-- Compare function to sort CVE on the score
local function compare(a,b)

   local a_array = split(a,"|")
   local a_has_score = false
   if (#a_array > 1) then
      a_has_score = true
      a = a_array[2]
   end

   local b_array = split(b,"|")
   local b_has_score = false
   if (#b_array > 1) then
      b_has_score = true
      b = b_array[2]
   end

   if (a_has_score and b_has_score) then
      return a > b
   else
      return a_array[1] > b_array[1]
   end
end

-- **********************************************************

-- Function to format cve list with scores
local function get_cve_with_score(cve)
   local cve_with_score_list = {}
   local max_score = 0
   if(cve ~= nil) then
      for _,cve_id in ipairs(cve) do
         local score = cve_utils.getCVEscore(cve_id)

         local cve_formatted = cve
         if(score ~= nil) then

            if(max_score < score) then
               max_score = score
            end
            cve_formatted = string.format("%s|%s",cve_id,score)
         end
         cve_with_score_list[#cve_with_score_list+1] = cve_formatted
      end
   end

   if next(cve_with_score_list) then
      table.sort(cve_with_score_list, compare)
   end

   return cve_with_score_list, max_score
end

-- **********************************************************

-- Function to remove scanning host
local function remove_scanning_host(host_info)
   local host_to_scan_hash_key = vs_utils.get_host_hash_key(host_info.host, host_info.scan_type)
   ntop.delHashCache(host_in_scanning_hash_key,host_to_scan_hash_key)
end

-- **********************************************************

-- Function to set the actual scanning host on a redis key
local function save_scanning_host(scan_info)
   local host_to_scan_hash_key = vs_utils.get_host_hash_key(scan_info.host, scan_info.scan_type)
   ntop.setHashCache(host_in_scanning_hash_key, host_to_scan_hash_key, json.encode(scan_info))
end

-- **********************************************************

-- Function to select correctly redis keys on periodic or scan all
local function get_counter_periodic_all_scan_keys(exec_type)
   if (exec_type == vs_utils.scan_in_exec_type.periodic_scan) then
      return periodic_scan_host_info_key --host_periodic_scan_cve_num_key,host_periodic_scan_udp_ports_key,host_periodic_scan_tcp_ports_key
   elseif (exec_type == vs_utils.scan_in_exec_type.scan_all) then
      return ondemand_scan_host_info_key --host_scan_all_cve_num_key,host_scan_all_udp_ports_key,host_scan_all_tcp_ports_key
   elseif(exec_type == vs_utils.scan_in_exec_type.single_scan) then
      return single_scan_info_key
   end
end

-- **********************************************************
-- Function to get host id
local function get_host_id(host_details)
   local host_id =  ternary(isEmptyString(host_details.host_name),host_details.host, string.format("%s (%s)",host_details.host_name,host_details.host))
   return host_id
end

-- **********************************************************

-- Function to update counters of periodically scan or scan all
-- @param is_periodic (true -> is a periodic scan, false -> is a scan all)
local function update_scan_info_for_report(type_of_scan_execution, new_item, host_hash_key, discrepancies, was_down)
   -- select correctly redis keys
   local redis_info_key = get_counter_periodic_all_scan_keys(type_of_scan_execution)
   local info_string = ntop.getCache(redis_info_key)
   local info_json = nil

   if (info_string ~= nil) then
      info_json = json.decode(info_string)
   end

   if (info_json == nil) then
      info_json = {}
   end

   local host_id = get_host_id(new_item)
   -- handle hosts down list for email
   if (new_item.is_down) then
      local host_down = i18n("hosts_stats.page_scan_hosts.email.host_down_item", { host_id = host_id  })

      if (info_json) then
	 if(info_json.hosts_down_list == nil) then
	    info_json.hosts_down_list = {}
	 end

         info_json.hosts_down_list[host_down] = true
	 info_json.not_scanned_hosts = table.len(info_json.hosts_down_list)
      end

      goto continue
   end

   if (was_down) then

      local host_was_down = i18n("hosts_stats.page_scan_hosts.email.host_down_item", { host_id = host_id  })

      if (info_json) then
         if(info_json.hosts_was_down_list == nil) then
            info_json.hosts_was_down_list = {}
         end
     
         info_json.hosts_was_down_list[host_was_down] = true
         info_json.no_longer_down_now = table.len(info_json.hosts_was_down_list)
      end
   end

   if (new_item.discovered_hosts ~= nil and new_item.scan_type == 'ipv4_netscan') then

      if (info_json) then
         if (info_json.net_scanned == nil) then
            info_json.net_scanned = {}
         end

         if (info_json.net_scanned_no_hosts_found == nil) then
            info_json.net_scanned_no_hosts_found = {}
         end 

         if (new_item.discovered_hosts_num == 0) then
            info_json.net_scanned_no_hosts_found[new_item.host] = true
         else
            info_json.net_scanned[new_item.host] = new_item.discovered_hosts
         end
      end
   end

   -- **********************************************************

   if (new_item.num_vulnerabilities_found ~= nil) then

      if (info_json ~= {} and info_json.cves ~= nil) then
         info_json.cves = tonumber(info_json.cves) + new_item.num_vulnerabilities_found
      else
         info_json.cves = 0
      end
   end

   if (new_item.udp_ports ~= nil) then

      if (info_json ~= {} and info_json.udp_ports ~= nil) then
         info_json.udp_ports = tonumber(info_json.udp_ports) + new_item.udp_ports
      else
         info_json.udp_ports = 0
      end

   end

   if (new_item.tcp_ports ~= nil) then
      if (info_json ~= {} and info_json.tcp_ports ~= nil) then
         info_json.tcp_ports = tonumber(info_json.tcp_ports) + new_item.tcp_ports
      else
         info_json.tcp_ports = 0
      end
   end

   if (new_item.is_ok_last_scan == vs_utils.scan_status.ok) then
      if (info_json ~= {} and info_json.scanned_hosts ~= nil) then
	 -- count just in success case
	 info_json.scanned_hosts = tonumber(info_json.scanned_hosts) + 1
      else
         info_json.scanned_hosts = 1
      end
   end

   if (info_json ~= {} and info_json.begin_epoch == nil) then
      info_json.begin_epoch = os.time()
   end

   if discrepancies then
      -- HAD DISCREPANCY INFO TO EMAIL REPORT
      local cve_case = false
      if (info_json ~= {} and info_json.num_cve_solved ~= nil and new_item.scan_type == 'cve') then
         info_json.num_cve_solved = tonumber(info_json.num_cve_solved or 0) + tonumber(discrepancies.num_cve_solved or 0)
         cve_case = true
      end

      local host_discrepancies_details = ""
      local port_type = "TCP"
      local prefix_key = "tcp_"
      if (new_item.scan_type == 'udp_portscan' and info_json ~= {}) then
         port_type = "UDP"
         prefix_key = "udp_"
      end


      if (not cve_case and info_json ~= {}) then
         -- DISCREPANCY PORTS CASES
         info_json.discrepancy_case = 'ports'

         if (tonumber(discrepancies[prefix_key.."open_ports"].num or 0) > 0) then
            -- take only open ports not the closed ports
            info_json.new_open_ports = tonumber(info_json.new_open_ports or 0) + tonumber(discrepancies[prefix_key.."open_ports"].num or 0)
            host_discrepancies_details = i18n("hosts_stats.page_scan_hosts.email.host_port_discrepancy_description", {
						 host_id = host_id,
						 port_type = port_type,
						 ports = discrepancies[prefix_key.."open_ports"].ports
            })

            if (isEmptyString(info_json.hosts_discrepancies_details)) then
               info_json.hosts_discrepancies_details = host_discrepancies_details
            else
               info_json.hosts_discrepancies_details = info_json.hosts_discrepancies_details .. host_discrepancies_details
            end
         end
      end

      if (cve_case and info_json ~= {} and info_json.num_cve_solved and info_json.num_cve_solved > 0) then
         -- DISCREPANCY CVE CASE
         info_json.discrepancy_case = 'cves'

         local cve_string = format_port_list_to_string(discrepancies.cve_solved)
         host_discrepancies_details = i18n("hosts_stats.page_scan_hosts.email.host_cve_discrepancy_description", {
					      host_id = host_id,
					      cves = cve_string,
         })

         if (isEmptyString(info_json.hosts_discrepancies_details)) then
            info_json.hosts_discrepancies_details = host_discrepancies_details
         else
            info_json.hosts_discrepancies_details = info_json.hosts_discrepancies_details .. host_discrepancies_details
         end
      end

      if (debug_me) then
         if (info_json) then
            tprint(info_json.hosts_discrepancies_details)
         end
      end

   end

   ::continue::
   ntop.setCache(redis_info_key, json.encode(info_json))
end

-- **********************************************************

-- Function to restore scanning host
function vs_utils.restore_host_to_scan()
   local hash_keys = ntop.getHashKeysCache(host_in_scanning_hash_key)

   if hash_keys then
      for k in pairs(hash_keys) do
         local hash_value_string = ntop.getHashCache(host_in_scanning_hash_key, k)

         if (not isEmptyString(hash_value_string)) then
            local host_info_to_restore = json.decode(hash_value_string)

            if (host_info_to_restore) then
               -- enqueue to scan
               ntop.lpushCache(host_scan_queue_key, hash_value_string)

               -- set status to scheduled
               vs_utils.set_status_scan(host_info_to_restore.scan_type, host_info_to_restore.host, host_info_to_restore.ports,
                                        host_info_to_restore.id, host_info_to_restore.is_periodicity, host_info_to_restore.is_all, host_info_to_restore.is_single_scan,
                                        vs_utils.scan_status.scheduled)
            end
         end
      end
   end
end

-- **********************************************************

-- Function to restore backup config
function vs_utils.restore_config_backup(vs_backup)
   -- remove old hash entries
   ntop.delCache(host_to_scan_key)
   ntop.delCache(prefs_host_values_key)

   for _,item in ipairs(vs_backup) do
      -- restoring hash entries with status not scanned
      local host_hash_key = vs_utils.get_host_hash_key(item.host, item.scan_type)

      local item_to_restore = item
      ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(item_to_restore))
      ntop.setHashCache(prefs_host_values_key, host_hash_key, json.encode(item_to_restore))

   end
end

-- *********************************************************

-- Function to retrieve hosts keys for backups
function vs_utils.retrieve_hosts_backup()
   local hash_keys = ntop.getHashKeysCache(prefs_host_values_key)
   local rsp = {}
   if hash_keys then
      for k in pairs(hash_keys) do
         local hash_value_string = ntop.getHashCache(prefs_host_values_key, k)
         if (not isEmptyString(hash_value_string)) then
            local hash_value = json.decode(hash_value_string)
            rsp[#rsp+1] = hash_value
         end
      end
   end

   return rsp
end

-- **********************************************************
-- Function to save host on configuration
function vs_utils.add_host_pref(scan_type, host, ports, scan_frequency, discovered_host_scan_type, cidr)

   local host_hash_key = vs_utils.get_host_hash_key(host, scan_type)

   local new_item = {
      host = host,
      host_name = host_name,
      scan_type = scan_type,
      ports = ports,
      cidr = cidr,
      discovered_host_scan_type = discovered_host_scan_type
   }
   if not isEmptyString(scan_frequency) then
      new_item.scan_frequency = scan_frequency
   end

   local result = 1 -- success
   if (debug_me) then
      tprint("SAVING HOST: "..new_item.host)
   end

   --saved_hosts[#saved_hosts+1] = new_item
   ntop.setHashCache(prefs_host_values_key, host_hash_key, json.encode(new_item))

   return result
end

-- *********************************************************
-- Function to edit host on configuration
function vs_utils.edit_host_pref(scan_type, host, ports, scan_frequency, discovered_host_scan_type)
   local host_hash_key = vs_utils.get_host_hash_key(host, scan_type)
   local old_item_string = ntop.getHashCache(prefs_host_values_key,host_hash_key)
   if (not isEmptyString(old_item_string)) then
      local old_item = json.decode(old_item_string)
      old_item.ports = ports
      old_item.scan_frequency = scan_frequency
      old_item.discovered_host_scan_type = discovered_host_scan_type

      ntop.setHashCache(prefs_host_values_key, host_hash_key, json.encode(old_item))
      return 1 --ok
   end
   return 2 -- not found
end

-- **********************************************************
-- Function to trigger alert when an host is now down
local function trigger_alert_host_down(host,host_name, epoch)
   local host_info_to_cache = {
      host = host,
      host_name =  host_name,
      epoch = epoch,
      is_up_check_case = true,
   }
   ntop.rpushCache(scanned_hosts_changes_queue_key, json.encode(host_info_to_cache))

end

-- **********************************************************
-- Function to trigger alert when an host is not configured
local function trigger_alert_host_not_configured(host,scan_type)
   local host_info_to_cache = {
      host = host,
      sub_scan_type = scan_type,
      is_host_not_configured = true,
   }
   ntop.rpushCache(scanned_hosts_changes_queue_key, json.encode(host_info_to_cache))

end

-- **********************************************************
-- Function to update host scan values
function vs_utils.save_host_to_scan(scan_type, host, scan_result, last_scan_time, last_duration,
                                    is_ok_last_scan, ports, scan_frequency, num_open_ports,
                                    num_vulnerabilities_found, cve, id, is_edit, udp_ports, tcp_ports, discovered_hosts)
   local checks = require "checks"
   local trigger_alert = checks.isCheckEnabled("active_monitoring", "vulnerability_scan")
      or checks.isCheckEnabled("system", "vulnerability_scan")
   local host_hash_key = vs_utils.get_host_hash_key(host, scan_type)
   local old_data_string = ntop.getHashCache(host_to_scan_key, host_hash_key)
   local old_data = json.decode(old_data_string)

   -- Getting the hostname, the only way is to scan all the interfaces and retrieve it
   local host_name = ntop.resolveName(host)
   if host_name == host then
      host_name = ""
   end
   -- In case the alert needs to be triggered, save the differences in order to lessen
   -- the info dropped on redis
   -- if is_ok_last_scan is nil then no prior scan was done, so do not trigger the alert


   local epoch_id = 0
   if isEmptyString(id) then
      local key = "ntopng.prefs.last_host_id"
      local res = ntop.incrCache(key)
      epoch_id = res
   else
      epoch_id = id
   end
   local is_down = false
   if (isEmptyString(is_ok_last_scan)) then
      -- first time saved without scan
      -- check if possible
      is_ok_last_scan = vs_utils.scan_status.not_scanned
   elseif (is_ok_last_scan == vs_utils.scan_status.failed and trigger_alert) then
      -- case host is not up and running, possible just in TCP/UDP portscan

      trigger_alert_host_down(host,host_name,last_scan_time)

      is_down = true
   end

   local cve_formatted, max_score_cve = get_cve_with_score(cve)


   local new_item = {
      num_open_ports = num_open_ports,
      num_vulnerabilities_found = num_vulnerabilities_found,
      cve = cve_formatted,
      max_score_cve = max_score_cve,
      is_ok_last_scan = is_ok_last_scan,
      host_name = host_name
   }

   
   new_item.is_down = is_down
   -- on a specific entry the bool was_down is enabled when
   -- old_data is_down true -> (previous scan the host was down)
   -- new_data is_down false -> (actual scan the host is reachable)
   -- otherwise was_down is not configured selecting nil value
   local was_down = ((old_data and toboolean(old_data.is_down) == true and (not new_item.is_down))) or nil

   new_item.was_down = was_down

   local discovered_hosts_comma_list_string = ""
   if (discovered_hosts ~= nil) then
      discovered_hosts_comma_list_string = table.concat(discovered_hosts,",")
   end
   new_item.discovered_hosts = discovered_hosts_comma_list_string
   new_item.discovered_hosts_num = table.len(discovered_hosts) or 0

   if tcp_ports ~= nil then
      new_item.tcp_ports = tcp_ports.num_ports
      new_item.tcp_ports_list = tcp_ports.ports
   end

   if udp_ports ~= nil then
      new_item.udp_ports = udp_ports.num_ports
      new_item.udp_ports_list = udp_ports.ports
   end

   if (udp_ports == nil and tcp_ports == nil) then
      new_item.tcp_ports = num_open_ports
   end

   if last_scan_time or last_duration then
      --local time_formatted = format_utils.formatEpoch(last_scan_time)
      if (last_duration == nil) or (last_duration <= 0) then
         last_duration = 1
      end

      last_duration = secondsToTime(last_duration)
      new_item.last_scan = {
         epoch = last_scan_time,
         --time  = time_formatted,
         duration = last_duration,
         duration_epoch = last_duration
      }

      if is_ok_last_scan == vs_utils.scan_status.ok then
         new_item.is_ok_last_scan = vs_utils.scan_status.ok
      end
   end

   if not isEmptyString(scan_frequency) then
      new_item.scan_frequency = scan_frequency
   elseif old_data and not isEmptyString(old_data.scan_frequency) then
      new_item.scan_frequency = old_data.scan_frequency
   end

   -- the is_periodicity, is_all and is_single_scan params are set outside the save_host_to_scan into the set_status method
   if (old_data and old_data.is_periodicity ~= nil) then
      new_item.is_periodicity = old_data.is_periodicity
   end

   if (old_data and old_data.is_all ~= nil) then
      new_item.is_all = old_data.is_all
   end

   if (old_data and old_data.is_single_scan ~= nil) then
      new_item.is_single_scan = old_data.is_single_scan
   end

   if(scan_result ~= nil and not ntop.isClickHouseEnabled()) then
      local handle = io.open(get_report_path(scan_type, host), "w")
      local result = handle:write(scan_result)
      handle:close()
   end

   local result = 1 -- success

   if (debug_me) then
      -- traceError(TRACE_NORMAL, TRACE_CONSOLE, "Updating host " .. host)
   end

   -- add ports statistics comparing the ntopng passive monitoring info
   if (scan_type == "tcp_portscan" or scan_type == "tcp_openports" or scan_type == "udp_portscan") then
      local compare_info = vs_rest_utils.compare_scan_info_ntopng_info(host, scan_type, new_item.tcp_ports_list, new_item.udp_ports_list)

      new_item.host_in_mem = compare_info.host_in_mem
      local prefix_key = "udp_"
      if (scan_type == "tcp_portscan" or scan_type == "tcp_openports") then
         prefix_key = "tcp_"
      end

      new_item[prefix_key.."ports_unused"] = compare_info[prefix_key.."ports_unused"]
      new_item[prefix_key.."ports_filtered"] = compare_info[prefix_key.."ports_filtered"]
      new_item[prefix_key.."ports_case"] = compare_info[prefix_key.."ports_case"]

   end
   ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(new_item))
   local counts = vs_utils.update_ts_counters()

   local host_hash_key = vs_utils.get_host_hash_key(host, scan_type)
   local hash_prefs_string = ntop.getHashCache(prefs_host_values_key,host_hash_key)
   -- hash value found
   local hash_pref_value = json.decode(hash_prefs_string)
   if (hash_pref_value ~= nil) then
      -- is necessary this check to avoid to save entries not presents in the config and to save all data of the entry.
      for key,value in pairs(hash_pref_value) do
         if (key ~= 'is_ok_last_scan') then
            new_item[key] = value
         end
      end

      vs_db_utils.save_vs_result(scan_type, host, new_item.last_scan.epoch, json.encode(new_item), scan_result)
   end

   local host_info_differences

   if trigger_alert and old_data and (not is_edit)  and
      -- old scan and new scan must be successfully to perform discrepancies check
      -- check only on host up
      (old_data.is_down == false and new_item.is_down == false)
   then
      local already_scanned = (old_data.last_scan and old_data.last_scan.epoch)

      if already_scanned then

         if debug_me then
            -- traceError(TRACE_NORMAL,TRACE_CONSOLE, "Vulnerability Scan: checking for changes in host")
         end

         local old_cve_no_score = {}
         for _,cve in ipairs(old_data.cve) do
            old_cve_no_score[#old_cve_no_score+1] = split(cve,"|")[1]
         end

         local host_info_to_cache = check_differences(host, host_name,
                                                      scan_type,
                                                      {
                                                         vulnerabilities = old_data.num_vulnerabilities_found,
                                                         ports = old_data.num_open_ports,
                                                         cve = old_cve_no_score,
                                                         tcp_ports = {num_ports = old_data.tcp_ports, ports = old_data.tcp_ports_list },
                                                         udp_ports = {num_ports = old_data.udp_ports, ports = old_data.udp_ports_list},
                                                         old_epoch = old_data.last_scan.epoch
                                                      },
                                                      {
                                                         vulnerabilities = num_vulnerabilities_found,
                                                         ports = num_open_ports,
                                                         cve = cve,
                                                         tcp_ports = tcp_ports,
                                                         udp_ports = udp_ports,
                                                         last_scan_time = last_scan_time

	 })
         if host_info_to_cache then
            host_info_differences = host_info_to_cache

            if debug_me then
               traceError(TRACE_NORMAL,TRACE_CONSOLE, "Vulnerability Scan detected change: enqueueing event to vulnerability_scan check\n")
            end

            ntop.rpushCache(scanned_hosts_changes_queue_key, json.encode(host_info_to_cache))
         end
      end
   end
   
   if (new_item.is_periodicity) then
      update_scan_info_for_report(vs_utils.scan_in_exec_type.periodic_scan, new_item, host_hash_key, host_info_differences, was_down)
   end

   if (new_item.is_all) then
      update_scan_info_for_report(vs_utils.scan_in_exec_type.scan_all, new_item, host_hash_key, host_info_differences, was_down)
   end

   remove_scanning_host({host=host, scan_type=scan_type, ports=ports})

   return result, new_item.id
end

-- **********************************************************
-- Function to update timeseries data
function vs_utils.update_ts_counters()
   local hosts_details = vs_utils.retrieve_hosts_to_scan()
   local count_cve         = 0
   local hosts_scanned
   local open_ports_count  = 0
   local hosts_count       = 0

   for _,item in ipairs(hosts_details) do

      hosts_count = hosts_count + 1

      if item.num_open_ports ~= nil then
         open_ports_count = open_ports_count + item.num_open_ports
      end
      if item.num_vulnerabilities_found ~= nil then
         count_cve = count_cve + item.num_vulnerabilities_found
      end

   end

   local count = ntop.getCache(scanned_hosts_count_key)
   if (not isEmptyString(count)) then
      hosts_scanned = tonumber(count)
   end

   local response = {
      cve_count = count_cve,
      scanned_hosts = hosts_scanned,
      open_ports = open_ports_count,
      hosts_count = hosts_count
   }

   return response
end

-- **********************************************************

-- Function to format num for emails
-- @param case: 0 - cve, 1 - udp, 2 - tcp, 3 - scanned_hosts, 4 - hosts unreachable, 5 - no hosts down now
local function format_num_for_email(num, case)
   local formatted_num = format_high_num_value_for_tables({num = num}, "num")

   if (case == 0) then
      -- cve
      if (num == 0) then
         return(i18n("hosts_stats.page_scan_hosts.email.no_cves"))
      else
         return(i18n("hosts_stats.page_scan_hosts.email.num_cves", {num = formatted_num}))
      end
   elseif (case == 1) then
      -- udp
      if (num == 0) then
         return(i18n("hosts_stats.page_scan_hosts.email.no_udp"))
      else
         return(i18n("hosts_stats.page_scan_hosts.email.num_udp", {num = formatted_num}))
      end
   elseif (case == 2) then
      -- tcp
      if (num == 0) then
         return(i18n("hosts_stats.page_scan_hosts.email.no_tcp"))
      else
         return(i18n("hosts_stats.page_scan_hosts.email.num_tcp", {num = formatted_num}))
      end

   elseif (case == 3) then
      -- scanned_hosts

      if (num == 0) then
         return(i18n("hosts_stats.page_scan_hosts.email.no_scanned_hosts"))
      else
         return(i18n("hosts_stats.page_scan_hosts.email.num_scanned_hosts", {num = formatted_num}))
      end
   elseif (case == 4) then
      -- not scanned_hosts  --> hosts unreachable

      if (num == 0) then
         return(i18n("hosts_stats.page_scan_hosts.email.num_failed_scanned_hosts", {num = 0}))
      else
         return(i18n("hosts_stats.page_scan_hosts.email.num_failed_scanned_hosts", {num = formatted_num}))
      end
   elseif (case == 5) then 
      if (num == 0) then
         return ""
      else 
         return(i18n("hosts_stats.page_scan_hosts.email.num_no_longer_down_now", {num = formatted_num}))
      end
   end

end

-- **********************************************************
-- Function to retrieve info for email notification
local function retrieve_email_info(exec_type)
   local info_redis_key = get_counter_periodic_all_scan_keys(exec_type)

   local info_string = ntop.getCache(info_redis_key)

   local info_json = nil
   if (info_string ~= nil) then
      info_json = json.decode(info_string) or {}
   else
      info_json = {
         cves = 0,
         udp_ports = 0,
         tcp_ports = 0
      }
   end

   if(debug_me) then
      tprint("----- INFO JSON -------")
      tprint(info_json)
   end

   local email_info = {
      cve_num = tonumber(info_json.cves) or 0,
      udp_ports = tonumber(info_json.udp_ports) or 0,
      tcp_ports = tonumber(info_json.tcp_ports) or 0,
      scanned_hosts = tonumber(info_json.scanned_hosts) or 0,
      not_scanned_hosts = tonumber(info_json.not_scanned_hosts) or 0,
      no_longer_down_now = tonumber(info_json.no_longer_down_now) or 0,
      begin_epoch_t = tonumber(info_json.begin_epoch),
      end_epoch_t   = os.time(),
      report_type = exec_type,
      -- has_discrepancy must be true only if there are new open ports or cves fixed
      has_discrepancy = ((info_json.new_open_ports or 0) > 0) or ((info_json.num_cve_solved or 0) > 0),
      hosts_down_list = info_json.hosts_down_list,
      hosts_was_down_list = info_json.hosts_was_down_list,
      scanned_networks = info_json.net_scanned or {},
      scanned_networks_no_hosts_found = info_json.net_scanned_no_hosts_found or {}
   }

   if (email_info.has_discrepancy) then
      email_info.new_open_ports = info_json.new_open_ports or 0
      email_info.fixed_cves = info_json.num_cve_solved or 0
      email_info.discrepancies_details = tostring(info_json.hosts_discrepancies_details)
   end

   email_info.duration = email_info.end_epoch_t - email_info.begin_epoch_t

   ntop.setCache(info_redis_key,json.encode({
		       cves = 0,
		       udp_ports = 0,
		       tcp_ports = 0,
		       begin_epoch = 0,
		       scanned_hosts = 0,
		       not_scanned_hosts = 0,
             no_longer_down_now = 0,
   }))
   return email_info
end

-- **********************************************************
-- Function to retrieves report info

local function retrieve_report_info(date)
   local host_scanned_info = vs_utils.retrieve_hosts_to_scan()
   local info = {
      cves = 0,
      tcp_ports = 0,
      udp_ports = 0,
      scanned_hosts = 0,
      not_scanned_hosts = 0
   }

   for _, item in ipairs(host_scanned_info) do
      if (not isEmptyString(item.num_vulnerabilities_found)) then
         info.cves = info.cves + tonumber(item.num_vulnerabilities_found or 0)
      end

      info.tcp_ports = info.tcp_ports + tonumber(item.tcp_ports or 0)
      info.udp_ports = info.udp_ports + tonumber(item.udp_ports or 0)

      -- plus 1 because start from 0
      info.scanned_hosts = info.scanned_hosts + 1
      info.not_scanned_hosts = info.not_scanned_hosts + 1
   end

   if (date) then
      -- case of periodic scan and scan all
      -- in order to save same date on email and report
      info.date = tonumber(date)
   else
      info.date = tonumber(os.time())
   end

   local info_date_formatted = tostring(formatEpoch(info.date))
   info.name = "Report of "..info_date_formatted

   info.all_data_details = host_scanned_info
   
   return info
end

-- **********************************************************
-- params date used only in case of periodic or scan all exec
function vs_utils.generate_report(date)

   local report_info = retrieve_report_info(date)

   vs_db_utils.save_report_info(report_info)

   return report_info

end

-- **********************************************************
-- Function to format open port for email notification
local function add_ports_open_for_email_report(l4_key_prefix, host_details)
   if (tonumber(host_details[l4_key_prefix.."_ports"]) > 0) then
      return (i18n("hosts_stats.page_scan_hosts.email.host_details_open_ports", {
		      ports_list = host_details[l4_key_prefix.."_ports_list"],
		      l4_proto = ternary(l4_key_prefix == "tcp", "TCP", "UDP")
      }))

   end
   return nil
end

-- **********************************************************
-- Function to format hosts details for email notification
local function format_all_hosts_details_info_for_email(all_hosts_details)
   local formatted_hosts_details_string = ""
   for _,host_details in ipairs(all_hosts_details) do
      -- by default the first element is the scan type always fullfil
      local label_id_scan_type = string.format("hosts_stats.page_scan_hosts.scan_type_list.%s",host_details.scan_type)
      local scan_type_label = i18n(label_id_scan_type)
      local formatted_host_details_string = i18n("hosts_stats.page_scan_hosts.email.host_details_scan_type",{
						    scan_type = scan_type_label
      })
      local host_id = get_host_id(host_details)

      local tcp_prefix = "tcp"
      local udp_prefix = "udp"

      local tcp_ports_details = add_ports_open_for_email_report(tcp_prefix, host_details)
      if(tcp_ports_details) then
         formatted_host_details_string = formatted_host_details_string .. tcp_ports_details
      end

      local udp_ports_details = add_ports_open_for_email_report(udp_prefix, host_details)
      if(udp_ports_details) then
         formatted_host_details_string = formatted_host_details_string .. udp_ports_details
      end

      if (host_details.num_vulnerabilities_found > 0) then
         local cve_list_string = cve_utils.getFirst5(host_details.cve, host_details.scan_type, false)
         formatted_host_details_string = formatted_host_details_string .. i18n("hosts_stats.page_scan_hosts.email.host_details_cves", {cves_num = host_details.num_vulnerabilities_found, cves_list = cve_list_string})
      end

      local host_details_email_line = i18n("hosts_stats.page_scan_hosts.email.host_details", {
					      host_id = host_id,
					      details = formatted_host_details_string
      })
      formatted_hosts_details_string = formatted_hosts_details_string .. host_details_email_line

   end


   return formatted_hosts_details_string
end

-- **********************************************************

-- Function to send notification after a periodic scan
-- @param exec_type (2 -> is an all scan message, 3 -> is a periodic scan message)
-- @param periodicity (can be nil in case of scan all)
function vs_utils.notify_scan_results(exec_type, periodicity)
   local notification_message = ""

   local email_info = retrieve_email_info(exec_type)

   local title = i18n("hosts_stats.page_scan_hosts.email.vulnerability_scan_report_title",{host = getHttpHost()})
   local duration = 0
   local duration_label = ''

   if (email_info.duration ~= nil) then
      duration_label = secondsToTime(email_info.duration)
   end

   local start_date_formatted = formatEpoch(email_info.begin_epoch_t)
   local end_date_formatted   = formatEpoch(email_info.end_epoch_t)

   local report_date = tonumber(email_info.end_epoch_t)

   ntop.setCache(hosts_scan_last_report_dates, json.encode({start_date = start_date_formatted, end_date = end_date_formatted}))

   local email_body_i18n_key
   if (periodicity and periodicity == "1day") then
      -- 1 day scan case
      email_body_i18n_key = "hosts_stats.page_scan_hosts.email.periodicity_scan_1_day_ended"
   elseif (periodicity and periodicity == "1week") then
      -- 1 week scan case
      email_body_i18n_key = "hosts_stats.page_scan_hosts.email.periodicity_scan_1_week_ended"
   else
      -- on demand case
      email_body_i18n_key = "hosts_stats.page_scan_hosts.email.scan_all_ended"
   end

   local skipped_hosts_list = ""
   local no_hosts_down_br = ""

   if (email_info.hosts_down_list ~= nil) then
      local ret = ""

      for k, v in pairsByKeys(email_info.hosts_down_list, asc) do
	 ret = ret .. k .. "\n"
      end
      
      skipped_hosts_list = i18n("hosts_stats.page_scan_hosts.email.host_down_list", { host_down_items = ret })
   else
      no_hosts_down_br = "</br><br>"
   end

   notification_message = i18n(email_body_i18n_key, {
				  cves = format_num_for_email(email_info.cve_num, 0),
				  udp_ports = format_num_for_email(email_info.udp_ports, 1),
				  tcp_ports = format_num_for_email(email_info.tcp_ports, 2),
				  scanned_hosts = format_num_for_email(email_info.scanned_hosts, 3),
				  not_scanned_hosts = format_num_for_email(email_info.not_scanned_hosts or 0, 4),
				  no_hosts_down_br = no_hosts_down_br,
				  skipped_hosts_list = skipped_hosts_list,
				  duration = duration_label,
				  start_date = start_date_formatted,
				  end_date = end_date_formatted,
   })
   
   local possible_discrepancies_info = ""
   local add_br = ""

   local add_new_configured_hosts_br = true
   if (email_info.has_discrepancy) then
      
      -- ports or cves discrepancies 

      possible_discrepancies_info = i18n("hosts_stats.page_scan_hosts.email.discrepancy", {
					    new_ports_open = ternary(email_info.new_open_ports ~= 0, format_high_num_value_for_tables({num = email_info.new_open_ports}, "num"),"0"),
					    cves_fixed = ternary(email_info.fixed_cves ~= 0, format_high_num_value_for_tables({num = email_info.fixed_cves }, "num"),"0"),
					    hosts_discrepancy_details = email_info.discrepancies_details
      })
      add_new_configured_hosts_br = false
   end

   local no_longer_down_now = ""
   local no_longer_down_list = ""
   if (email_info.no_longer_down_now and email_info.no_longer_down_now > 0) then
      
      -- hosts no longer down 
      add_new_configured_hosts_br = false
      if (email_info.hosts_was_down_list ~= nil) then
         local ret = ""
   
         for k, v in pairsByKeys(email_info.hosts_was_down_list, asc) do
       ret = ret .. k .. "\n"
         end
         
         no_longer_down_list = i18n("hosts_stats.page_scan_hosts.email.host_down_list", { host_down_items = ret })
      end

      local add_br = ternary(email_info.has_discrepancy, "", "<br>")
      no_longer_down_now = i18n("hosts_stats.page_scan_hosts.email.hosts_no_longer_down", {
         no_longer_down_now_num = format_num_for_email(email_info.no_longer_down_now or 0, 5),
         no_longer_down_list = no_longer_down_list,
         add_br = add_br
      })  
   end

   local net_scan_without_hosts_discovered = "" 
   if (email_info.scanned_networks_no_hosts_found ~= nil and next(email_info.scanned_networks_no_hosts_found)) then
      if (add_new_configured_hosts_br) then
         net_scan_without_hosts_discovered = net_scan_without_hosts_discovered .. "<br>"
      end
      add_new_configured_hosts_br = false
      local net_scan_list = ""

      for net, v in pairsByKeys(email_info.scanned_networks_no_hosts_found, asc) do
      
         net_scan_list = net_scan_list .. i18n("hosts_stats.page_scan_hosts.email.host_down_item", {host_id = net.."/24"}) 
      end

      local no_discovered_hosts_list = i18n("hosts_stats.page_scan_hosts.email.host_down_list", {host_down_items = net_scan_list})
      net_scan_without_hosts_discovered = i18n("hosts_stats.page_scan_hosts.email.no_new_hosts_detected", {subnets = no_discovered_hosts_list})
   end

   local discovered_hosts_list = ""
   local discovered_hosts = false
   if email_info.scanned_networks ~= nil and next(email_info.scanned_networks) then

      -- hosts not configured but discovered by the netscan
      discovered_hosts = true
      if (add_new_configured_hosts_br) then
         discovered_hosts_list = discovered_hosts_list .. "<br>"
      end

      for net, v in pairsByKeys(email_info.scanned_networks, asc) do
         local hosts_string = email_info.scanned_networks[net]
         
         local net_scan = net.."/24"

         local new_hosts_discovered = {}
         if (not isEmptyString(hosts_string)) then
            if (hosts_string:find(",")) then
               new_hosts_discovered = string.split(hosts_string, ",")
            else
               new_hosts_discovered[#new_hosts_discovered+1] = hosts_string
            end
         end
         local num_hosts = table.len(new_hosts_discovered) or 0

         local hosts_list_formatted_string = ""
         for _,host in ipairs(new_hosts_discovered) do 
            hosts_list_formatted_string = hosts_list_formatted_string .. i18n("hosts_stats.page_scan_hosts.email.host_down_item", {host_id = host})
         end

         local discovered_formatted_hosts_list = i18n("hosts_stats.page_scan_hosts.email.host_down_list", {host_down_items = hosts_list_formatted_string})

         discovered_hosts_list = discovered_hosts_list .. i18n("hosts_stats.page_scan_hosts.email.netscan_new_hosts", {
            net_scan = net_scan,
            num_hosts = num_hosts,
            host_list = discovered_formatted_hosts_list,
            add_br = ""
         })
      end
   end

   local no_discrepancy = false
   if ((not email_info.has_discrepancy) and email_info.no_longer_down_now == 0 and not discovered_hosts) then
      add_br = "<br>"
      no_discrepancy = true
      possible_discrepancies_info = i18n("hosts_stats.page_scan_hosts.email.no_discrepancy")
   end

   if (not no_discrepancy) then
      notification_message = notification_message .. possible_discrepancies_info .. no_longer_down_now .. net_scan_without_hosts_discovered .. discovered_hosts_list
   else
      notification_message = notification_message .. no_longer_down_now .. net_scan_without_hosts_discovered .. discovered_hosts_list .. possible_discrepancies_info
   end
   local report_link_line = i18n("hosts_stats.page_scan_hosts.email.report_link_line",
				 {url = string.format(getHttpHost() .. ntop.getHttpPrefix() .. "/lua/enterprise/vulnerability_scan_report.lua?epoch_end=%u&epoch_begin=%u",
						      report_date,report_date), add_br = add_br})

   notification_message = notification_message .. report_link_line

   vs_utils.generate_report(email_info.end_epoch_t)

   if verbose then
      traceError(TRACE_NORMAL,TRACE_CONSOLE, "Vulnerability Scan completed. Sending " .. title .."\n")
   end

   recipients.sendMessageByNotificationType({periodicity = periodicity, success = true, message = notification_message, title = title}, "vulnerability_scans")
end

-- **********************************************************
-- Function to retrieve dates of scan all or periodic scan
function vs_utils.get_scan_all_dates()
   return ntop.getCache(hosts_scan_last_report_dates)
end

-- **********************************************************
-- Function to verify if periodic scan is ended
function vs_utils.is_periodic_scan_completed()
   local periodicity_scan_in_progress = ntop.getCache(periodic_scan_key) == "1"

   if (periodicity_scan_in_progress) then
      local hosts_details = vs_utils.retrieve_hosts_to_scan()
      for _,item in ipairs(hosts_details) do

         -- verify status of in progress periodic scanning
         if(item.is_periodicity and (item.is_ok_last_scan == vs_utils.scan_status.scheduled or item.is_ok_last_scan == vs_utils.scan_status.scanning)) then
            return false
         end
      end

      ntop.setCache(periodic_scan_key, "0")

      local periodicity = ntop.getCache(periodic_scan_type_key)

      for _,item in ipairs(hosts_details) do
         local host_hash_key = vs_utils.get_host_hash_key(item.host, item.scan_type)
         local host_hash_value_string = ntop.getHashCache(host_to_scan_key, host_hash_key)
         if(not isEmptyString(host_hash_value_string)) then

            local host_hash_value = json.decode(host_hash_value_string)

            host_hash_value.is_periodicity = false

            ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(host_hash_value))
         end
      end

      return true, periodicity
   end

   return false
end

-- **********************************************************
-- Function to verify if scan all is ended
function vs_utils.is_ondemand_scan_completed()
   local scan_all_in_progress = ntop.getCache(ondemand_scan_key) == "1"

   if (scan_all_in_progress) then
      local hosts_details = vs_utils.retrieve_hosts_to_scan()
      for _,item in ipairs(hosts_details) do

         -- verify status of in progress periodic scanning
         if(item.is_all and (item.is_ok_last_scan == vs_utils.scan_status.scheduled or item.is_ok_last_scan == vs_utils.scan_status.scanning)) then
            return false
         end
      end

      ntop.setCache(ondemand_scan_key, "0")

      for _,item in ipairs(hosts_details) do
         local host_hash_key = vs_utils.get_host_hash_key(item.host, item.scan_type)
         local host_hash_value_string = ntop.getHashCache(host_to_scan_key, host_hash_key)
         if(not isEmptyString(host_hash_value_string)) then

            local host_hash_value = json.decode(host_hash_value_string)

            host_hash_value.is_all = false

            ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(host_hash_value))
         end
      end

      return true
   end

   return false
end

-- **********************************************************
-- Function to verify if single scan is ended
function vs_utils.is_single_scan_completed()
   local single_scan_in_progress = ntop.getCache(single_scan_key) == "1"

   if (single_scan_in_progress) then
      local hosts_details = vs_utils.retrieve_hosts_to_scan()
      for _,item in ipairs(hosts_details) do

         -- verify status of in progress periodic scanning
         if(item.is_single_scan and (item.is_ok_last_scan == vs_utils.scan_status.scheduled or item.is_ok_last_scan == vs_utils.scan_status.scanning)) then
            return false
         end
      end

      ntop.setCache(single_scan_key, "0")

      for _,item in ipairs(hosts_details) do
         local host_hash_key = vs_utils.get_host_hash_key(item.host, item.scan_type)
         local host_hash_value_string = ntop.getHashCache(host_to_scan_key, host_hash_key)
         if(not isEmptyString(host_hash_value_string)) then

            local host_hash_value = json.decode(host_hash_value_string)

            host_hash_value.is_single_scan = false

            ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(host_hash_value))
         end
      end

      return true
   end

   return false

end

-- **********************************************************
-- Function to enable periodic scan end check on callbacks
function vs_utils.is_periodic_scan_running()
   return ntop.getCache(periodic_scan_key) == "1"
end

-- **********************************************************
-- Function to retrieve single host scan info from
-- host hash key
local function retrieve_host_by_hash_key(host_hash_key)
   local hash_value_string = ntop.getHashCache(host_to_scan_key, host_hash_key)
   local hash_prefs_string = ntop.getHashCache(prefs_host_values_key,host_hash_key)
   local hash_value = json.decode(hash_prefs_string)
   if (not isEmptyString(hash_value_string)) then
      -- hash value found
      hash_value = json.decode(hash_value_string)
      local hash_pref_value = json.decode(hash_prefs_string) or {}
      for k,value in pairs(hash_pref_value) do
         if (k ~= 'is_ok_last_scan') then
            hash_value[k] = value
         end
      end
   end
   return hash_value
end

-- **********************************************************
-- Function to retrieve a specific host scan info with scan type CVE
-- (for host_details page )
function vs_utils.retrieve_host(host)
   local hosts_scanned = ntop.getHashKeysCache(host_to_scan_key) or {}
   for key, _ in pairs(hosts_scanned) do
      if key:find(host) and key:find("cve") then
         return retrieve_host_by_hash_key(key)
      end
   end
   return nil
end

-- **********************************************************
-- Function to retrieve hosts list to scan
function vs_utils.retrieve_hosts_to_scan(epoch)
   if (isEmptyString(epoch) or (not ntop.isClickHouseEnabled())) then
      -- not a request for historical data (only for report)
      local hash_keys = ntop.getHashKeysCache(prefs_host_values_key)
      local rsp = {}
      if hash_keys then
         for k in pairs(hash_keys) do
            local hash_value_string = ntop.getHashCache(host_to_scan_key, k)
            local hash_prefs_string = ntop.getHashCache(prefs_host_values_key,k)
            local hash_value = json.decode(hash_prefs_string)

            if (not isEmptyString(hash_value_string)) then
               -- hash value found

               hash_value = json.decode(hash_value_string)
               local hash_pref_value = json.decode(hash_prefs_string) or {}

	       for key,value in pairs(hash_pref_value) do
		  if (key ~= 'is_ok_last_scan') then
		     hash_value[key] = value
		  end
	       end
            else
               -- hash value not found
               ntop.setHashCache(host_to_scan_key, k, hash_prefs_string)

            end

            rsp[#rsp+1] = hash_value
         end
      end
      return rsp
   else
      -- request for report
      local all_details, data = vs_db_utils.retrieve_report(epoch)
      return(data)
   end
end

-- **********************************************************
-- Function to retrieve hosts list to scan just for status_info
function vs_utils.check_in_progress_status()
   local hash_keys = ntop.getHashKeysCache(host_to_scan_key)
   local total_in_progress = 0
   local total = 0

   if hash_keys then
      for k in pairs(hash_keys) do
         local hash_value_string = ntop.getHashCache(host_to_scan_key, k)

         if (not isEmptyString(hash_value_string)) then
            local hash_value = json.decode(hash_value_string)
            -- Check IN PROGRESS --> FIX ME with enums
            if hash_value and (hash_value.is_ok_last_scan == vs_utils.scan_status.scheduled or hash_value.is_ok_last_scan == vs_utils.scan_status.scanning) then
               total_in_progress = total_in_progress + 1
            end
            total = total + 1
         end
      end
   end

   return total, total_in_progress
end

-- **********************************************************
-- Function restrieve scan result from FS
local function retrieve_scan_result_from_file(scan_type, host)
   -- for retrocompatibility
   local path = get_report_path(scan_type, host)

   if(ntop.exists(path)) then
      local handle = io.open(path, "r")
      local result = handle:read("*a")
      handle:close()

      return result
   else
      return("Unable to read file "..path)
   end

end

-- **********************************************************
-- Function to retrieve last host scan result
function vs_utils.retrieve_hosts_scan_result(scan_type, host, epoch)

   -- retrieve from DB
   local db_result
   local fs_result

   local is_clickhouse_enabled = ntop.isClickHouseEnabled()

   -- retrieve data here
   if (is_clickhouse_enabled) then
      -- retrieve from DB
      db_result = vs_db_utils.retrieve_scan_result(scan_type, host, tonumber(epoch))
   else
      -- retrieve from FS
      fs_result = retrieve_scan_result_from_file(scan_type, host)

      return fs_result
   end

   -- return data
   if (is_clickhouse_enabled and db_result == "") then

      -- ****************************************
      -- retrocompatibility case:

      -- when there were results saved on FS,
      -- but the user updates ntopng to use clickhouse,
      -- and there are currently no results in clickhouse.
      -- ****************************************

      -- clickhouse is enabled but maybe the user had old data on FS and no data on DB
      fs_result = retrieve_scan_result_from_file(scan_type, host)
      return fs_result
   else
      -- the result from DB is ok.
      return db_result
   end

end

-- **********************************************************
-- Function to delete host to scan
function vs_utils.delete_host_to_scan(host, scan_type, all)
   if all then
      ntop.delCache(prefs_host_values_key)
      ntop.delCache(host_to_scan_key)
      ntop.delCache(host_scan_queue_key)
      ntop.delCache(host_in_scanning_hash_key)
      ntop.delCache(periodic_scan_key)
      ntop.delCache(periodic_scan_type_key)

      local path_to_s_result = get_report_path(scan_type, host, true)
      os.execute("rm -f "..path_to_s_result)
   else
      local host_hash_key = vs_utils.get_host_hash_key(host, scan_type)
      local path_to_s_result = get_report_path(scan_type, host, false)
      os.remove(path_to_s_result)
      ntop.delHashCache(host_to_scan_key, host_hash_key)
      ntop.delHashCache(prefs_host_values_key, host_hash_key)
      ntop.delHashCache(host_in_scanning_hash_key, host_hash_key)

      -- Remove this host from active schedules
      local elems = {}
      while(true) do
         local e = ntop.lpopCache(host_scan_queue_key)

         if(e == nil) then
            break
         else
            local r = json.decode(e)
            if(r and not((r.scan_type == scan_type) and (r.host == host))) then
               table.insert(elems, e)
            end
         end
      end

      for _,i in pairs(elems) do
         ntop.lpushCache(host_scan_queue_key, i)
      end
   end

   return true
end

-- *********************************************************
-- Function to delete host to scan by id
function vs_utils.delete_host_to_scan_by_id(id)
   local hosts_details = vs_utils.retrieve_hosts_to_scan()
   local host_to_delete = {}
   local id_number = tonumber(id)

   for _,value in ipairs(hosts_details) do
      if(tonumber(value.id) == id_number ) then
         host_to_delete.host = value.host
         host_to_delete.scan_type = value.scan_type
         break
      end
   end

   local host_hash_key = vs_utils.get_host_hash_key(host_to_delete.host, host_to_delete.scan_type)
   local path_to_s_result = get_report_path(host_to_delete.scan_type, host_to_delete.host, false)
   os.remove(path_to_s_result)
   ntop.delHashCache(host_to_scan_key, host_hash_key)
   ntop.delHashCache(prefs_host_values_key, host_hash_key)


   return true
end

-- **********************************************************
-- Function to retrieve scan types list
function vs_utils.retrieve_scan_types()
   local scan_types = vs_utils.list_scan_modules()
   local ret = {}

   for _,scan_type in ipairs(scan_types) do
      table.insert(ret, { id = scan_type, label = i18n("hosts_stats.page_scan_hosts.scan_type_list."..scan_type) })
   end

   return ret
end

-- **********************************************************
-- Function to list scan modules
function vs_utils.list_scan_modules()
   local dirs = ntop.getDirs()
   local basedir = dirs.scriptdir .. "/lua/modules/vulnerability_scan/modules"
   local modules = {}

   for name in pairs(ntop.readdir(basedir)) do
      if(ends(name, ".lua")) then
         name = string.sub(name, 1, string.len(name)-4) -- remove .lua trailer
         local m = vs_utils.load_module(name)

         if(m:is_enabled()) then
            table.insert(modules, name)
         end
      end
   end

   return(modules)
end

-- **********************************************************
-- Function to load VS module by name
function vs_utils.load_module(name)
   package.path = dirs.installdir .. "/scripts/lua/modules/vulnerability_scan/modules/?.lua;".. package.path

   return(require(name):new())
end

-- **********************************************************
-- Function to discover open ports
function vs_utils.discover_open_ports(host)
   local result,duration,scan_result,num_open_ports,num_vulnerabilities_found, cve, udp_ports, tcp_ports, scan_ports, network_alert_store,now

   local scan_module = vs_utils.load_module("tcp_portscan")
   now,result,duration,scan_result,num_open_ports,num_vulnerabilities_found, cve, udp_ports, tcp_ports = scan_module:scan_host(host, ports)

   return format_port_list_to_string(tcp_ports)
end

-- **********************************************************
-- Function to exec single host scan
function vs_utils.scan_host(scan_type, host, ports, scan_id, use_coroutines, cidr)
   if(ntop.isShuttingDown()) then return(false) end

   if(use_coroutines == nil) then use_coroutines = false end

   if debug_me then
      if (ports ~= nil) then
         traceError(TRACE_NORMAL,TRACE_CONSOLE, "Scanning Host ".. host .. " on Ports: " .. ports .. "\n")
      else
         traceError(TRACE_NORMAL,TRACE_CONSOLE, "Scanning Host ".. host.."\n")
      end
   end

   -- to save on redis the user input
   local ports_scan_param = ports

   if(string.contains(scan_type, '_portscan')) then
      -- Nothing to do
   else
      if (isEmptyString(ports)) then
         ports = vs_utils.discover_open_ports(host)
      end
   end

   if(ntop.isShuttingDown()) then return(false) end

   if (isAlreadyPresent({host= host, scan_type= scan_type})) then
      -- It is possible that the scan entry could be removed during the vs_utils.discover_open_ports phase.
      vs_utils.set_status_scan(scan_type, host, ports_scan_param, id, nil,nil,nil, vs_utils.scan_status.scanning)
      -- Save on redis the scanning host to avoid inconsistent state on ntopng restarts
      local scanning_host = {scan_type = scan_type, host = host, ports = ports_scan_param, id = scan_id}
      save_scanning_host(scanning_host)
   else
      return false
   end
   
   -- Scan host
   local scan_module = vs_utils.load_module(scan_type)
   local now,result,duration,scan_result,num_open_ports,num_vulnerabilities_found, cve, udp_ports, tcp_ports, discovered_hosts = scan_module:scan_host(host, ports, use_coroutines, cidr)

   if(ntop.isShuttingDown()) then
      return false
   end

   if (udp_ports ~= nil) then
      udp_ports = {ports = format_port_list_to_string(udp_ports), num_ports = #udp_ports}
   end

   if(tcp_ports ~= nil) then
      tcp_ports = {ports = format_port_list_to_string(tcp_ports), num_ports = #tcp_ports}
   end

   if scan_result and scan_result ~= vs_utils.scan_status.failed then
      scan_result = vs_utils.scan_status.ok

      if (scan_type ~= 'ipv4_netscan') then
         -- excluding the netscan 
         ntop.incrCache(scanned_hosts_count_key)
      end
   elseif(scan_result and scan_result == vs_utils.scan_status.failed) then
      scan_result = vs_utils.scan_status.failed
   end

      --traceError(TRACE_NORMAL, TRACE_CONSOLE, "End scan Host ".. host .. ", result: " .. result .. "\n")

   if (isAlreadyPresent({host= host, scan_type= scan_type})) then
      vs_utils.save_host_to_scan(scan_type, host, result, now, duration, scan_result,
                                 ports_scan_param, nil, num_open_ports, num_vulnerabilities_found, cve, scan_id, false, udp_ports, tcp_ports, discovered_hosts)
   end

   return true
end

-- **********************************************************
-- Function to set singles can in progress redis key
local function set_single_scan_in_progress()

   if (ntop.getCache(single_scan_key) ~= '1') then
      ntop.setCache(single_scan_key, "1")
      ntop.setCache(single_scan_info_key, json.encode({
			  cves = 0,
			  udp_ports = 0,
			  tcp_ports = 0,
			  begin_epoch = os.time(),
			  scanned_host = 1
      }))
   end

end

-- **********************************************************
-- Function to update single host status
function vs_utils.set_status_scan(scan_type, host, ports, id, is_periodicity, is_all,is_single_scan, status)
   local host_hash_key = vs_utils.get_host_hash_key(host, scan_type)
   local host_hash_value_string = ntop.getHashCache(host_to_scan_key, host_hash_key)
   local host_hash_value
   if(not isEmptyString(host_hash_value_string)) then

      host_hash_value = json.decode(host_hash_value_string) or {}
   else
      host_hash_value = {}
   end

   host_hash_value.is_ok_last_scan = status
   if (is_periodicity ~= nil and is_periodicity) then
      host_hash_value.is_periodicity = is_periodicity
   end

   if (is_all ~= nil and is_all) then
      host_hash_value.is_all = is_all
   end

   if (is_single_scan ~= nil and is_single_scan) then
      host_hash_value.is_single_scan = is_single_scan
      set_single_scan_in_progress()
   end

   ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(host_hash_value))

   return true
end

-- **********************************************************
-- Function to schedule ondemand single host scan
function vs_utils.schedule_ondemand_single_host_scan(scan_type, host, ports, scan_id, is_periodicity, is_all, is_single_scan, cidr)
   local scan = { scan_type = scan_type, host = host, ports = ports, id= scan_id, cidr = cidr}
   vs_utils.set_status_scan(scan_type, host, ports, scan_id, is_periodicity, is_all, is_single_scan, vs_utils.scan_status.scheduled)

   ntop.rpushCache(host_scan_queue_key, json.encode(scan))

   return true
end

-- **********************************************************
-- Function to schedule ondemand scan all hosts
function vs_utils.schedule_ondemand_all_hosts_scan()
   local host_to_scan_list = vs_utils.retrieve_hosts_to_scan()

   local is_scanning_almost_one = false
   if #host_to_scan_list > 0 then
      for _,scan_info in ipairs(host_to_scan_list) do
         vs_utils.schedule_ondemand_single_host_scan(scan_info.scan_type, scan_info.host, scan_info.ports, scan_info.id, false, true, false, scan_info.cidr)
         is_scanning_almost_one = true
      end
   end

   if (is_scanning_almost_one) then
      ntop.setCache(ondemand_scan_key , "1")
   end

   ntop.setCache(ondemand_scan_host_info_key, json.encode({
                       cves = 0,
                       udp_ports = 0,
                       tcp_ports = 0,
                       begin_epoch = os.time(),
                       scanned_host = 0
   }))

   return true
end

-- **********************************************************
-- periodicity can be set to "1day" "1week" "disabled"
function vs_utils.schedule_periodic_scan(periodicity)
   local host_to_scan_list = vs_utils.retrieve_hosts_to_scan()

   if (#host_to_scan_list > 0 ) then

      local is_already_running = ntop.getCache(periodic_scan_key) == "1"
      if not is_already_running then

         local is_scanning_almost_one = false

         for _,scan_info in ipairs(host_to_scan_list) do
            local frequency = scan_info.scan_frequency
            if(frequency == periodicity) then
               vs_utils.schedule_ondemand_single_host_scan(scan_info.scan_type, scan_info.host, scan_info.ports, scan_info.id, true, false, false, scan_info.cidr)
               is_scanning_almost_one = true
            end
         end


         if is_scanning_almost_one then
            ntop.setCache(periodic_scan_key , "1")
            ntop.setCache(periodic_scan_type_key, periodicity)


            ntop.setCache(periodic_scan_host_info_key , json.encode({
                                cves = 0,
                                udp_ports = 0,
                                tcp_ports = 0,
                                begin_epoch = os.time(),
                                scanned_hosts = 0,
                                not_scanned_hosts = 0
            }))

         end
      end
   end
   return true
end

-- **********************************************************
-- Process a single host scan request that has been queued
function vs_utils.process_oldest_scheduled_scan(use_coroutines)
   if(ntop.isShuttingDown()) then return(false) end

   local elem = ntop.lpopCache(host_scan_queue_key)

   if((elem ~= nil) and (elem ~= "")) then
      if debug_me then
         traceError(TRACE_NORMAL,TRACE_CONSOLE, "Found vulnerability scan: ".. elem .. "\n")
      end

      local elem = json.decode(elem)

      if(use_coroutines) then
         if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "Starting scan on host "..elem.host.."["..elem.scan_type .."]") end
         return(coroutine.create(function () vs_utils.scan_host(elem.scan_type, elem.host, elem.ports, elem.id, use_coroutines, elem.cidr) end))
      else
         vs_utils.scan_host(elem.scan_type, elem.host, elem.ports, elem.id, use_coroutines, elem.cidr)

         return true
      end
   else
      if(use_coroutines) then
         if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "No host to scan") end
         return nil
      else
         return false
      end
   end
end

-- **********************************************************
-- Process all host scans request that has been queued
function vs_utils.process_all_scheduled_scans(max_num_scans, use_coroutines)
   local num = 0
   local co = {}

   if(max_num_scans == nil) then max_num_scans = 9999 end

   if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "Starting up to "..max_num_scans.." scans...") end

   while((max_num_scans > 0) and not(ntop.isShuttingDown())) do
      local res = vs_utils.process_oldest_scheduled_scan(use_coroutines)
      local do_inc = true

      if(use_coroutines) then
         if(res == nil) then
            break -- nothing to do
            do_inc = false
         else
            co[#co + 1] = res
         end
      else
         if(res == false) then
            break
         end
      end

      if(do_inc) then
         max_num_scans = max_num_scans - 1
         num = num + 1
      end
   end

   if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "Started "..num.." scans") end

   if(use_coroutines and (num > 0)) then
      -- See snmp_poll.lua

      while(not(ntop.isShuttingDown())) do
         local tot = #co
         local keep_on = false

         for i = 1, tot do
            if coroutine.status(co[i]) ~= "dead" then
               local rc, msg = coroutine.resume(co[i])

               -- Note that resume runs in protected mode.
               -- Therefore, if there is any error inside a coroutine, Lua will not show the error message,
               -- but instead will return it to the resume call.
               if not rc then
                  traceError(TRACE_NORMAL, TRACE_CONSOLE, msg or "Unknown error occurred")
               end

               keep_on = rc or keep_on
            end
         end -- for

         if(keep_on == false) then
            break
         end
      end -- while
   end

   if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "All "..num.." scans are completed") end

   return num
end

-- **********************************************************
-- Example vs_utils.get_active_hosts("192.168.2.0", "24")
function vs_utils.get_active_hosts(host, cidr)
   local result = vs_utils.exec_netscan(host, cidr)
   return result
end

-- **********************************************************
-- Function to cleanup netscan result
function vs_utils.netscan_cleanup(scan_result)

   scan_result = lines(scan_result)

   local scan_out = {}
   local discovered_hosts = {}
   for i=1,4 do
      table.remove(scan_result, 1)
   end

   table.remove(scan_result, #scan_result)

   for _,l in pairs(scan_result) do 
      if (string.find(l, "Nmap scan report for") ~= nil) then
         discovered_hosts[#discovered_hosts + 1] =  string.split(l," ")[5]
         
      end
      table.insert(scan_out, l)
   end

   local scan_result_out = table.concat(scan_out, "\n")
   return scan_result_out, discovered_hosts
end

-- **********************************************************
-- Example vs_utils.exec_netscan("192.168.2.0", "24")
function vs_utils.exec_netscan(host, cidr)
   local result = {}
   local out
   local begin_epoch
   local duration
   local scan_ok

   cidr = tonumber(cidr)

   if((cidr == 32) or (cidr == 128)
      or (host:find('.') == nil) -- not dots in IP, it looks symbolic
      or (string.sub(host, -1) ~= "0") -- last digit is not 0, so let's assume /32
   ) then
      result[#result+1] = host -- return it as is
   else
      local net_range = '1-254'
      if (cidr == 25) then
         net_range = '1-127'
      elseif (cidr == 26) then
         net_range = '1-63'
      elseif (cidr == 27) then
         net_range = '1-31'
      elseif (cidr == 28) then
         net_range = '1-15'
      elseif (cidr == 29) then
         net_range = '1-7'
      elseif (cidr == 30) then
         net_range = '1-3'
      end
      
      local s = string.split(host, '%.')
      local net = s[1].."."..s[2].."."..s[3].."."
      local nmap = vs_utils.get_nmap_path()
      local command = nmap..' -sP -n ' .. net .. net_range
      
      begin_epoch = os.time()
      out = ntop.execCmd(command)
      duration = os.time() - begin_epoch

      out, result = vs_utils.netscan_cleanup(out)
      scan_ok = true
      
   end

   return result,out,begin_epoch,duration,scan_ok
end

-- **********************************************************
-- Update all scan frequencies
function vs_utils.update_all_periodicity(scan_frequency)
   local host_to_scan_list = vs_utils.retrieve_hosts_to_scan()

   for _,value in ipairs(host_to_scan_list) do
      local host_hash_key = vs_utils.get_host_hash_key(value.host, value.scan_type)
      local host_hash_value_string = ntop.getHashCache(host_to_scan_key, host_hash_key)
      if(not isEmptyString(host_hash_value_string)) then

         local host_hash_value = json.decode(host_hash_value_string)

         host_hash_value.scan_frequency = scan_frequency

         ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(host_hash_value))
      end

      local prefs_host_value_string = ntop.getHashCache(prefs_host_values_key, host_hash_key)

      if (not isEmptyString(prefs_host_value_string)) then
         local host_hash_value = json.decode(prefs_host_value_string)

         host_hash_value.scan_frequency = scan_frequency

         ntop.setHashCache(prefs_host_values_key, host_hash_key, json.encode(host_hash_value))
      end
   end

   return true
end

-- **********************************************************
-- Function to verify if VS is enabled
function vs_utils.is_available()
   if (ntop.isnEdge()) then
      return false
   end
   local scan_modules = vs_utils.list_scan_modules()
   return (#scan_modules > 0)
end

-- **********************************************************
-- Function to run command
function vs_utils.runCommand(scan_command, use_coroutines)
   local result
   local debug_me = false

   if ntop.isWindows() then
      local handle = io.popen(scan_command)
      result = handle:read("*a")
      handle:close()
   else
      if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "Started " .. scan_command) end

      if(use_coroutines) then
         if(true) then
            local job_id = ntop.execCmdAsync(scan_command)

            result = nil

            while((result == nil) and not(ntop.isShuttingDown())) do
               coroutine.yield()
               result = ntop.readResultCmdAsync(job_id)
               ntop.msleep(100)
            end

            if(debug_me) then tprint(result) end
         else
            coroutine.yield()
         end
      else
         result = ntop.execCmd(scan_command)
      end
   end

   return result
end

-- **********************************************************
-- Function to scan host
function vs_utils.nmap_scan_host(command, host_ip, ports, use_coroutines, module_name)
   local scan_command

   if(ntop.isShuttingDown()) then
      return nil
   end

   -- IPv6 check
   if(string.contains(host_ip, ':')) then command = command .. " -6 " end

   if(not(isEmptyString(ports))) then command = command .. " -p " .. ports end

   scan_command = string.format("%s %s", command, host_ip)

   if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "Executing: "..scan_command.."\n") end

   local begin_epoch = os.time()
   local result = vs_utils.runCommand(scan_command, use_coroutines)
   local duration = os.time() - begin_epoch
   local scan_ok = true
   local num_open_ports
   local num_vulnerabilities_found
   local cve
   local tcp_ports
   local udp_ports

   result, num_open_ports, num_vulnerabilities_found, cve, udp_ports, tcp_ports = vs_utils.cleanup_nmap_result(result, module_name)
   return begin_epoch, result, duration, scan_ok, num_open_ports, num_vulnerabilities_found, cve, udp_ports, tcp_ports
end

-- **********************************************************
-- Function to check if a host is available
function vs_utils.nmap_check_host(host_ip, use_coroutines)
   local nmap =  vs_utils.get_nmap_path()
   local scan_command = nmap.." -sn"

   -- IPv6 check (need to use --privileged otherwise nmap will report hosts are down even if up)
   if(string.contains(host_ip, ':')) then scan_command = scan_command .. " --privileged -6" end
   scan_command = string.format("%s %s",scan_command,host_ip)

   if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "Executing: "..scan_command.."\n") end

   local start_scan = os.time()
   local result = vs_utils.runCommand(scan_command, use_coroutines)
   local end_scan = os.time()
   local scan_duration = end_scan - start_scan

   local is_up = vs_utils.cleanup_nmap_check_host_result(result)
   if(debug_me) then traceError(TRACE_NORMAL, TRACE_CONSOLE, "Host is up: "..tostring(is_up).."\n") end

   return is_up, scan_duration, start_scan, end_scan

end

-- **********************************************************
-- Migrate old configurations
function vs_utils.migrate_keys()
   local old_hash_key = "ntopng.prefs.host_to_scan"
   local old_hosts = ntop.getHashKeysCache(old_hash_key) or {}

   for key,_ in pairs(old_hosts) do
      local hash_value_string = ntop.getHashCache(old_hash_key, key)
      local old_hash_value = json.decode(hash_value_string)
      if (old_hash_value) then
         local new_hash_value = {
            host = old_hash_value.host,
            host_name = old_hash_value.host_name,
            scan_type = old_hash_value.scan_type,
            scan_frequency = old_hash_value.scan_frequency,
            ports = old_hash_value.ports,
         }

         if (not isEmptyString(old_hash_value.discovered_host_scan_type)) then
            new_hash_value = old_hash_value.discovered_host_scan_type
         end

         ntop.setHashCache(prefs_host_values_key, key,json.encode(new_hash_value))
      end
   end

   ntop.delCache(old_hash_key)

   local hosts = ntop.getHashKeysCache(host_to_scan_key) or {}
   local from_key = "tcp_openports"
   local to_key   = "tcp_portscan"

   for key, _ in pairs(hosts) do
      if(string.contains(key, from_key)) then
         value = ntop.getHashCache(host_to_scan_key, key)

         new_key   = key:gsub(from_key, to_key)
         new_value = value:gsub(from_key, to_key)

         ntop.setHashCache(host_to_scan_key, new_key, new_value)
         ntop.delHashCache(host_to_scan_key, key)
      end
   end
end

-- **********************************************************
-- init once
if(ntop.getCache("ntopng.prefs.vs.vs_slow_scan") == "1") then
   use_slow_scan = " -T polite --max-parallelism 1"
else
   use_slow_scan = ""
end

-- ##########################################################
-- ipv4_netscan functions 

-- **********************************************************
-- Function to retrieve discovered_host_scan_type 
-- and scan_frequency

-- discovered_host_scan_type: 
   -- the scan type for the hosts discovered by the netscan 

-- scan_frequency:
   -- the scan_frequency for the hosts discovered by the netscan
function vs_utils.get_network_pref_value(network_ip, scan_type)

   local hash_key = vs_utils.get_host_hash_key(network_ip, scan_type)
   local network_pref_value = json.decode(ntop.getHashCache(prefs_host_values_key,hash_key) or {})

   --[[ Retrieving values to includes net sub scans in reports and email data ]]
   local network_others_value = json.decode(ntop.getHashCache(host_to_scan_key, hash_key) or {})
   local net_scan_all, net_periodic_scan, net_single_scan
   if (network_others_value) then
      net_scan_all, net_periodic_scan, net_single_scan = network_others_value.is_all, network_others_value.is_periodicity, network_others_value.is_single_scan
      net_scan_all = toboolean(net_scan_all)
      net_periodic_scan = toboolean(net_periodic_scan)
      net_single_scan = toboolean(net_single_scan)
   end
   if(network_pref_value and not isEmptyString(network_pref_value)) then
      return network_pref_value.discovered_host_scan_type, network_pref_value.scan_frequency, net_scan_all, net_periodic_scan, net_single_scan
   end
   return nil
end

-- **********************************************************
-- Function to find if an ip is configured with a specific scan_type
function vs_utils.isVSConfiguredHostScanType(ip,scan_type)
   local host_configured_key = vs_utils.get_host_hash_key(ip, scan_type)
   local pref_value = ntop.getHashCache(prefs_host_values_key,host_configured_key)
   return not isEmptyString(pref_value)
end


-- **********************************************************
-- Function to find if an ip is configured
function vs_utils.isVSConfiguredHost(ip)
   local hosts_scanned = ntop.getHashKeysCache(prefs_host_values_key) or {}
   for key, _ in pairs(hosts_scanned) do
      if key:find(ip) then
         return true
      end
   end
   return false
end

-- **********************************************************
-- Function to trigger an alert if the host
-- is not configured
function vs_utils.triggerHostNotConfiguredAlert(host, scan_type)
   trigger_alert_host_not_configured(host, scan_type)
end

-- **********************************************************

-- ##########################################################
-- VS DB Functions
-- **********************************************************

-- Function to retrieve reports list from the DB
function vs_utils.retrieve_report_list(epoch)
   local sort_on = "DATE"
   return (vs_db_utils.retrieve_reports(sort_on,epoch))
end

-- **********************************************************
-- Function to retrieve single report from the DB
function vs_utils.retrieve_report(report_name)
   return (vs_db_utils.retrieve_report(report_name))
end

-- **********************************************************
-- Function to delete single report on the DB
function vs_utils.delete_report(epoch)
   return(vs_db_utils.delete_report(epoch))
end

-- **********************************************************
-- Function to edit single report on the DB
function vs_utils.edit_report(epoch, report_name)
   return(vs_db_utils.edit_report(epoch,report_name))
end

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

return vs_utils
