local ipOps = require "ipOps"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local stringaux = require "stringaux"
local table = require "table"
local type    = type
local next    = next
local pairs   = pairs
local ipairs  = ipairs
local select  = select
local tostring = tostring
local insert  = table.insert
local concat  = table.concat
local sort    = table.sort
local setmetatable = setmetatable
local string_format = string.format
local string_upper = string.upper
local debug = stdnse.debug
local compare_ip = ipOps.compare_ip
_ENV = stdnse.module("vulns", stdnse.seeall)
local VULNS
local NMAP_ID_NUM = 0
local SHOW_ALL = stdnse.get_script_args('vulns.showall') or
                    stdnse.get_script_args('vuln.showall') or
                    stdnse.get_script_args('vulns.show-all') or
                    stdnse.get_script_args('vuln.show-all')
local SHORT_OUTPUT = stdnse.get_script_args('vulns.short')
STATE = {
  LIKELY_VULN = 0x01,
  NOT_VULN = 0x02,
  VULN = 0x04,
  DoS = 0x08,
  EXPLOIT = 0x10,
  UNKNOWN = 0x20,
}
STATE_MSG = {
  [STATE.LIKELY_VULN] = 'LIKELY VULNERABLE',
  [STATE.NOT_VULN] = 'NOT VULNERABLE',
  [STATE.VULN] = 'VULNERABLE',
  [STATE.DoS] = 'VULNERABLE (DoS)',
  [STATE.EXPLOIT] = 'VULNERABLE (Exploitable)',
  [STATE.DoS | STATE.VULN] = 'VULNERABLE (DoS)',
  [STATE.EXPLOIT | STATE.VULN] = 'VULNERABLE (Exploitable)',
  [STATE.UNKNOWN] = 'UNKNOWN (unable to test)',
}
local RISK_FACTORS = {
  ['HIGH'] = true,
  ['MEDIUM'] = true,
  ['LOW'] = true,
}
local function tcopy(src)
  if src and type(src) == "table" then
    if next(src) then
      local dst = {}
      for k,v in pairs(src) do
        if type(v) == "table" then
          dst[k] = tcopy(v)
        else
          dst[k] = v
        end
      end
      return dst
    else
      return nil
    end
  end
  return src
end
local function tadd(dst, src)
  if dst and type(dst) == "table" and src and type(src) == "table" then
    for _, v in ipairs(src) do
      dst[#dst + 1] = v
    end
  end
end
local POPULAR_IDS_LINKS = {
  CVE = function(id)
          local link = 'https://cve.mitre.org/cgi-bin/cvename.cgi?name='
          return string_format("%s%s", link, id)
        end,
  OSVDB = function(id)
            local link = 'http://osvdb.org/'
            return string_format("%s%s", link, id)
          end,
  BID = function(id)
          local link = 'https://www.securityfocus.com/bid/'
          return string_format("%s%s", link, id)
        end,
}
register_popular_id = function(id_type, callback)
  if id_type and callback and type(id_type) == "string" and
    type(callback) == "function" then
      POPULAR_IDS_LINKS[string_upper(id_type)] = callback
      return true
  end
  return false
end
get_popular_link = function(id_type, id)
  local id_vuln_type = string_upper(id_type)
  if POPULAR_IDS_LINKS[id_vuln_type] then
    return POPULAR_IDS_LINKS[id_vuln_type](id)
  end
end
local validate_vuln = function(vuln_table)
  local ret = false
  if type(vuln_table) == "table" and vuln_table.title and
  type(vuln_table.title) == "string" and vuln_table.state and
  STATE_MSG[vuln_table.state] then
    if vuln_table.risk_factor then
      if type(vuln_table.risk_factor) == "string" and
        vuln_table.risk_factor:len() > 0 then
        if RISK_FACTORS[string_upper(vuln_table.risk_factor)] then
          ret = true
        end
      end
    else
      ret = true
    end
  end
  return ret
end
local normalize_vuln_info = function(vuln_table)
  if not vuln_table.IDS then
    vuln_table.IDS = vuln_table.ids or {}
  end
  if not next(vuln_table.IDS) then
    NMAP_ID_NUM = NMAP_ID_NUM + 1
    vuln_table.IDS.NMAP_ID = string_format("NMAP-%d", NMAP_ID_NUM)
  else
    for id_type, id in pairs(vuln_table.IDS) do
      if type(id) == "number" then
        vuln_table.IDS[id_type] = tostring(id)
      end
    end
  end
  if vuln_table.state == STATE.DoS or
  vuln_table.state == STATE.EXPLOIT then
    vuln_table.state = vuln_table.state | STATE.VULN
  end
  if vuln_table.description and
  type(vuln_table.description) == "string" then
    vuln_table.description = {vuln_table.description}
  end
  if vuln_table.check_results and
  type(vuln_table.check_results) == "string" then
    vuln_table.check_results = {vuln_table.check_results}
  end
  if vuln_table.exploit_results and
  type(vuln_table.exploit_results) == "string" then
    vuln_table.exploit_results = {vuln_table.exploit_results}
  end
  if vuln_table.extra_info and
  type(vuln_table.extra_info) == "string" then
    vuln_table.extra_info = {vuln_table.extra_info}
  end
  if vuln_table.references and
  type(vuln_table.references) == "string" then
    vuln_table.references = {vuln_table.references}
  end
end
local default_filter = function(vuln_table) return true end
local register_filter = function(filters_db, filter_callback)
  if filter_callback and type(filter_callback) == "function" then
    filters_db[#filters_db + 1] = filter_callback
  else
    filters_db[#filters_db + 1] = default_filter
  end
  return #filters_db
end
local filter_vulns = function(filters_db, vuln_table)
  local FIDS = {}
  for fid, callback in ipairs(filters_db) do
    if callback(vuln_table) == true then
      FIDS[#FIDS + 1] = fid
    end
  end
  return next(FIDS) and FIDS or nil
end
local l_add_id_type = function(fid_table, id_type)
  fid_table[string_upper(id_type)] = fid_table[id_type] or {}
end
local l_get_host_port_key = function(vuln_table)
  local target = ""
  if vuln_table.host and next(vuln_table.host) then
    target = stdnse.get_hostname(vuln_table.host)
    if vuln_table.port and next(vuln_table.port) then
      target = target..string_format(":%d", vuln_table.port.number)
    end
  end
  return target
end
local l_update_id = function(fid_table, id_type, id, vuln_table)
  local id_type = string_upper(id_type)
  l_add_id_type(fid_table, id_type)
  fid_table[id_type][id] = fid_table[id_type][id] or {}
  fid_table[id_type][id]['ENTRIES'] = fid_table[id_type][id]['ENTRIES'] or {}
  local push_table = fid_table[id_type][id]['ENTRIES']
  if vuln_table.host and next(vuln_table.host) then
    local target_key = l_get_host_port_key(vuln_table)
    local host_info = string_format(" (host:%s %s)", vuln_table.host.ip, target_key)
    debug(5,
      "vulns.lua: Updating VULNS.FILTERS_IDS{} with '%s' ID:%s:%s %s",
      vuln_table.title, id_type, id, host_info)
    push_table.HOSTS = push_table.HOSTS or {}
    push_table.HOSTS[vuln_table.host.ip] =
        push_table.HOSTS[vuln_table.host.ip] or {}
    push_table.HOSTS[vuln_table.host.ip][target_key] = vuln_table
    return push_table.HOSTS[vuln_table.host.ip][target_key]
  else
    debug(5,
      "vulns.lua: Updating VULNS.FILTERS_IDS{} with '%s' ID:%s:%s",
      vuln_table.title, id_type, id)
    push_table.NETWORKS = push_table.NETWORKS or {}
    push_table.NETWORKS[#push_table.NETWORKS + 1] = vuln_table
    return push_table.NETWORKS[#push_table.NETWORKS]
  end
end
local l_lookup_id = function(fid_table, id_type, id)
  local id_type = string_upper(id_type)
  if fid_table[id_type] then
    return fid_table[id_type][id]
  end
end
local l_push_references = function(references_db, new_refs)
  if new_refs and next(new_refs) then
    local refs = {}
    for _, l in ipairs(new_refs) do
      refs[l] = true
    end
    insert(references_db, refs)
    return references_db[#references_db]
  end
end
local l_push_vuln = function(vulndb, new_vuln)
  local new_vuln = new_vuln
  local vuln = {
    title = new_vuln.title,
    state = new_vuln.state,
    _FIDS_MATCH = tcopy(new_vuln._FIDS_MATCH),
    IDS = {},
  }
  if new_vuln.IDS and next(new_vuln.IDS) then
    for id_type, id in pairs(new_vuln.IDS) do
      vuln.IDS[string_upper(id_type)] = id
    end
  end
  if (new_vuln.state & STATE.NOT_VULN) == 0 then
    if new_vuln.risk_factor then
      vuln.risk_factor = new_vuln.risk_factor
      vuln.scores = tcopy(new_vuln.scores)
    end
    vuln.description = tcopy(new_vuln.description)
    vuln.dates = tcopy(new_vuln.dates)
  end
  vuln.references = l_push_references(vulndb.SHARED.REFERENCES,
                                      new_vuln.references)
  if new_vuln.script_name then
    vuln.scripts = {}
    insert(vuln.scripts, new_vuln.script_name)
  end
  local ref_vuln
  if new_vuln.host and next(new_vuln.host) then
    vuln.host = tcopy(new_vuln.host)
    vuln.port = tcopy(new_vuln.port)
    vulndb.ENTRIES.HOSTS[vuln.host.ip] = vulndb.ENTRIES.HOSTS[vuln.host.ip] or {}
    insert(vulndb.ENTRIES.HOSTS[vuln.host.ip], vuln)
    ref_vuln = vulndb.ENTRIES.HOSTS[vuln.host.ip][#vulndb.ENTRIES.HOSTS[vuln.host.ip]]
  else
    insert(vulndb.ENTRIES.NETWORKS, vuln)
    ref_vuln = vulndb.ENTRIES.NETWORKS[#vulndb.ENTRIES.NETWORKS]
  end
  return ref_vuln
end
local l_update_references = function(references_db, old_refs, new_refs)
  if old_refs and next(old_refs) and new_refs and next(new_refs) then
    for _, l in ipairs(new_refs) do
      old_refs[l] = true
    end
  end
  return next(old_refs) and old_refs or nil
end
local l_update_vuln = function(vulndb, old_vuln, new_vuln)
  local old_vuln, new_vuln = old_vuln, new_vuln
  if old_vuln.state < new_vuln.state then
    old_vuln.state = new_vuln.state
  end
  for fid_table in pairs(new_vuln._FIDS_MATCH) do
    old_vuln[fid_table] = true
  end
  if new_vuln.IDS and next(new_vuln.IDS) then
    for id_type, id in pairs(new_vuln.IDS) do
      local id_vuln_type = string_upper(id_type)
      if not old_vuln.IDS[id_vuln_type] then
        old_vuln.IDS[id_vuln_type] = id
      end
    end
  end
  if (old_vuln.state & STATE.NOT_VULN) ~= 0 then
    old_vuln.risk_factor = nil
    old_vuln.scores = nil
    old_vuln.description = nil
    old_vuln.dates = nil
  else
    if new_vuln.risk_factor then
      old_vuln.risk_factor = new_vuln.risk_factor
      if not old_vuln.scores and new_vuln.scores then
        old_vuln.scores = tcopy(new_vuln.scores)
      end
    end
    if not old_vuln.description and new_vuln.description then
      old_vuln.description = tcopy(new_vuln.description)
    end
    if not old_vuln.dates and new_vuln.dates then
      old_vuln.dates = tcopy(old_vuln.dates)
    end
  end
  if not old_vuln.port and new_vuln.port then
    old_vuln.port = tcopy(new_vuln.port)
  end
  if new_vuln.script_name then
    old_vuln.scripts = old_vuln.scripts or {}
    insert(old_vuln.scripts, new_vuln.script_name)
  end
  if new_vuln.references and next(new_vuln.references) then
    old_vuln.references = l_update_references(vulndb.SHARED.REFERENCES,
                                              old_vuln.references,
                                              new_vuln.references)
  end
  return old_vuln
end
local l_add = function(vulndb, vuln_table)
  local vuln_table = vuln_table
  local FIDS = filter_vulns(vulndb.FILTERS_FUNCS, vuln_table)
  if not FIDS then
    return false
  else
    vuln_table._FIDS_MATCH = {}
    for _, fid in ipairs(FIDS) do
      vuln_table._FIDS_MATCH[vulndb.FILTERS_IDS[fid]] = true
    end
  end
  local NEW_IDS = {}
  local old_entries = {}
  local ids_count = 0
  local ids_found = 0
  local host_info, target_key = "", ""
  if vuln_table.host and next(vuln_table.host) then
    target_key = l_get_host_port_key(vuln_table)
    host_info = string_format(" (host:%s %s)", vuln_table.host.ip, target_key)
  end
  for _, fid in ipairs(FIDS) do
    for id_type, id in pairs(vuln_table.IDS) do
      ids_count = ids_count + 1
      local id_not_found = true
      debug(5,
        "vulns.lua: Searching VULNS.FILTERS_IDS[%d] for '%s' ID:%s:%s",
        fid, vuln_table.title, id_type, id)
      local db = l_lookup_id(vulndb.FILTERS_IDS[fid], id_type, id)
      if db and db.ENTRIES and db.ENTRIES.HOSTS then
        if vuln_table.host and next(vuln_table.host) then
          local old_vuln_list = db.ENTRIES.HOSTS[vuln_table.host.ip]
          if old_vuln_list then
            local tmp_vuln = old_vuln_list[target_key]
            if tmp_vuln then
              debug(5,
              "vulns.lua: VULNS.FILTERS_IDS[%d] '%s' ID:%s:%s%s: FOUND",
                fid, vuln_table.title, id_type, id, host_info)
              if old_entries[#old_entries] ~= tmp_vuln then
                old_entries[#old_entries + 1] = tmp_vuln
              end
              ids_found = ids_found + 1
              id_not_found = false
            end
          end
        end
      end
      if id_not_found then
        debug(5,
          "vulns.lua: VULNS.FILTERS_IDS[%d] '%s' ID:%s:%s%s: NOT FOUND",
          fid, vuln_table.title, id_type, id, host_info)
        NEW_IDS[id_type] = {['id'] = id, ['fid'] = fid}
      end
    end
  end
  local vuln_ref
  if ids_found > 0 then
    if #old_entries > 1 then
      debug(3, "vulns.lua: Warning at vuln '%s': "..
          "please check the vulnerability IDs field.", vuln_table.title)
      for _, old_vuln in ipairs(old_entries) do
        debug(3, "vulns: Warning at vuln '%s': "..
            "please check the vulnerability IDs field.", old_vuln.title)
      end
    end
    debug(3,
        "vulns.lua: Updating vulnerability entry: '%s'%s",
        vuln_table.title, host_info)
    debug(3,
        "vulns.lua: Vulnerability '%s' referenced by %d IDs from %d (%s)",
        vuln_table.title, ids_found, ids_count,
        ids_found < ids_count and "Bad" or "Good")
    vuln_ref = l_update_vuln(vulndb, old_entries[1], vuln_table)
  else
    debug(3,
        "vulns.lua: Adding new vulnerability entry: '%s'%s",
        vuln_table.title, host_info)
    vuln_ref = l_push_vuln(vulndb, vuln_table)
  end
  if ids_found < ids_count then
    for _, fid in ipairs(FIDS) do
      for id_type, new_entry in pairs(NEW_IDS) do
        if new_entry['fid'] == fid then
          debug(5,
            "vulns.lua: Updating VULNS.FILTERS_IDS[%d]", new_entry.fid)
          l_update_id(vulndb.FILTERS_IDS[new_entry['fid']],
                      id_type, new_entry.id, vuln_ref)
        end
      end
    end
  end
  return true
end
local l_normalize_selection_filter = function(filter)
  if filter and type(filter) == "table" and next(filter) then
    local ret = {}
    if filter.state and STATE_MSG[filter.state] then
      ret.state = filter.state
    end
    if filter.risk_factor and type(filter.risk_factor) == "string" and
    RISK_FACTORS[string_upper(filter.risk_factor)] then
      ret.risk_factor = string_upper(filter.risk_factor)
    end
    if filter.hosts_filter and
    type(filter.hosts_filter) == "function" then
      ret.hosts_filter = filter.hosts_filter
    end
    if filter.ports_filter and
    type(filter.ports_filter) == "function" then
      ret.ports_filter = filter.ports_filter
    end
    if filter.id_type and type(filter.id_type) == "string" then
      ret.id_type = string_upper(filter.id_type)
      ret.id = filter.id
    end
    return ret
  end
end
local l_filter_vuln = function(vuln_table, filter)
  if filter and next(filter) then
    if filter.state and (vuln_table.state & filter.state) == 0 then
      return false
    end
    if filter.risk_factor then
      if not vuln_table.risk_factor or
      string_upper(vuln_table.risk_factor) ~= string_upper(filter.risk_factor) then
        return false
      end
    end
    if filter.hosts_filter then
      if not vuln_table.host or not next(vuln_table.host) or
      not filter.hosts_filter(vuln_table.host) then
        return false
      end
    end
    if filter.ports_filter then
      if not vuln_table.port or not next(vuln_table.port) or
      not filter.ports_filter(vuln_table.port) then
        return false
      end
    end
    if filter.id_type then
      if not vuln_table.IDS or not next(vuln_table.IDS) or
      not vuln_table.IDS[filter.id_type] then
        return false
      elseif filter.id then
        return (vuln_table.IDS[filter.id_type] == filter.id)
      end
    end
  end
  return true
end
local l_find_by_id = function(fid_table, vuln_id_type, id)
  local out = {}
  local db = l_lookup_id(fid_table, vuln_id_type, id)
  if db then
    debug(5,
      "vulns.lua: Lookup VULNS.FILTERS_IDS{}  for ID:%s:%s:  FOUND",
      vuln_id_type, id)
    if db.ENTRIES and db.ENTRIES.HOSTS and next(db.ENTRIES.HOSTS) then
      for _, vuln_list in pairs(db.ENTRIES.HOSTS) do
        for _, vuln_table in pairs(vuln_list) do
          debug(5,
            "vulns.lua: Vulnerability '%s' (host:%s):  FOUND",
            vuln_table.title, vuln_table.host.ip)
          out[#out + 1] = vuln_table
        end
      end
    end
    if db.ENTRIES.NETWORKS and next(db.ENTRIES.NETWORKS) then
      for _, vuln_table in ipairs(db.ENTRIES.NETWOKRS) do
        debug(5,
          "vulns.lua: Vulnerability '%s':  FOUND", vuln_table.title)
        out[#out + 1] = vuln_table
      end
    end
  end
  return next(out) and out or nil
end
local l_find_vulns = function(fid_table, entries, filter)
  local out, check_vuln = {}
  if filter then
    check_vuln = function(vuln_table, fid_table, filter)
      return vuln_table._FIDS_MATCH[fid_table] and
             l_filter_vuln(vuln_table, filter)
    end
  else
    check_vuln = function(vuln_table, fid_table)
      return vuln_table._FIDS_MATCH[fid_table]
    end
  end
  for host_ip, vulns_list in pairs(entries.HOSTS) do
    for _, vuln_table in ipairs(vulns_list) do
      if check_vuln(vuln_table, fid_table, filter) then
        debug(5,
          "vulns.lua: Vulnerability '%s' (host: %s):  FOUND",
          vuln_table.title, vuln_table.host.ip)
        out[#out + 1] = vuln_table
      end
    end
  end
  for _, vuln_table in ipairs(entries.NETWORKS) do
    if check_vuln(vuln_table, fid_table, filter) then
      debug(5,
        "vulns.lua: Vulnerability '%s':  FOUND", vuln_table.title)
      out[#out + 1] = vuln_table
    end
  end
  return next(out) and out or nil
end
local l_push_vuln_output = function(output, vlist, showall)
  local out, vuln_list = output, vlist
  for idx, vuln_table in ipairs(vuln_list) do
    local vuln_out = format_vuln_table(vuln_table, showall)
    if vuln_out then
      insert(out, concat(vuln_out, "\n"))
      if #vuln_list > 1 and idx ~= #vuln_list then
        insert(out, "")
      end
    end
  end
end
local l_make_output = function(fid_table, entries, filter)
  local hosts, networks = {}, {vulns = {}, not_vulns = {}}
  local save_not_vulns = function(vulns, vuln_table)
  end
  if SHOW_ALL then
    save_not_vulns = function(vulns, vuln_table)
      vulns[#vulns + 1] = vuln_table
    end
  end
  local check_vuln
  if filter then
    check_vuln = function(vuln_table, fid_table, filter)
      return vuln_table._FIDS_MATCH[fid_table] and
             l_filter_vuln(vuln_table, filter)
    end
  else
    check_vuln = function(vuln_table, fid_table)
      return vuln_table._FIDS_MATCH[fid_table]
    end
  end
  for ip, vulns_list in pairs(entries.HOSTS) do
    local host_entries = {
      ip = ip,
      vulns = {},
      not_vulns = {},
    }
    for _, vuln_table in ipairs(vulns_list) do
      if check_vuln(vuln_table, fid_table, filter) then
        debug(5,
          "vulns.lua: Vulnerability '%s' (host: %s):  FOUND",
          vuln_table.title, vuln_table.host.ip)
        if (vuln_table.state & STATE.NOT_VULN) == 0 then
          host_entries.vulns[#host_entries.vulns + 1] = vuln_table
        else
          save_not_vulns(host_entries.not_vulns, vuln_table)
        end
      end
    end
    host_entries.state = next(host_entries.vulns) and
                            STATE.VULN or STATE.NOT_VULN
    insert(hosts, host_entries)
  end
  for _, vuln_table in ipairs(entries.NETWORKS) do
    if check_vuln(vuln_table, fid_table, filter) then
      debug(5,
        "vulns.lua: Vulnerability '%s':  FOUND", vuln_table.title)
      if (vuln_table.state & STATE.NOT_VULN) == 0 then
        networks.vulns[#networks.vulns + 1] = vuln_table
      else
        save_not_vulns(networks.not_vulns, vuln_table)
      end
    end
  end
  local output = {}
  local function sort_hosts(a, b)
    return compare_ip(a.ip, "le", b.ip)
  end
  local function sort_ports(a, b)
    if a.port and b.port then
      return a.port.number < b.port.number
    end
    return false
  end
  if next(hosts) then
    debug(3,
      "vulns.lua: sorting vulnerability entries for %d host",
      #hosts)
    sort(hosts, sort_hosts)
    for hidx, host in ipairs(hosts) do
      insert(output, string_format("Vulnerability report for %s: %s",
                        host.ip, STATE_MSG[host.state]))
      if next(host.vulns) then
        sort(host.vulns, sort_ports)
        l_push_vuln_output(output, host.vulns)
      end
      if next(host.not_vulns) and SHOW_ALL then
        sort(host.vulns, sort_ports)
        if #host.vulns > 0 then
          insert(output, "")
        end
        l_push_vuln_output(output, host.not_vulns, SHOW_ALL)
      end
      if #hosts > 1 and hidx ~= #hosts then
        insert(output, "")
      end
    end
  end
  if next(networks.vulns) then
    if next(hosts) then
      insert(output, "")
    end
    insert(output, "VULNERABLE Entries:")
    l_push_vuln_output(output, networks.vulns)
  end
  if next(networks.not_vulns) and SHOW_ALL then
    if #networks.vulns or next(hosts) then
      insert(output, "")
    end
    insert(output, "NOT VULNERABLE Entries:")
    l_push_vuln_output(output, networks.not_vulns, SHOW_ALL)
  end
  return next(output) and output or nil
end
local registry_add_ids = function(fid, ...)
  local t = {...}
  for _, v in ipairs(t) do
    local id_type = v
    l_add_id_type(VULNS.FILTERS_IDS[fid], id_type)
  end
end
local registry_get_ids = function(fid)
  return VULNS.FILTERS_IDS[fid]
end
local registry_lookup_id = function(fid, vuln_id_type, id)
  if l_lookup_id(VULNS.FILTERS_IDS[fid], vuln_id_type, id) then
    return true
  end
  return false
end
local registry_find_by_id = function(fid, vuln_id_type, id)
  if registry_lookup_id(fid, vuln_id_type, id) then
    debug(5,
      "vulns.lua: Lookup VULNS.FILTERS_IDS[%d]  for vulnerabilities",
      fid)
    return l_find_by_id(VULNS.FILTERS_IDS[fid], vuln_id_type, id)
  end
end
local registry_find_vulns = function(fid, selection_filter)
  local fid_table = VULNS.FILTERS_IDS[fid]
  if fid_table and next(fid_table) then
    local filter = l_normalize_selection_filter(selection_filter)
    debug(5,
      "vulns.lua: Lookup VULNS.FILTERS_IDS[%d]  for vulnerabilities",
      fid)
    return l_find_vulns(VULNS.FILTERS_IDS[fid], VULNS.ENTRIES, filter)
  end
end
local registry_make_output = function(fid, selection_filter)
  local fid_table = VULNS.FILTERS_IDS[fid]
  if fid_table and next(fid_table) then
    local filter = l_normalize_selection_filter(selection_filter)
    debug(5,
      "vulns.lua: Lookup VULNS.FILTERS_IDS[%d]  for vulnerabilities",
      fid)
    local output = l_make_output(VULNS.FILTERS_IDS[fid],
                                 VULNS.ENTRIES, filter)
    return stdnse.format_output(true, output)
  end
end
local registry_add_vulns = function(script_name, ...)
  local vulns = {...}
  if not script_name or not next(vulns) then
    return false
  end
  local count = 0
  for _, vuln_table in ipairs(vulns) do
    if validate_vuln(vuln_table) then
      normalize_vuln_info(vuln_table)
      vuln_table.script_name = script_name
      debug(3,
        "vulns.lua: ***  New Vuln '%s' %sreported by '%s' script  ***",
        vuln_table.title,
        vuln_table.host and
            string_format(" host:%s ", vuln_table.host.ip) or "",
        vuln_table.script_name)
      if l_add(VULNS, vuln_table) then
        count = count + 1
      end
    end
  end
  return count > 0 and true or false, count
end
add_ids = function(fid, ...)
end
get_ids = function(fid)
end
lookup_id = function(fid, vuln_id_type, id)
end
add = function(script_name, ...)
end
find = function(fid, selection_filter)
end
find_by_id = function(fid, vuln_id_type, id)
end
make_output = function(fid, selection_filter)
end
local format_vuln_special_fields = function(vuln_field)
  local out = {}
  if vuln_field then
    if type(vuln_field) == "table" then
      for _, line in ipairs(vuln_field) do
        if type(line) == "string" then
          tadd(out, stringaux.strsplit("\r?\n", line))
        else
          insert(out, line)
        end
      end
    elseif type(vuln_field) == "string" then
      out = stringaux.strsplit("\r?\n", vuln_field)
    end
  end
  return next(out) and out or nil
end
local format_vuln_base = function(vuln_table, showall)
  if not vuln_table.title or not type(vuln_table.title) == "string" or
  not vuln_table.state or not STATE_MSG[vuln_table.state] then
    return nil
  end
  if not showall and (vuln_table.state & STATE.NOT_VULN) ~= 0 then
    debug(2, "vulns.lua: vulnerability '%s'%s: %s.",
        vuln_table.title,
        vuln_table.host and
            string_format(" (host:%s%s)", vuln_table.host.ip,
            vuln_table.host.targetname and
              " "..vuln_table.host.targetname or "")
            or "", STATE_MSG[vuln_table.state])
    return nil
  end
  local output_table = stdnse.output_table()
  local out = {}
  if SHORT_OUTPUT then
    setmetatable(out, {
        __newindex = function () return nil end
      })
  end
  output_table.title = vuln_table.title
  insert(out, vuln_table.title)
  output_table.state = STATE_MSG[vuln_table.state]
  insert(out,
      string_format("  State: %s", STATE_MSG[vuln_table.state]))
  if vuln_table.IDS and next(vuln_table.IDS) then
    local ids_t = {}
    for id_type, id in pairs(vuln_table.IDS) do
      if id_type ~= 'NMAP_ID' then
        table.insert(ids_t, string_format("%s:%s", id_type, id))
      end
    end
    if next(ids_t) then
      insert(out, string_format("  IDs:  %s", table.concat(ids_t, "  ")))
      output_table.ids = ids_t
    end
  end
  if (vuln_table.state & STATE.NOT_VULN) == 0 then
    if vuln_table.risk_factor then
      local risk_str = ""
      if vuln_table.scores and next(vuln_table.scores) then
        output_table.scores = vuln_table.scores
        for score_type, score in pairs(vuln_table.scores) do
          risk_str = risk_str .. string_format("  %s: %s", score_type, score)
        end
      end
      insert(out, string_format("  Risk factor: %s%s",
                      vuln_table.risk_factor, risk_str))
    end
    if vuln_table.description then
      local desc = format_vuln_special_fields(vuln_table.description)
      if desc then
        for _, line in ipairs(desc) do
          insert(out, string_format("    %s", line))
        end
        output_table.description = vuln_table.description
      end
    end
    if vuln_table.dates and next(vuln_table.dates) then
      output_table.dates = vuln_table.dates
      if vuln_table.dates.disclosure and
      next(vuln_table.dates.disclosure) then
        output_table.disclosure = string_format("%s-%s-%s",
          vuln_table.dates.disclosure.year,
          vuln_table.dates.disclosure.month,
          vuln_table.dates.disclosure.day)
        insert(out, string_format("  Disclosure date: %s-%s-%s",
                        vuln_table.dates.disclosure.year,
                        vuln_table.dates.disclosure.month,
                        vuln_table.dates.disclosure.day))
      end
    end
    if vuln_table.check_results then
      output_table.check_results = vuln_table.check_results
      local check = format_vuln_special_fields(vuln_table.check_results)
      if check then
        insert(out, "  Check results:")
        for _, line in ipairs(check) do
          insert(out, string_format("    %s", line))
        end
      end
    end
    if vuln_table.exploit_results then
      output_table.exploit_results = vuln_table.exploit_results
      local exploit = format_vuln_special_fields(vuln_table.exploit_results)
      if exploit then
        insert(out, "  Exploit results:")
        for _, v in ipairs(vuln_table.exploit_results) do
          insert(out, string_format("    %s", v))
        end
      end
    end
    if vuln_table.extra_info then
      output_table.extra_info = vuln_table.extra_info
      local extra = format_vuln_special_fields(vuln_table.extra_info)
      if extra then
        insert(out, "  Extra information:")
        for _, v in ipairs(vuln_table.extra_info) do
          insert(out, string_format("    %s", v))
        end
      end
    end
  end
  if vuln_table.IDS or vuln_table.references then
    local ref_set = {}
    if vuln_table.IDS and next(vuln_table.IDS) then
      for id_type, id in pairs(vuln_table.IDS) do
        local id_type = string_upper(id_type)
        local link = get_popular_link(id_type, id)
        if link then ref_set[link] = true end
      end
    end
    if vuln_table.references and next(vuln_table.references) then
      for k, v in pairs(vuln_table.references) do
        local str = type(k) == "string" and k or v
        ref_set[str] = true
      end
    end
    if next(ref_set) then
      insert(out, "  References:")
      local ref_str = {}
      for link in pairs(ref_set) do
        insert(out, string_format("    %s", link))
        table.insert(ref_str, link)
      end
      output_table.refs = ref_str
    end
  end
  if SHORT_OUTPUT then
    out = {("%s %s %s"):format(
        vuln_table.host.targetname or vuln_table.host.ip,
        STATE_MSG[vuln_table.state],
        vuln_table.IDS.CVE or vuln_table.title
      )}
  end
  return out, output_table
end
format_vuln_table = function(vuln_table, showall)
  local out = format_vuln_base(vuln_table, showall)
  if out then
    if vuln_table.host and type(vuln_table.host) == "table" and
    vuln_table.host.ip then
      local run_info = "Target: "
      if vuln_table.host.targetname then
        run_info = run_info..vuln_table.host.targetname
      end
      run_info = run_info..string_format(" (%s)", vuln_table.host.ip)
      if vuln_table.port and type(vuln_table.port == "table") and
      vuln_table.port.number then
        run_info = run_info..string_format("  Port: %s%s",
                                vuln_table.port.number,
                                vuln_table.port.service and
                                "/"..vuln_table.port.service or "")
      end
      insert(out, 1, run_info)
    end
    if vuln_table.scripts and next(vuln_table.scripts) then
      local script_list = string_format("  Reported by scripts: %s",
                              concat(vuln_table.scripts, " "))
      insert(out, script_list)
    end
    return out
  end
end
format_vuln = function(vuln_table, showall)
  local out = format_vuln_table(vuln_table, showall or SHOW_ALL)
  if out then
    return concat(out, "\n")
  end
end
save_reports = function(filter_callback)
  if not VULNS then
    nmap.registry.VULNS = nmap.registry.VULNS or {}
    VULNS = nmap.registry.VULNS
    VULNS.ENTRIES = VULNS.ENTRIES or {}
    VULNS.ENTRIES.HOSTS = VULNS.ENTRIES.HOSTS or {}
    VULNS.ENTRIES.NETWORKS = VULNS.ENTRIES.NETWORKS or {}
    VULNS.SHARED = VULNS.SHARED or {}
    VULNS.SHARED.REFERENCES = VULNS.SHARED.REFERENCES or {}
    VULNS.FILTERS_FUNCS = VULNS.FILTERS_FUNCS or {}
    VULNS.FILTERS_IDS = VULNS.FILTERS_IDS or {}
    add_ids = registry_add_ids
    get_ids = registry_get_ids
    lookup_id = registry_lookup_id
    add = registry_add_vulns
    find_by_id = registry_find_by_id
    find = registry_find_vulns
    make_output = registry_make_output
  end
  local fid = register_filter(VULNS.FILTERS_FUNCS, filter_callback)
  VULNS.FILTERS_IDS[fid] = {}
  debug(3,
      "vulns.lua: New Filter table:  VULNS.FILTERS_IDS[%d]", fid)
  return fid
end
Report = {
  new = function(self, script_name, host, port)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.entries = {vulns = {}, not_vulns = {}}
    o.script_name = script_name
    if host then
      o.host = {}
      o.host.ip = host.ip
      o.host.targetname = host.targetname
      o.host.bin_ip = host.bin_ip
      if port then
        o.port = {}
        o.port.number = port.number
        o.port.protocol = port.protocol
        o.port.service = port.service
        o.port.version = tcopy(port.version)
      end
    end
    return o
  end,
  add_popular_id = function(self, id_type, callback)
    return register_popular_id(id_type, callback)
  end,
  add_vulns = function(self, ...)
    local count = 0
    for i = 1, select("#", ...) do
      local vuln_table = select(i, ...)
      if validate_vuln(vuln_table) then
        normalize_vuln_info(vuln_table)
        vuln_table.script_name = self.script_name
        vuln_table.host = self.host
        vuln_table.port = self.port
        if (vuln_table.state & STATE.NOT_VULN) ~= 0 then
          insert(self.entries.not_vulns, vuln_table)
        else
          insert(self.entries.vulns, vuln_table)
        end
        add(vuln_table.script_name, vuln_table)
        count = count + 1
      end
    end
    return count > 0 and true or false, count
  end,
  make_output = function(self, ...)
    self:add_vulns(...)
    local vuln_count = #self.entries.vulns
    local not_vuln_count = #self.entries.not_vulns
    local output = {}
    local output_table = stdnse.output_table()
    local out_t = stdnse.output_table()
    local output_t2 = stdnse.output_table()
    if vuln_count > 0 then
      output_table.state = "VULNERABLE"
      if not SHORT_OUTPUT then
        insert(output, "VULNERABLE:")
      end
      for i, vuln_table in ipairs(self.entries.vulns) do
        local vuln_out, out_t = format_vuln_base(vuln_table)
        if type(out_t) == "table" then
          local ID = vuln_table.IDS.CVE or vuln_table.IDS[next(vuln_table.IDS)]
          output_t2[ID] = out_t
        end
        if vuln_out then
          output_table.report = concat(vuln_out, "\n")
          insert(output, concat(vuln_out, "\n"))
          if vuln_count > 1 and i ~= vuln_count then
            insert(output, "") -- separate several entries
          end
        end
      end
    end
    if not_vuln_count > 0 then
      if SHOW_ALL then
        if vuln_count > 0 then insert(output, "") end
        output_table.state = "NOT VULNERABLE"
        if not SHORT_OUTPUT then
          insert(output, "NOT VULNERABLE:")
        end
      end
      for i, vuln_table in ipairs(self.entries.not_vulns) do
        local vuln_out, out_t = format_vuln_base(vuln_table, SHOW_ALL)
        if type(out_t) == "table" then
          local ID = vuln_table.IDS.CVE or vuln_table.IDS[next(vuln_table.IDS)]
          output_t2[ID] = out_t
        end
        if vuln_out then
          output_table.report = concat(vuln_out, "\n")
          insert(output, concat(vuln_out, "\n"))
          if not_vuln_count > 1 and i ~= not_vuln_count then
            insert(output, "") -- separate several entries
          end
        end
      end
    end
   if #output==0 and #output_t2==0 then
      return nil
    end
    return output_t2, stdnse.format_output(true, output)
  end,
}
return _ENV;
