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

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

require "ntop_utils"
require "flow_utils"
local json = require ("dkjson")
local checks = require("checks")

local syslog_module = {
  -- Script category
  category = checks.check_categories.security,

  nedge_exclude = true,

  key = "suricata",

  -- See below
  hooks = {},

  gui = {
    i18n_title = "suricata_collector.title",
    i18n_description = "suricata_collector.description",
  },
}

local external_stats_key = "ntopng.prefs.ifid_"..tostring(interface.getId())..'.external_stats'

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

-- The function below is called once (#pragma once)
function syslog_module.setup()
   -- Cleanup old stats, if any
   ntop.delCache(external_stats_key)

   return true
end

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

local function parseFiveTuple(event, flow)
   flow.vlan_id = tonumber(event.vlan)
   flow.src_ip = event.src_ip
   flow.dst_ip = event.dest_ip
   flow.src_port = tonumber(event.src_port)
   flow.dst_port = tonumber(event.dest_port)
   flow.l4_proto = event.proto
end

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

local function parseFlowMetadata(event_flow, flow)
   flow.first_switched_iso8601 = event_flow.start
   flow.last_switched_iso8601 = event_flow['end']
   flow.in_pkts = tonumber(event_flow.pkts_toserver)
   flow.out_pkts = tonumber(event_flow.pkts_toclient)
   flow.in_bytes = tonumber(event_flow.bytes_toserver)
   flow.out_bytes = tonumber(event_flow.bytes_toclient)
end

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

local function parseAlertMetadata(event_alert, flow)

   -- Alert severity in Suricata is 1..255, however it is usually 1..4
   -- where 1 is highest priority

   if event_alert.severity ~= nil then
      -- Severity is currently not used
   end

   local external_alert = {
      source = "suricata",
      alert = event_alert,
   }

   flow.external_alert = json.encode(external_alert)
end

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

local function parsenDPIMetadata(event_ndpi, flow)

   -- Read nDPI metadata coming from the ndpi plugin in Suricata

   if event_ndpi.proto_id then
      local parts = string.split(event_ndpi.proto_id, "%.")
      if parts and #parts > 1 then
         flow.master_protocol = tonumber(parts[1])
         flow.app_protocol = tonumber(parts[2])
      else
         flow.app_protocol = tonumber(event_ndpi.proto_id)
      end
      if event_ndpi.confidence then
         for id, label in pairs(event_ndpi.confidence) do
            flow.confidence = tonumber(id)
         end
      end
   end
end

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

local function parseNetflowMetadata(event_flow, flow)
   flow.first_switched_iso8601 = event_flow.start
   flow.last_switched_iso8601 = event_flow['end']
   flow.in_pkts = tonumber(event_flow.pkts)
   flow.in_bytes = tonumber(event_flow.bytes)
end

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

local function parseHTTPMetadata(event_http, flow)
   flow.http_method = event_http.http_method
   flow.http_ret_code = tonumber(event_http.status)
   flow.http_site = event_http.hostname
   if event_http.hostname ~= nil and event_http.url ~= nil then
      flow.http_url = event_http.hostname..event_http.url
   end

   -- Additional fields
   flow.HTTP_PROTOCOL = event_http.protocol
   flow.HTTP_REFERER = event_http.http_refer
   flow.HTTP_MIME = event_http.http_content_type
   flow.HTTP_LENGTH = event_http.length
end

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

local function parseFileInfoMetadata(event_fileinfo, flow)
   -- Additional fields
   flow.FILE_NAME = event_fileinfo.filename
   flow.FILE_SIZE = event_fileinfo.size
   flow.FILE_STATE = event_fileinfo.state
   flow.FILE_GAPS = event_fileinfo.gaps
   flow.FILE_STORED = event_fileinfo.stored
   flow.FILE_ID = event_fileinfo.file_id
end

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

local function parseDNSMetadata(event_dns, flow)

   -- Backward compatibility
   if event_dns.type == "query" then
      flow.dns_query = event_dns.rrname
      flow.dns_query_type = get_dns_type(event_dns.rrtype)
      flow.dns_query_id = event_dns.id
   end

   if event_dns.query 
      and type(event_dns.query) == "table"
      and table.len(event_dns.query) > 0 then

      local query = event_dns.query[1]

      if query.type == "query" then
         flow.dns_query = query.rrname
         flow.dns_query_type = get_dns_type(query.rrtype)
      end

      flow.dns_query_id = query.id
   end

end

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

local function parseTLSMetadata(event_tls, flow)
   flow.tls_server_name = event_tls.sni

   -- Additional fields
   flow.TLS_VERSION = event_tls.version
   flow.TLS_CERT_NOT_BEFORE = event_tls.notbefore
   flow.TLS_CERT_AFTER = event_tls.notafter
   flow.TLS_CERT_SHA1 = event_tls.fingerprint
   flow.TLS_CERT_SUBJECT = event_tls.subject
   flow.TLS_CERT_DN = event_tls.issuerdn
   flow.TLS_CERT_SN = event_tls.serial
end

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

local function parseStats(event_stats)
   if((event_stats.capture.kernel_packets ~= nil) and (event_stats.capture.kernel_drops ~= nil)) then
      local external_stats = {}
      
      external_stats.capture_packets = (event_stats.capture.kernel_packets - event_stats.capture.kernel_drops)
      external_stats.capture_drops = event_stats.capture.kernel_drops
      
      external_stats.signatures_loaded = 0
      external_stats.signatures_failed = 0
      for _, engine in ipairs(event_stats.detect.engines) do
	 external_stats.signatures_loaded = external_stats.signatures_loaded + engine.rules_loaded
	 external_stats.signatures_failed = external_stats.signatures_failed + engine.rules_failed
      end
      
      external_stats.i18n_title = "suricata_collector.statistics"
      
      local external_json_stats = json.encode(external_stats)
      ntop.setCache(external_stats_key, external_json_stats)
   end
end

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

-- The function below is called for each received alert
function syslog_module.hooks.handleEvent(syslog_conf, message, host, priority)
   local handled = false
   local num_unhandled = 0
   local num_alerts = 0
   local num_collected_flows = 0

   local event = json.decode(message)
   if event == nil or type(event) ~= "table" then
      num_unhandled = num_unhandled + 1
      interface.incSyslogStats(1, 0, num_unhandled, num_alerts, 0, num_collected_flows)
      return
   end

   local flow = {}
   parseFiveTuple(event, flow)

   -- Additional (common) fields
   flow.SURICATA_FLOW_ID = event.flow_id
   flow.SURICATA_APP_PROTO = event.app_proto
   flow.COMMUNITY_ID = event.community_id

   if event.ndpi then
      parsenDPIMetadata(event.ndpi, flow)
   end

   if event.event_type == "alert" then

      if event.flow ~= nil then
         parseFlowMetadata(event.flow, flow)
         if event.alert ~= nil then
            parseAlertMetadata(event.alert, flow)
            num_alerts = num_alerts + 1

            if event.http ~= nil then
               parseHTTPMetadata(event.http, flow)
            elseif event.dns ~= nil then
               parseDNSMetadata(event.dns, flow) 
            elseif event.tls ~= nil then
               parseTLSMetadata(event.tls, flow) 
            end

         else
            num_unhandled = num_unhandled + 1
            flow = nil
         end

      else
         num_unhandled = num_unhandled + 1
         flow = nil
      end
 
   elseif event.event_type == "netflow" and event.netflow ~= nil then
      parseNetflowMetadata(event.netflow, flow)
      num_collected_flows = num_collected_flows + 1

      if event.http ~= nil then
         parseHTTPMetadata(event.http, flow)
      elseif event.dns ~= nil then
         parseDNSMetadata(event.dns, flow) 
      elseif event.tls ~= nil then
         parseTLSMetadata(event.tls, flow) 
      end

   --[[
   elseif event.event_type == "http" and event.http ~= nil then
      parseHTTPMetadata(event.http, flow)
      num_collected_flows = num_collected_flows + 1

   elseif event.event_type == "dns" and event.dns ~= nil then
      parseDNSMetadata(event.dns, flow) 
      num_collected_flows = num_collected_flows + 1

   elseif event.event_type == "tls" and event.tls ~= nil then
      parseTLSMetadata(event.tls, flow) 
      num_collected_flows = num_collected_flows + 1
   --]]

   elseif event.event_type == "fileinfo" then
      if event.app_proto == "http" and event.http ~= nil then
         parseHTTPMetadata(event.http, flow)
      end
      parseFileInfoMetadata(event.fileinfo, flow)
      num_collected_flows = num_collected_flows + 1

   elseif event.event_type == "stats" and event.stats ~= nil then
      parseStats(event.stats)
      flow = nil
   else
      -- traceError(TRACE_NORMAL, TRACE_CONSOLE, "Unsupported Suricata event '"..event.event_type.."'")
      num_unhandled = num_unhandled + 1
      flow = nil
   end

   if flow ~= nil then
      -- If first/last ts is not available, use the event timestamp as last resort
      if flow.first_switched_iso8601 == nil then
         flow.first_switched_iso8601 = event.timestamp
      end
      if flow.last_switched_iso8601 == nil then
         flow.last_switched_iso8601 = event.timestamp
      end

      -- Processing flow or alert
      interface.processFlow(flow)
   end

   interface.incSyslogStats(1, 0, num_unhandled, num_alerts, 0, num_collected_flows)
end 

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

-- The function below is called once (#pragma once)
function syslog_module.teardown()
   return true
end

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

return syslog_module
