-- -- (C) 2013-23 - 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 -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) local host_to_scan_key = "ntopng.prefs.host_to_scan" local host_to_scan_periodicity_key = "ntopng.prefs.host_to_scan.periodicity_scan" local host_scannned_count_key = "ntopng.prefs.host_to_scan.count_scanned" local host_scan_queue_key = "ntopng.vs_scan_queue" local scanned_hosts_changes_key = "ntopng.alerts.scanned_hosts_changes" local json = require("dkjson") local format_utils = require("format_utils") local recipients = require("recipients") local debug_print = false local vs_utils = {} -- ********************************************************** function vs_utils.get_host_hash_key(host, scan_type) return string.format("%s-%s",host,scan_type) end -- ********************************************************** vs_utils.scan_status = { error = 0, ok = 1, scheduled = 2, not_scanned = 3, scanning = 4 } -- ********************************************************** function vs_utils.is_nmap_installed() local path = { "/usr/bin/nmap", "/usr/local/bin/nmap", "/opt/homebrew/bin/nmap" } local module_path = { "/usr/share/nmap/scripts/", "opt/homebrew/share/nmap/scripts/vulscan/", "/usr/local/share/nmap/scripts/vulscan", } for _,p in pairs(path) do if(ntop.exists(p)) then -- nmap is present. Now check if vulscan is present for _,m in pairs(module_path) do if(ntop.exists(m)) then return true end end end end return false end -- ********************************************************** local function get_report_path(scan_type, ip, all) local base_dir = dirs.workingdir .. "/-1/vulnerability_scan" ntop.mkdir(base_dir) local ret = "" if (not all or all == nil) then ret = base_dir .. "/"..ip.."_"..scan_type..".txt" else ret = base_dir .. "/*.txt" end return(ret) end -- ############################################## local function lines(str) local result = {} for line in str:gmatch '[^\n]+' do table.insert(result, line) end return result 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 if tonumber(old_data.ports or 0) ~= tonumber(new_data.ports or 0) then rsp["num_ports"] = { old_num_ports = old_data.ports or 0, new_num_ports = new_data.ports or 0 } 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 -- 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 if num_cve_solved > 0 then rsp["num_cve_solved"] = num_cve_solved rsp["cve_solved"] = cve_solved end if num_new_cve_issues > 0 then rsp["num_new_cve_issues"] = num_new_cve_issues rsp["new_cve"] = new_cve end if table.empty(rsp) then rsp = nil else rsp["host"] = host rsp["host_name"] = host_name rsp["scan_type"] = scan_type end return rsp end -- ############################################## 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 -- 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) 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 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) 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 if(string.sub(l, 1, 2) == " [") then local c = string.split(string.sub(l,3), "]") if(scan_type == "cve") then l = '['..c[1]..']'..c[2] elseif(scan_type == "openvas") then l = '['..c[1]..']'..c[2] end table.insert(cve, c[1]) 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, udp_ports, tcp_ports 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 hosts_details = vs_utils.retrieve_hosts_to_scan() for _,value in ipairs(hosts_details) do if (item.host == value.host and item.scan_type == value.scan_type ) then return true end end return false end -- ********************************************************** -- Function to save host configuration 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) local checks = require "checks" local host_name = "" local trigger_alert = checks.isCheckEnabled("system", "vulnerability_scan") or false 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 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 if trigger_alert and old_data and (old_data.is_ok_last_scan == vs_utils.scan_status.ok) then 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_data.cve, }, { vulnerabilities = num_vulnerabilities_found, ports = num_open_ports, cve = cve, }) if host_info_to_cache then ntop.rpushCache(scanned_hosts_changes_key, json.encode(host_info_to_cache)) end end 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 if (isEmptyString(is_ok_last_scan)) then is_ok_last_scan = vs_utils.scan_status.not_scanned end local new_item = { host = host, host_name = host_name, scan_type = scan_type, ports = ports, num_open_ports = num_open_ports, num_vulnerabilities_found = num_vulnerabilities_found, cve = cve, id = epoch_id, is_ok_last_scan = is_ok_last_scan } 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 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.formatPastEpochShort(last_scan_time) if 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 } 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 if(scan_result ~= nil) then local handle = io.open(get_report_path(scan_type, host), "w") local result = handle:write(scan_result) handle:close() end if not isEmptyString(id) and is_edit then vs_utils.delete_host_to_scan_by_id(id) end local result = 1 -- success if(not isAlreadyPresent(new_item)) then --saved_hosts[#saved_hosts+1] = new_item ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(new_item)) elseif not isEmptyString(id) then -- edit case ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(new_item)) else result = 2 --aleready_present end local counts = vs_utils.update_ts_counters() vs_utils.notify_end_periodicity() --ntop.setCache(host_to_scan_key, json.encode(saved_hosts)) return result, new_item.id end 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(host_scannned_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 vs_utils.notify_end_periodicity() local periodicity_scan_in_progress = ntop.getCache(host_to_scan_periodicity_key) == "1" if (periodicity_scan_in_progress) then local hosts_details = vs_utils.retrieve_hosts_to_scan() for _,item in ipairs(hosts_details) do if(item.is_periodicity and item.is_ok_last_scan == vs_utils.scan_status.scheduled) then return end end ntop.setCache(host_to_scan_periodicity_key, "0") local periodicity = ntop.getCache(host_to_scan_periodicity_key.."type") 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 local notification_message = "" if (periodicity == "1day") then notification_message = i18n("hosts_stats.page_scan_hosts.periodicity_scan_1_day_ended") elseif (periodicity == "1week") then notification_message = i18n("hosts_stats.page_scan_hosts.periodicity_scan_1_week_ended") end recipients.sendMessageByNotificationType({periodicity = periodicity, success=true, message = notification_message}, "vulnerability_scans") end end -- ********************************************************** -- Function to retrieve hosts list to scan function vs_utils.retrieve_hosts_to_scan() local hash_keys = ntop.getHashKeysCache(host_to_scan_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) 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 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 to retrieve last host scan result function vs_utils.retrieve_hosts_scan_result(scan_type, host) 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 "" end end -- ********************************************************** -- Function to delete host to scan function vs_utils.delete_host_to_scan(host, scan_type, all) if all then ntop.delCache(host_to_scan_key) ntop.delCache(host_scan_queue_key) ntop.delCache(host_to_scan_periodicity_key) ntop.delCache(host_to_scan_periodicity_key.."type") 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) -- 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(not((r.scan_type == "cve") and (r.host == "127.0.0.1"))) 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) 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 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 vs_utils.load_module(name) package.path = dirs.installdir .. "/scripts/lua/modules/vulnerability_scan/modules/?.lua;".. package.path return(require(name):new()) end 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 local scan_module = vs_utils.load_module("tcp_openports") result,duration,scan_result,num_open_ports,num_vulnerabilities_found, cve, udp_ports, tcp_ports = scan_module:scan_host(host, ports) -- FIX ME -> only tcp for now for _,port in ipairs(tcp_ports) do if (_ == 1) then scan_ports = ""..port else scan_ports = scan_ports .. ","..port end end return scan_ports end -- ********************************************************** -- Function to exec single host scan function vs_utils.scan_host(scan_type, host, ports, scan_id) if debug_print then traceError(TRACE_NORMAL,TRACE_CONSOLE,"Scanning Host ".. host .. " on Ports: " .. ports .. "\n") end if (isEmptyString(ports)) then ports = vs_utils.discover_open_ports(host) end vs_utils.set_status_scan(scan_type, host, ports, id, nil, vs_utils.scan_status.scanning) local scan_module = vs_utils.load_module(scan_type) local result,duration,scan_result,num_open_ports,num_vulnerabilities_found, cve, udp_ports, tcp_ports = scan_module:scan_host(host, ports) -- FIX HERE UDP ports tcp_ports = {ports = ports, num_ports = num_open_ports} if scan_result then scan_result = vs_utils.scan_status.ok ntop.incrCache(host_scannned_count_key) end if debug_print then traceError(TRACE_NORMAL,TRACE_CONSOLE,"End scan Host ".. host .. ", result: " .. result .. "\n") end if (isAlreadyPresent({host= host, scan_type= scan_type})) then vs_utils.save_host_to_scan(scan_type, host, result, now, duration, scan_result, ports, nil, num_open_ports, num_vulnerabilities_found, cve, scan_id, false, udp_ports, tcp_ports) end return true end -- ********************************************************** -- Function to update single host status function vs_utils.set_status_scan(scan_type, host, ports, id, is_periodicity, 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) if(not isEmptyString(host_hash_value_string)) then local host_hash_value = json.decode(host_hash_value_string) host_hash_value.is_ok_last_scan = status if (is_periodicity ~= nil) then host_hash_value.is_periodicity = is_periodicity end ntop.setHashCache(host_to_scan_key, host_hash_key, json.encode(host_hash_value)) end return true end -- ********************************************************** function vs_utils.schedule_host_scan(scan_type, host, ports, scan_id, is_periodicity) local scan = { scan_type = scan_type, host = host, ports = ports, id= scan_id} vs_utils.set_status_scan(scan_type, host, ports, scan_id, is_periodicity, vs_utils.scan_status.scheduled) ntop.rpushCache(host_scan_queue_key, json.encode(scan)) return true end -- ********************************************************** function vs_utils.schedule_all_hosts_scan() local host_to_scan_list = vs_utils.retrieve_hosts_to_scan() if #host_to_scan_list > 0 then for _,scan_info in ipairs(host_to_scan_list) do vs_utils.schedule_host_scan(scan_info.scan_type, scan_info.host, scan_info.ports, scan_info.id, false) end end 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(host_to_scan_periodicity_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_host_scan(scan_info.scan_type, scan_info.host, scan_info.ports, scan_info.id, true) is_scanning_almost_one = true end end if is_scanning_almost_one then ntop.setCache(host_to_scan_periodicity_key , "1") ntop.setCache(host_to_scan_periodicity_key.."type", periodicity) local notification_message = "" if (periodicity == "1day") then notification_message = i18n("hosts_stats.page_scan_hosts.periodicity_scan_1_day_started") elseif (periodicity == "1week") then notification_message = i18n("hosts_stats.page_scan_hosts.periodicity_scan_1_week_started") end recipients.sendMessageByNotificationType({periodicity = periodicity, success=true, message = notification_message}, "vulnerability_scans") end end end return true end -- ********************************************************** -- Process a single host scan request that has been queued function vs_utils.process_oldest_scheduled_scan() local elem = ntop.lpopCache(host_scan_queue_key) if((elem ~= nil) and (elem ~= "")) then if debug_print then traceError(TRACE_NORMAL,TRACE_CONSOLE,"Found vulnerability scan: ".. elem .. "\n") end local elem = json.decode(elem) vs_utils.scan_host(elem.scan_type, elem.host, elem.ports, elem.id) return true else return false end end -- ********************************************************** -- Process a single host scan request that has been queued function vs_utils.process_all_scheduled_scans(max_num_scans) local num = 0 if(max_num_scans == nil) then max_num_scans = 9999 end while(max_num_scans > 0) do local res = vs_utils.process_oldest_scheduled_scan() if(res == false) then break else max_num_scans = max_num_scans - 1 num = num + 1 end 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 = {} 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 s = string.split(host, '%.') local net = s[1].."."..s[2].."."..s[3].."." local command = 'nmap -sP -n ' .. net .. '1-254 | grep "Nmap scan report for" | cut -d " " -f 5' local out = ntop.execCmd(command) local l = lines(out) for _,h in pairs(l) do result[#result+1] = h end end return result 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 end return true end -- ********************************************************** return vs_utils