-- -- (C) 2013-22 - ntop.org -- local dirs = ntop.getDirs() package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path require "lua_utils" require "flow_utils" require "voip_utils" local shaper_utils local format_utils = require "format_utils" local have_nedge = ntop.isnEdge() local nf_config = nil local alert_consts = require "alert_consts" local alert_utils = require "alert_utils" local alert_entities = require "alert_entities" local dscp_consts = require "dscp_consts" local tag_utils = require "tag_utils" local flow_risk_utils = require "flow_risk_utils" local flow_consts = require "flow_consts" local template = require "template_utils" local categories_utils = require "categories_utils" local protos_utils = require("protos_utils") local discover = require("discover_utils") local http_utils = require("http_utils") local json = require ("dkjson") local page_utils = require("page_utils") if ntop.isPro() then package.path = dirs.installdir .. "/scripts/lua/pro/modules/?.lua;" .. package.path shaper_utils = require("shaper_utils") if ntop.isnEdge() then package.path = dirs.installdir .. "/scripts/lua/pro/nedge/modules/system_config/?.lua;" .. package.path nf_config = require("nf_config") end end function formatASN(v) local asn if(v == 0) then asn = " " else asn = "".. v .."" end print(""..asn.."\n") end local function colorNotZero(v) if(v == 0) then return("0") else return(''..formatValue(v).."") end end local function drawiecgraph(iec, total) local nodes = {} local nodes_id = {} -- tprint(iec) for k,v in pairs(iec) do local keys = split(k, ",") nodes[keys[1]] = true nodes[keys[2]] = true end print[[
]] end local function ja3url(what, safety, label) if(what == nil) then print(" ") else print(format_external_link("sslbl.abuse.ch/ja3-fingerprints/".. what .."/", what, false, "https")) if((safety ~= nil) and (safety ~= "safe")) then print(' [ '..capitalize(safety)..' Cipher ]') end end end sendHTTPContentTypeHeader('text/html') warn_shown = 0 local alert_banners = {} if isAdministrator() then if _POST["custom_hosts"] and _POST["l7proto"] then local proto_id = tonumber(_POST["l7proto"]) local proto_name = interface.getnDPIProtoName(proto_id) if protos_utils.addAppRule(proto_name, {match="host", value=_POST["custom_hosts"]}) then local info = ntop.getInfo() alert_banners[#alert_banners + 1] = { type = "success", text = i18n("custom_categories.protos_reboot_necessary", {product=info.product}) } else alert_banners[#alert_banners + 1] = { type="danger", text=i18n("flow_details.could_not_add_host_to_category", {host=_POST["custom_hosts"], category=proto_name}) } end elseif _POST["custom_hosts"] and _POST["category"] then local lists_utils = require("lists_utils") local category_id = tonumber(split(_POST["category"], "cat_")[2]) if categories_utils.addCustomCategoryHost(category_id, _POST["custom_hosts"]) then lists_utils.reloadLists() local label = interface.getnDPICategoryName(category_id) alert_banners[#alert_banners + 1] = { type="success", text=i18n("flow_details.host_successfully_added_to_category", {host=_POST["custom_hosts"], category=(i18n("ndpi_categories." .. label) or label), url = ntop.getHttpPrefix() .. "/lua/admin/edit_categories.lua?l7proto=" .. category_id}) } else local label = interface.getnDPICategoryName(category_id) alert_banners[#alert_banners + 1] = { type="danger", text=i18n("flow_details.could_not_add_host_to_category", {host=_POST["custom_hosts"], category=(i18n("ndpi_categories." .. label) or label)}) } end end end local function printAddCustomHostRule(full_url) if not isAdministrator() then return end local categories = interface.getnDPICategories() local protocols = interface.getnDPIProtocols() local short_url = categories_utils.getSuggestedHostName(full_url) -- Fill the category dropdown local cat_select_dropdown = '" -- Fill the application dropdown local app_select_dropdown = '" -- Put a note if the URL is already assigned to another customized category local existing_note = "" local matched_category = ntop.matchCustomCategory(full_url) existing_note = "
" .. i18n("flow_details.existing_rules_note", {name=i18n("custom_categories.apps_and_categories"), url=ntop.getHttpPrefix().."/lua/admin/edit_categories.lua"}) if matched_category ~= nil then local cat_name = interface.getnDPICategoryName(matched_category) existing_note = existing_note .. "

" .. i18n("details.note") .. ": " .. i18n("custom_categories.similar_host_found", {host=page_utils.safe_html(full_url), category=(i18n("ndpi_categories." .. cat_name) or cat_name)}) .. "

" end local rule_type_selection = "" if protos_utils.hasProtosFile() then rule_type_selection = i18n("flow_details.rule_type")..":"..[[

]] end print( template.gen("modal_confirm_dialog.html", { dialog={ id = "add_to_customized_categories", action = "addToCustomizedCategories()", custom_alert_class = "", custom_dialog_class = "dialog-body-full-height", title = i18n("custom_categories.custom_host_category"), message = rule_type_selection .. i18n("custom_categories.select_url_category") .. "
" .. cat_select_dropdown .. app_select_dropdown .. "
" .. i18n("custom_categories.the_following_url_will_be_added") .. '
' .. existing_note, confirm = i18n("custom_categories.add"), cancel = i18n("cancel"), } }) ) print(' ') print[[]] end local function displayContainer(cont, label) print(label) if not isEmptyString(cont["id"]) then -- short 12-chars UUID as in docker print(""..i18n("containers_stats.container")..""..format_utils.formatContainer(cont).."\n") end local k8s_name = cont["k8s.name"] local k8s_pod = cont["k8s.pod"] local k8s_ns = cont["k8s.ns"] local k8s_rows = {} if not isEmptyString(k8s_name) then k8s_rows[#k8s_rows + 1] = {i18n("flow_details.k8s_name"), k8s_name} end if not isEmptyString(k8s_pod) then k8s_rows[#k8s_rows + 1] = {i18n("flow_details.k8s_pod"), '' .. k8s_pod .. ''} end if not isEmptyString(k8s_ns) then k8s_rows[#k8s_rows + 1] = {i18n("flow_details.k8s_ns"), k8s_ns} end for i, row in ipairs(k8s_rows) do local header = '' if i == 1 then header = ""..i18n("flow_details.k8s").."" end print(""..header..""..row[1]..""..row[2].."\n") end local docker_name = cont["docker.name"] local docker_rows = {} if not isEmptyString(docker_name) then docker_rows[#docker_rows + 1] = {i18n("flow_details.docker_name"), docker_name} end for i, row in ipairs(docker_rows) do local header = '' if i == 1 then header = ""..i18n("flow_details.docker").."" end print(""..header..""..row[1]..""..row[2].."\n") end end local function displayProc(proc, label) if(proc.pid == 0) then return end print(label) print(""..i18n("flow_details.user_name").."".. proc.user_name .."\n") print(""..i18n("flow_details.process_pid_name").."".. proc.name .. " ") if proc.pid then print("["..i18n("flow_details.process_pid") .. ": "..proc.pid.."]") end if not isEmptyString(proc.pkg_name) then print(" ["..i18n("flow_details.process_package") .. ": " .. proc.pkg_name.."] ") end if not isEmptyString(proc.father_name) then print(" "..i18n("flow_details.son_of_father_process") .. " "..proc.father_name.." ") if proc.father_pid then print(i18n("flow_details.process_pid")..": ".. proc.father_pid) end if not isEmptyString(proc.father_pkg_name) then print(" "..i18n("flow_details.process_package") .. ": " .. proc.father_pkg_name) end end print("") if((proc.actual_memory ~= nil) and (proc.actual_memory > 0)) then print(""..i18n("graphs.actual_memory").."".. bytesToSize(proc.actual_memory * 1024) .. "\n") print(""..i18n("graphs.peak_memory").."".. bytesToSize(proc.peak_memory * 1024) .. "\n") end end page_utils.set_active_menu_entry(page_utils.menu_entries.flow_details) dofile(dirs.installdir .. "/scripts/lua/inc/menu.lua") printMessageBanners(alert_banners) if not table.empty(alert_banners) then print("
") end print('
 '..i18n("flow_details.now_purged")..'
') throughput_type = getThroughputType() local flow_key = _GET["flow_key"] local flow_hash_id = _GET["flow_hash_id"] flow = interface.findFlowByKeyAndHashId(tonumber(flow_key), tonumber(flow_hash_id)) local ifid = interface.name2id(ifname) local label = getFlowLabel(flow, nil, nil, nil, nil, nil, false) local title = i18n("flow")..": "..label local url = ntop.getHttpPrefix().."/lua/flow_details.lua" page_utils.print_navbar(title, url, { { active = true, page_name = "overview", label = i18n("overview"), }, } ) if(flow == nil) then print('
'..i18n("flow_details.flow_cannot_be_found_message")..' '.. purgedErrorString()..'
') else if isAdministrator() then if(_POST["drop_flow_policy"] == "true") then interface.dropFlowTraffic(tonumber(flow_key)) flow["verdict.pass"] = false end end ifstats = interface.getStats() print("\n") if ifstats.vlan and flow["vlan"] > 0 then print("\n") end print("\n") print("") if((ifstats.inline and flow["verdict.pass"]) or (flow.vrfId ~= nil)) then print("') if(flow.vrfId ~= nil) then print("") end print("\n") if(ntop.isPro() and ifstats.inline and (flow["shaper.cli2srv_ingress"] ~= nil)) then local host_pools_nedge = require("host_pools_nedge") print("") c = flowinfo2hostname(flow,"cli") s = flowinfo2hostname(flow,"srv") if flow["cli.pool_id"] ~= nil then c = c .. " (".. host_pools_nedge.poolIdToUsername(flow["cli.pool_id"]) ..")" end if flow["srv.pool_id"] ~= nil then s = s .. " (".. host_pools_nedge.poolIdToUsername(flow["srv.pool_id"]) ..")" end local shaper = shaper_utils.nedge_shaper_id_to_shaper(flow["shaper.cli2srv_egress"]) print("") local shaper = shaper_utils.nedge_shaper_id_to_shaper(flow["shaper.cli2srv_ingress"]) print("") print("") if flow["cli.pool_id"] ~= nil and flow["srv.pool_id"] ~= nil then print("") print("") print("") print("") print("") print("") end -- ENABLE MARKER DEBUG if ntop.isnEdge() and false then print("") print("") print("") end local alert_info = flow2alertinfo(flow) local forbidden_proto = flow["proto.ndpi_id"] local forbidden_peer = nil if alert_info then forbidden_proto = alert_info["devproto_forbidden_id"] or forbidden_proto forbidden_peer = alert_info["devproto_forbidden_peer"] end local cli_mac = flow["cli.mac"] and interface.getMacInfo(flow["cli.mac"]) local srv_mac = flow["srv.mac"] and interface.getMacInfo(flow["srv.mac"]) local cli_show = (cli_mac and cli_mac.location == "lan" and flow["cli.pool_id"] == 0) local srv_show = (srv_mac and srv_mac.location == "lan" and flow["srv.pool_id"] == 0) local num_rows = 0 if cli_show then num_rows = num_rows + 1 end if srv_show then num_rows = num_rows + 1 end if num_rows > 0 then print("") if cli_show then print("") print("") end if srv_show then print("") print("") end end end print("\n") print("\n") if flow["bytes"] > 0 then print("") if((ifstats.type ~= "zmq") and ((flow["proto.l4"] == "TCP") or (flow["proto.l4"] == "UDP")) and (flow["goodput_bytes"] > 0)) then print("\n") else print("\n") end print("\n") print("\n") end if(flow.iec104 and (table.len(flow.iec104.typeid) > 0)) then print("\n") -- ######################### total = 0 for k,v in pairsByValues(flow.iec104.typeid_transitions, rev) do total = total+v end print("\n") -- ######################### print("\n") print("\n") print("\n") local url = "/lua/rest/v2/get/flow/l7/iec104.lua?flow_key="..flow_key.."&flow_hash_id="..flow_hash_id.."&ifid="..ifid print("") end if((flow.tos.client.ECN ~= 0) or (flow.tos.server.DSCP ~= 0)) then print("") print("") print("") print("") end if(flow["tcp.nw_latency.client"] ~= nil) then local rtt = flow["tcp.nw_latency.client"] + flow["tcp.nw_latency.server"] if(rtt > 0) then local cli2srv = round(flow["tcp.nw_latency.client"], 3) local srv2cli = round(flow["tcp.nw_latency.server"], 3) print("\n") c = interface.getAddressInfo(flow["cli.ip"]) s = interface.getAddressInfo(flow["srv.ip"]) if(not(c.is_private and s.is_private)) then -- Inspired by https://gist.github.com/geraldcombs/d38ed62650b1730fb4e90e2462f16125 print("\n") end end end if(flow["tcp.appl_latency"] ~= nil and flow["tcp.appl_latency"] > 0) then print("\n") end if not ntop.isnEdge() then if flow["cli2srv.packets"] > 1 and flow["interarrival.cli2srv"] and flow["interarrival.cli2srv"]["max"] > 0 then print("\n") if(flow["srv2cli.packets"] < 2) then print("\n") if(flow["flow.idle"] == true) then print("") end end if((flow["cli2srv.fragments"] + flow["srv2cli.fragments"]) > 0) then rowspan = 2 print("") print("\n") print("\n") end if flow["tcp.seq_problems"] then rowspan = 1 if((flow["cli2srv.retransmissions"] + flow["srv2cli.retransmissions"]) > 0) then rowspan = rowspan + 1 end if((flow["cli2srv.out_of_order"] + flow["srv2cli.out_of_order"]) > 0) then rowspan = rowspan + 1 end if((flow["cli2srv.lost"] + flow["srv2cli.lost"]) > 0) then rowspan = rowspan + 1 end if((flow["cli2srv.keep_alive"] + flow["srv2cli.keep_alive"]) > 0) then rowspan = rowspan + 1 end if rowspan > 1 then print("") print("\n") if((flow["cli2srv.retransmissions"] + flow["srv2cli.retransmissions"]) > 0) then print("\n") end if((flow["cli2srv.out_of_order"] + flow["srv2cli.out_of_order"]) > 0) then print("\n") end if((flow["cli2srv.lost"] + flow["srv2cli.lost"]) > 0) then print("\n") end if((flow["cli2srv.keep_alive"] + flow["srv2cli.keep_alive"]) > 0) then print("\n") end end end end if(flow["protos.tls.client_requested_server_name"] ~= nil) then print("") print("") print("\n") end if((flow["protos.tls.notBefore"] ~= nil) or (flow["protos.tls.notAfter"] ~= nil)) then local now = os.time() print('\n") end if(flow["protos.tls.issuerDN"] ~= nil) then print('\n') end if(flow["protos.tls.subjectDN"] ~= nil) then print('\n') end if((flow["protos.tls.ja3.client_hash"] ~= nil) or (flow["protos.tls.ja3.server_hash"] ~= nil)) then print('") end if(flow["protos.tls.client_alpn"] ~= nil) then print('\n') end if(flow["protos.tls.client_tls_supported_versions"] ~= nil) then print('\n') end if((flow["tcp.max_thpt.cli2srv"] ~= nil) and (flow["tcp.max_thpt.cli2srv"] > 0)) then print("\n") end if((flow["cli2srv.trend"] ~= nil) and false) then print("\n") end local flags = flow["cli2srv.tcp_flags"] or flow["srv2cli.tcp_flags"] if((flags ~= nil) and (flags > 0)) then print("\n") print("\n") end -- ###################################### local icmp = flow["icmp"] if(icmp ~= nil) then local icmp_utils = require "icmp_utils" local icmp_label = icmp_utils.get_icmp_label(flow["icmp"]["type"], flow["icmp"]["code"]) print("") end -- ###################################### if(isScoreEnabled() and (flow.score.flow_score > 0)) then print("\n\n") local score_category_network = flow.score.host_categories_total["0"] local score_category_security = flow.score.host_categories_total["1"] local tot = score_category_network + score_category_security score_category_network = (score_category_network*100)/tot score_category_security = 100 - score_category_network print('\n') print("\n") end -- ###################################### local alerts_by_score = {} -- Table used to keep messages ordered by score local num_statuses = 0 local first = true for id, _ in pairs(flow["alerts_map"] or {}) do local is_predominant = id == flow["predominant_alert"] local alert_label = alert_consts.alertTypeLabel(id, true, alert_entities.flow.entity_id) local message = alert_label local alert_score = ntop.getFlowAlertScore(id) local alert_risk = ntop.getFlowAlertRisk(id) if alert_score > 0 then message = message .. string.format(" [%s: %s]", i18n("score"), format_utils.formatValue(alert_score)) end if not alerts_by_score[alert_score] then alerts_by_score[alert_score] = {} end alerts_by_score[alert_score][#alerts_by_score[alert_score] + 1] = {message = message, is_predominant = is_predominant, alert_id = id, alert_label = alert_label, alert_risk = alert_risk} num_statuses = num_statuses + 1 end -- ###################################### -- Unhandled flow risk as 'fake' alerts with a 'fake' score of zero if flow["unhandled_flow_risk"] and table.len(flow["unhandled_flow_risk"]) > 0 then local unhandled_risk_score = 0 local risk = flow["unhandled_flow_risk"] for risk_str,risk_id in pairs(risk) do if not alerts_by_score[unhandled_risk_score] then alerts_by_score[unhandled_risk_score] = {} end local message = string.format("%s [%s: %s]", risk_str, i18n("score"), i18n("score_not_accounted")) alerts_by_score[unhandled_risk_score][#alerts_by_score[unhandled_risk_score] + 1] = {message = message, is_predominant = false, alert_risk = risk_id} num_statuses = num_statuses + 1 end end -- ###################################### -- Print flow alerts (ordered by score and then alphabetically) if num_statuses > 0 then -- Prepare a mapping between alert id and check local alert_id_to_flow_check = {} local checks = require "checks" local riskInfo = {} local flow_checks = checks.load(ifId, checks.script_types.flow, "flow") for flow_check_name, flow_check in pairs(flow_checks.modules) do if flow_check.alert_id then alert_id_to_flow_check[flow_check.alert_id] = flow_check_name end end if not isEmptyString(flow.riskInfo) then riskInfo = json.decode(flow.riskInfo, 1, nil) end if (riskInfo ~= nil) then for _, score_alerts in pairsByKeys(alerts_by_score, rev) do for _, score_alert in pairsByField(score_alerts, "message", asc) do if first then print("") first = false end local status_icon = "" local riskLabel = riskInfo[tostring(score_alert.alert_risk)] if(riskLabel ~= nil) then riskLabel = "[" .. shortenString(riskLabel,64) .. "]" else riskLabel = "" end if score_alert.alert_id then alert_consts.alertTypeIcon(score_alert.alert_id, map_score_to_severity(score_alert.alert_id), 'fa-lg') end print(string.format('')) local msg = string.format('', score_alert.message, riskLabel, (score_alert.alert_risk > 0 and flow_risk_utils.get_documentation_link(score_alert.alert_risk)) or '', status_icon or '') print(msg) if score_alert.alert_id then print('') else -- These are unhandled alerts, e.g., flow risks for which a check doesn't exist print(string.format('')) end print('') end end end end -- ###################################### if(flow.entropy and flow.entropy.client and flow.entropy.server) then print("") print("") print("") print("\n") end if((flow.community_id ~= nil) and (flow.community_id ~= "")) then print("\n") end if((flow["protos.http.last_url"] ~= nil) and (flow.l7_error_code ~= 0)) then print("") print("") print("\n") end if((flow.client_process == nil) and (flow.server_process == nil)) then print("\n") end if((flow.client_process ~= nil) or (flow.server_process ~= nil)) then local epbf_utils = require "ebpf_utils" print('\n') if(flow.client_process ~= nil) then displayProc(flow.client_process, "\n") end if(flow.client_container ~= nil) then displayContainer(flow.client_container, "\n") end if(flow.server_process ~= nil) then displayProc(flow.server_process, "\n") end if(flow.server_container ~= nil) then displayContainer(flow.server_container, "\n") end end if(flow["protos.dns.last_query"] ~= nil) then local dns_utils = require "dns_utils" print("\n") end if not isEmptyString(flow["protos.ssh.hassh.client_hash"]) or not isEmptyString(flow["protos.ssh.hassh.server_hash"]) then print("") print("") print("") end if(not isEmptyString(flow["protos.ssh.client_signature"])) then print("\n") end if(not isEmptyString(flow["bittorrent_hash"])) then print("\n") end if(flow["protos.http.last_url"] ~= nil) then local rowspan = 2 if(not isEmptyString(flow["protos.http.last_method"])) then rowspan = rowspan + 1 end if not have_nedge and flow["protos.http.last_return_code"] and flow["protos.http.last_return_code"] ~= 0 then rowspan = rowspan + 1 end if(not isEmptyString(flow["protos.http.last_user_agent"])) then rowspan = rowspan + 1 end if(not isEmptyString(flow["protos.http.last_return_code"])) then rowspan = rowspan + 1 end print("") if(not isEmptyString(flow["protos.http.last_method"])) then print("") print("") print("") end -- Adding server name column print("\n") if(not isEmptyString(flow["protos.http.last_user_agent"])) then print("") end print("\n") if not have_nedge and flow["protos.http.last_return_code"] and flow["protos.http.last_return_code"] ~= 0 then if(flow["protos.http.last_return_code"] < 400) then color = "badge bg-success" else color = "badge bg-warning" end print("\n") end else if((flow["host_server_name"] ~= nil) and (flow["protos.dns.last_query"] == nil)) then print("\n") end end if(flow["profile"] ~= nil) then print("\n") end if(flow.src_as and flow.src_as ~= 0) or (flow.dst_as and flow.dst_as ~= 0) then local asn print("") print("") formatASN(flow.src_as) formatASN(flow.dst_as) print("\n") end if(flow.prev_adjacent_as or flow.next_adjacent_as) then print("") print("") formatASN(flow.prev_adjacent_as) formatASN(flow.next_adjacent_as) print("\n") end if (not interface.isPacketInterface()) and (flow["flow_verdict"]) and (tonumber(flow["flow_verdict"]) ~= 0) then local flow_verdict_badge = addFlowVerdictBadge(flow["flow_verdict"], true) print("\n") end if (flow["moreinfo.json"] ~= nil) then local flow_field_value_maps = require "flow_field_value_maps" local info, pos, err = json.decode(flow["moreinfo.json"], 1, nil) local isThereSIP = 0 local isThereRTP = 0 -- Convert the array to symbolic identifiers if necessary local syminfo = {} for key, value in pairs(info) do key, value = flow_field_value_maps.map_field_value(ifid, key, value) local k = rtemplate[tonumber(key)] if(k == nil) then k = flow_consts.flow_fields_description[tostring(key)] end if(k ~= nil) then syminfo[k] = value else local nprobe_description = interface.getZMQFlowFieldDescr(key) if not isEmptyString(nprobe_description) and nprobe_description ~= key then syminfo[nprobe_description] = value else syminfo[key] = value end end end info = syminfo -- get SIP rows if(ntop.isPro() and (flow["proto.ndpi"] == "SIP")) then local sip_table_rows = getSIPTableRows(info) print(sip_table_rows) isThereSIP = isThereProtocol("SIP", info) if(isThereSIP == 1) then isThereSIP = isThereSIPCall(info) end end info = removeProtocolFields("SIP",info) -- get RTP rows if(ntop.isPro() and (flow["proto.ndpi"] == "RTP")) then local rtp_table_rows = getRTPTableRows(info) print(rtp_table_rows) -- io.write(flow["proto.ndpi"].."\n") isThereRTP = isThereProtocol("RTP", info) end info = removeProtocolFields("RTP",info) local snmpdevice = nil if(ntop.isPro() and not isEmptyString(flow["device_ip"])) then snmpdevice = flow["device_ip"] end if((flow["observation_point_id"] ~= nil) and (flow["observation_point_id"] ~= 0)) then local custom_name = getObsPointAlias(flow["observation_point_id"], true, true) print("") print("") end if(snmpdevice ~= nil) then print("") print("") if(flow["in_index"] or flow["out_index"]) then if((flow["in_index"] == flow["out_index"]) and (flow["in_index"] == 0)) then -- nothing to do (they are likely to be not initialized) else printFlowSNMPInfo(snmpdevice, flow["in_index"], flow["out_index"]) end end end local num = 0 for key,value in pairsByKeys(info) do if(num == 0) then print("\n") end if(value ~= "") then print("\n") end num = num + 1 end end if(flow.flow_payload ~= nil) then local idx -- Check for HTTP (remove data past response) payload = string.reverse(flow.flow_payload) idx = string.find(payload, "\n\r\n\r") if(idx == nil) then payload = flow.flow_payload else payload = string.reverse(string.sub(payload, idx)) end print("\n") end print("
") print(i18n("details.vlan_id")) print("" .. getFullVlanName(flow["vlan"]) .. "
"..i18n("flow_details.flow_peers_client_server")..""..getFlowLabel(flow, true, not ifstats.isViewed --[[ don't add hyperlinks, viewed interface don't have hosts --]], nil, nil, false --[[ add flags ]]).."
"..i18n("protocol").." / "..i18n("application").."") else print("") end if(flow["verdict.pass"] == false) then print("") end print(flow["proto.l4"].." / ") if(flow["proto.ndpi_id"] == -1) then print(flow["proto.ndpi"]) else print("") print(getApplicationLabel(flow["proto.ndpi"],32).." ") print("(") print(getCategoryLabel(flow["proto.ndpi_cat"], interface.getnDPICategoryId(flow["proto.ndpi_cat"])).."") if(flow["proto.ndpi_cat_file"]) then print(" @ " ..flow["proto.ndpi_cat_file"]) end print(") ".. formatBreed(flow["proto.ndpi_breed"], flow["proto.is_encrypted"])) if(flow["proto.ndpi_address_family"] ~= nil) then print(" [".. i18n("network") ..": "..flow["proto.ndpi_address_family"].."]") end if (flow.confidence) and (not isEmptyString(flow.confidence)) then print(" ["..i18n("ndpi_confidence")..": "..format_confidence_badge(flow.confidence).."]") end end if(flow["verdict.pass"] == false) then print("") end historicalProtoHostHref(ifid, flow["cli.ip"], flow["proto.l4"], flow["proto.ndpi_id"], page_utils.safe_html(flow["protos.tls.certificate"] or '')) if((flow["protos.tls_version"] ~= nil) and (flow["protos.tls_version"] ~= 0)) then local tls_version_name = ntop.getTLSVersionName(flow["protos.tls_version"]) if isEmptyString(tls_version_name) then print(" [ TLS"..flow["protos.tls_version"].." ]") else print(" [ "..tls_version_name.." ]") end if(tonumber(flow["protos.tls_version"]) < 771) then print(' ') print(i18n("flow_details.tls_old_protocol_version")) end end if(ifstats.inline) then if(flow["verdict.pass"]) then print('
') print('') print('') print('\n') print('
') end end print('
VRF Id "..flow.vrfId.."
"..i18n("flow_details.flow_shapers")..""..c.."".. shaper.icon .. " " .. shaper.text .."
"..s.."".. shaper.icon .. " " .. shaper.text.."
"..i18n("flow_details.flow_quota")..""..c.."") printFlowQuota(ifstats.id, flow, true --[[ client ]]) print("
"..s.."") printFlowQuota(ifstats.id, flow, false --[[ server ]]) print("
"..i18n("flow_details.flow_marker").."".. nf_config.formatMarker(flow["marker"]) .."
"..i18n("device_protocols.device_protocol_policy")..""..i18n("device_protocols.devtype_as_proto_client", {devtype=discover.devtype2string(flow["cli.devtype"]), proto=interface.getnDPIProtoName(forbidden_proto)}).."") print(i18n(ternary(forbidden_peer ~= "cli", "allowed", "forbidden"))) print("
"..i18n("device_protocols.devtype_as_proto_server", {devtype=discover.devtype2string(flow["srv.devtype"]), proto=interface.getnDPIProtoName(forbidden_proto)}).."") print(i18n(ternary(forbidden_peer ~= "srv", "allowed", "forbidden"))) print("
"..i18n("details.first_last_seen").."
" .. formatEpoch(flow["seen.first"]) .. " [" .. secondsToTime(os.time()-flow["seen.first"]) .. " "..i18n("details.ago").."]" .. "
" .. formatEpoch(flow["seen.last"]) .. " [" .. secondsToTime(os.time()-flow["seen.last"]) .. " "..i18n("details.ago").."]" .. "
"..i18n("details.total_traffic")..""..i18n("total")..": " .. bytesToSize(flow["bytes"]) .. " "..i18n("details.goodput").." : " .. bytesToSize(flow["goodput_bytes"]) .. " (") pctg = round(((flow["goodput_bytes"]*100)/flow["bytes"]), 2) if(pctg < 50) then pctg = ""..pctg.."" elseif(pctg < 60) then pctg = ""..pctg.."" end print(pctg.."") print(" %)
 
" .. i18n("client") .. " " .. i18n("server") .. ": " .. formatPackets(flow["cli2srv.packets"]) .. " / ".. bytesToSize(flow["cli2srv.bytes"]) .. " " .. i18n("client") .. " " .. i18n("server") .. ": " .. formatPackets(flow["srv2cli.packets"]) .. " / ".. bytesToSize(flow["srv2cli.bytes"]) .. "
") cli2srv = round((flow["cli2srv.bytes"] * 100) / flow["bytes"], 0) local cli_name = shortHostName(flowinfo2hostname(flow, "cli")) local srv_name = shortHostName(flowinfo2hostname(flow, "srv")) if(flow["cli.port"] > 0) then cli_name = cli_name .. ":" .. flow["cli.port"] srv_name = srv_name .. ":" .. flow["srv.port"] end print('
'.. cli_name..'
' .. srv_name .. '
') print("
IEC 60870-5-104 "..i18n("flow_details.iec104_mask").."") total = 0 for k,v in pairsByKeys(flow.iec104.typeid, rev) do total = total + v end print("") for k,v in pairsByValues(flow.iec104.typeid, rev) do local pctg = (v*100)/total local key = iec104_typeids2str(tonumber(k)) print(string.format("\n", key, pctg)) end print("
%s%.3f %%
\n") print("
".. i18n("flow_details.iec104_transitions")) drawiecgraph(flow.iec104.typeid_transitions, total) print("") print("") for k,v in pairsByValues(flow.iec104.typeid_transitions, rev) do local pctg = (v*100)/total local keys = split(k, ",") local key = iec104_typeids2str(tonumber(keys[1])) if(keys[1] == keys[2]) then key = key ..' ' else key = key ..' ' end key = key .. iec104_typeids2str(tonumber(keys[2])) print(string.format("\n", key, pctg)) end print("
%s%.3f %%
\n") print("
"..i18n("flow_details.iec104_latency").."") if(flow.iec104.ack_time.stddev > flow.iec104.ack_time.average) then on = "" off = "" else on = "" off = "" end if((flow.iec104.ack_time.average > 1000) or (flow.iec104.ack_time.stddev > 1000)) then print(string.format("%.3f sec (%s%.3f sec%s)", flow.iec104.ack_time.average/1000, on, (flow.iec104.ack_time.stddev/1000), off)) else print(string.format("%.3f ms (%s%.3f msec%s)", flow.iec104.ack_time.average, on, flow.iec104.ack_time.stddev, off)) end print("
"..i18n("flow_details.iec104_msg_breakdown").."") local total = flow.iec104.stats.forward_msgs + flow.iec104.stats.reverse_msgs local pctg = string.format("%.1f", (flow.iec104.stats.forward_msgs * 100) / total) if(flow["srv.port"] == 2404) then -- we need to swap directions pctg = 100-pctg end print('
'..pctg..'%
') pctg = 100-pctg print('
'..pctg..'%
') -- print(formatValue(flow.iec104.stats.forward_msgs).." RX / "..formatValue(flow.iec104.stats.reverse_msgs).." TX") print("
"..i18n("flow_details.iec104_msg_loss").."") print(" "..colorNotZero(flow.iec104.pkt_lost.rx)..", "..colorNotZero(flow.iec104.pkt_lost.tx).." / ") if(flow.iec104.stats.retransmitted_msgs == 0) then print("0") else print(colorNotZero(flow.iec104.stats.retransmitted_msgs)) end print(" Retransmitted") print("
"..i18n("download").." JSON
"..i18n("flow_details.tos")..""..(dscp_consts.dscp_descr(flow.tos.client.DSCP)) .." / ".. (dscp_consts.ecn_descr(flow.tos.client.ECN)) ..""..(dscp_consts.dscp_descr(flow.tos.server.DSCP)) .." / ".. (dscp_consts.ecn_descr(flow.tos.server.ECN)) .."
"..i18n("flow_details.rtt_breakdown").."") print('
'.. cli2srv ..' ms (client)
') print('
' .. srv2cli .. ' ms (server)
') print("
"..i18n("flow_details.rtt_distance").." ") local c_vacuum_km_s = 299792 local c_vacuum_mi_s = 186000 local fiber_vf = .67 local delta_t = rtt/1000 local dd_fiber_km = delta_t * c_vacuum_km_s * fiber_vf local dd_fiber_mi = delta_t * c_vacuum_mi_s * fiber_vf print(formatValue(toint(dd_fiber_km)).." Km"..formatValue(toint(dd_fiber_mi)).." Miles") print("
"..i18n("flow_details.application_latency")..""..msToTime(flow["tcp.appl_latency"]).."
"..i18n("flow_details.packet_inter_arrival_time")..""..i18n("client").." "..i18n("server")..": ") print(msToTime(flow["interarrival.cli2srv"]["min"]).." / "..msToTime(flow["interarrival.cli2srv"]["avg"]).." / "..msToTime(flow["interarrival.cli2srv"]["max"])) print(" ") else print(""..i18n("client").." "..i18n("server")..": ") print(msToTime(flow["interarrival.srv2cli"]["min"]).." / "..msToTime(flow["interarrival.srv2cli"]["avg"]).." / "..msToTime(flow["interarrival.srv2cli"]["max"])) end print("
"..i18n("flow_details.looks_like_idle_flow_message").."
"..i18n("flow_details.ip_packet_analysis").." "..i18n("client").." "..i18n("server").." / "..i18n("client").." "..i18n("server").."
"..i18n("details.fragments").."".. formatPackets(flow["cli2srv.fragments"]) .." / ".. formatPackets(flow["srv2cli.fragments"]) .."
"..i18n("flow_details.tcp_packet_analysis")..""..i18n("client").." "..i18n("server").." / "..i18n("client").." "..i18n("server").."
"..i18n("details.retransmissions").."".. formatPackets(flow["cli2srv.retransmissions"]) .." / ".. formatPackets(flow["srv2cli.retransmissions"]) .."
"..i18n("details.out_of_order").."".. formatPackets(flow["cli2srv.out_of_order"]) .." / ".. formatPackets(flow["srv2cli.out_of_order"]) .."
"..i18n("details.lost").."".. formatPackets(flow["cli2srv.lost"]) .." / ".. formatPackets(flow["srv2cli.lost"]) .."
"..i18n("details.keep_alive").."".. formatPackets(flow["cli2srv.keep_alive"]) .." / ".. formatPackets(flow["srv2cli.keep_alive"]) .."
"..i18n("flow_details.tls_certificate").."") print(i18n("flow_details.client_requested")..":
") -- TLS, so use https print(format_external_link(page_utils.safe_html(flow["protos.tls.client_requested_server_name"]), page_utils.safe_html(flow["protos.tls.client_requested_server_name"]), false, "https")) if(flow["category"] ~= nil) then print(" "..getCategoryIcon(flow["protos.tls.client_requested_server_name"], flow["category"])) end historicalProtoHostHref(ifid, flow["cli.ip"], nil, flow["proto.ndpi_id"], page_utils.safe_html(flow["protos.tls.client_requested_server_name"] or ''), flow["vlan"]) printAddCustomHostRule(flow["protos.tls.client_requested_server_name"]) print("
") if(flow["protos.tls.server_names"] ~= nil) then local servers = string.split(flow["protos.tls.server_names"], ",") or {flow["protos.tls.server_names"]} print(i18n("flow_details.tls_server_names")..":
") for i, server in ipairs(servers) do if i > 1 then print("
") end if starts(server, '*') then print(server) else -- TLS, so use https print(format_external_link(server, server, false, "https")) end end end print("
'..i18n("flow_details.tls_certificate_validity").."") if((flow["protos.tls.notBefore"] > now) or (flow["protos.tls.notAfter"] < now)) then print(" ") end print(formatEpoch(flow["protos.tls.notBefore"])) print(" - ") print(formatEpoch(flow["protos.tls.notAfter"])) print("
TLS issuerDN'..flow["protos.tls.issuerDN"]..'
TLS subjectDN'..flow["protos.tls.subjectDN"]..'
JA3C / JA3S') if(flow["protos.tls.ja3.client_malicious"]) then print(' ') end ja3url(flow["protos.tls.ja3.client_hash"], nil, 'ja3c') print("") if(flow["protos.tls.ja3.server_malicious"]) then print(' ') end ja3url(flow["protos.tls.ja3.server_hash"], flow["protos.tls.ja3.server_unsafe_cipher"], 'ja3s') --print(tls_consts.cipher2str(flow["protos.tls.ja3.server_cipher"])) print("
TLS ALPN'..page_utils.safe_html(flow["protos.tls.client_alpn"])..'
'.. i18n("flow_details.client_tls_supported_versions") ..''..page_utils.safe_html(flow["protos.tls.client_tls_supported_versions"])..'
".. ''.. i18n("flow_details.max_estimated_tcp_throughput").." "..i18n("client").." "..i18n("server")..": ") print(bitsToSize(flow["tcp.max_thpt.cli2srv"])) print(" "..i18n("client").." "..i18n("server")..": ") print(bitsToSize(flow["tcp.max_thpt.srv2cli"])) print("
"..i18n("flow_details.throughput_trend")..""..flow["cli.ip"].." "..flow["srv.ip"]..": ") print(flow["cli2srv.trend"]) print(""..flow["cli.ip"].." "..flow["srv.ip"]..": ") print(flow["srv2cli.trend"]) print("
"..i18n("tcp_flags")..""..i18n("client").." "..i18n("server")..": ") printTCPFlags(flow["cli2srv.tcp_flags"]) print(""..i18n("client").." "..i18n("server")..": ") printTCPFlags(flow["srv2cli.tcp_flags"]) print("
") local flow_msg = "" if flow["tcp_reset"] then local resetter = "" if(hasbit(flow["cli2srv.tcp_flags"],0x04)) then resetter = "client" else resetter = "server" end flow_msg = flow_msg..i18n("flow_details.flow_reset_by_resetter_msg",{resetter = resetter}) elseif flow["tcp_closed"] then flow_msg = flow_msg..i18n("flow_details.flow_completed_msg") elseif flow["tcp_connecting"] then flow_msg = flow_msg..i18n("flow_details.flow_connecting_msg") elseif flow["tcp_established"] then flow_msg = flow_msg..i18n("flow_details.flow_active_msg") else flow_msg = flow_msg.." "..i18n("flow_details.flow_peer_roles_inaccurate_msg") end print(flow_msg) print("
"..i18n("flow_details.icmp_info")..""..icmp_label) if icmp["unreach"] then local unreachable_flow = interface.findFlowByTuple(flow["cli.ip"], flow["srv.ip"], flow["vlan"], icmp["unreach"]["dst_port"], icmp["unreach"]["src_port"], icmp["unreach"]["protocol"]) print("
"..i18n("flow")..": ") if unreachable_flow then print(" ") print(" "..getFlowLabel(unreachable_flow, true, true)) else -- The flow hasn't been found so very likely it is no longer active or it hasn't been seen. -- Still print the flow using data found in the original datagram found in the icmp packet print(getFlowLabel({ ["cli.ip"] = icmp["unreach"]["src_ip"], ["srv.ip"] = icmp["unreach"]["dst_ip"], ["cli.port"] = icmp["unreach"]["src_port"], ["srv.port"] = icmp["unreach"]["dst_port"]}, false, false)) end print("") end print("
"..i18n("flow_details.flow_score").. " / "..i18n("flow_details.flow_score_breakdown")..""..format_utils.formatValue(flow.score.flow_score).."
'.. i18n("flow_details.score_category_network")) print('
' .. i18n("flow_details.score_category_security") .. '
"..i18n("flow_details.flow_issues")..""..i18n("description")..""..i18n("actions").."
%s %s %s %s') -- Add rules to disable the check print(string.format('', score_alert.alert_id, score_alert.alert_label)) -- If available, add a cog to configure the check if alert_id_to_flow_check[score_alert.alert_id] then print(string.format(' ', alert_utils.getConfigsetURL(alert_id_to_flow_check[score_alert.alert_id], "flow"))) end -- Add an anchor to the historical alert if not ifstats.isViewed -- and score_alert.is_predominant then -- Prepare bounds for the historical alert search. local epoch_begin = flow["seen.first"] -- As this is the page of active flows, it is meaningful to use the current time for the epoch end. -- This will also enable multiple flows with the same tuple to be shown. local epoch_end = os.time() local l7_proto = flow["proto.ndpi_id"] .. tag_utils.SEPARATOR .. "eq" local cli_ip = flow["cli.ip"] .. tag_utils.SEPARATOR .. "eq" local srv_ip = flow["srv.ip"] .. tag_utils.SEPARATOR .. "eq" local cli_port = flow["cli.port"] .. tag_utils.SEPARATOR .. "eq" local srv_port = flow["srv.port"] .. tag_utils.SEPARATOR .. "eq" print(string.format(' ', ntop.getHttpPrefix(), epoch_begin, epoch_end, l7_proto, cli_ip, cli_port, srv_ip, srv_port)) end print('
"..i18n("flow_details.entropy").." "..i18n("client").." "..i18n("server")..": ".. string.format("%.3f", flow.entropy.client) .. ""..i18n("client").." "..i18n("server")..": ".. string.format("%.3f", flow.entropy.server) .. "
CommunityId ".. flow.community_id) print_copy_button('community_id', flow.community_id) print("
"..i18n("l7_error_code").."= 400)) then print("bg-danger") else print("bg-success") end print("\">".. http_utils.getResponseStatusCode(flow.l7_error_code) .. "
"..i18n("flow_details.actual_peak_throughput").."") if (throughput_type == "bps") then print("" .. bitsToSize(8*flow["throughput_bps"]) .. " ") elseif (throughput_type == "pps") then print("" .. pktsToSize(flow["throughput_bps"]) .. " ") end if (throughput_type == "bps") then print(" / " .. bitsToSize(8*flow["top_throughput_bps"]) .. " ") elseif (throughput_type == "pps") then print(" / " .. pktsToSize(flow["top_throughput_bps"]) .. " ") end print("0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") print("
') local width = 1024 local height = 200 local url = ntop.getHttpPrefix().."/lua/get_flow_process_tree.lua?flow_key="..flow_key.."&flow_hash_id="..flow_hash_id epbf_utils.draw_flow_processes_graph(width, height, url) print('
"..i18n("flow_details.client_process_information").."
"..i18n("flow_details.client_container_information").."
"..i18n("flow_details.server_process_information").."
"..i18n("flow_details.server_container_information").."
"..i18n("flow_details.dns_query").."") local dns_info = format_dns_query_info({ last_query_type = flow["protos.dns.last_query_type"], last_return_code = flow["protos.dns.last_return_code"]}) if dns_info["last_query_type"] ~= 0 then print(dns_info["last_query_type"] .. " ") end if dns_info["last_return_code"] ~= 0 then print(dns_info["last_return_code"] .. " ") end if(string.ends(flow["protos.dns.last_query"], "arpa")) then print(shortHostName(flow["protos.dns.last_query"])) else print(format_external_link(page_utils.safe_html(flow["protos.dns.last_query"]), page_utils.safe_html(shortHostName(flow["protos.dns.last_query"])), false, "dns")) end historicalProtoHostHref(ifid, flow["cli.ip"], flow["proto.l4"], flow["proto.ndpi_id"], page_utils.safe_html(flow["protos.dns.last_query"] or '')) if(flow["category"] ~= nil) then print(" "..getCategoryIcon(flow["protos.dns.last_query"], flow["category"])) end printAddCustomHostRule(flow["protos.dns.last_query"]) print("
HASSH") print(""..i18n("client")..": "..hostinfo2detailshref(flow2hostinfo(flow, "cli"), {page = "ssh"}, flow["protos.ssh.hassh.client_hash"])..""..i18n("server")..": "..hostinfo2detailshref(flow2hostinfo(flow, "srv"), {page = "ssh"}, flow["protos.ssh.hassh.server_hash"]).."
"..i18n("flow_details.ssh_signature")..""..i18n("client")..": "..(flow["protos.ssh.client_signature"] or '')..""..i18n("server")..": "..(flow["protos.ssh.server_signature"] or '').."
"..i18n("flow_details.bittorrent_hash").."".. flow["bittorrent_hash"].."
"..i18n("http")..""..i18n("flow_details.http_method")..""..(flow["protos.http.last_method"] or '').."
"..i18n("flow_details.server_name").."") local s = flowinfo2hostname(flow,"srv") if(not isEmptyString(flow["host_server_name"])) then s = flow["host_server_name"] end print(format_external_link(page_utils.safe_html(s), page_utils.safe_html(s), false, "http")) if(flow["category"] ~= nil) then print(" "..getCategoryIcon(flow["host_server_name"], flow["category"])) end -- Adding + with custom host rules next to the server name printAddCustomHostRule(s) print("
"..i18n("flow_details.user_agent")..""..flow["protos.http.last_user_agent"].."
"..i18n("flow_details.url").."") -- if(flow["srv.port"] ~= 80) then print(":"..flow["srv.port"]) end local last_url = page_utils.safe_html(flow["protos.http.last_url"]) local last_url_short = shortenString(last_url, 64) local proto = "https" if(starts(last_url, "http:") == false) then -- Now we need to check if nttp or https is needed if(not string.contains(last_url, ":443")) then proto = "http" end end print(format_external_link(last_url, last_url_short, false, proto)) print("
"..i18n("flow_details.response_code")..""..(http_utils.getResponseStatusCode(flow["protos.http.last_return_code"]) or '').."
"..i18n("flow_details.server_name").."") local proto = "http" if(starts(flow["proto.ndpi"], "TLS")) then proto = "https" end print(format_external_link(page_utils.safe_html(flow["host_server_name"]), page_utils.safe_html(flow["host_server_name"]), false, proto)) if not isEmptyString(flow["protos.http.server_name"]) then printAddCustomHostRule(flow["protos.http.server_name"]) end print("
"..i18n("flow_details.profile_name")..""..flow["profile"].."
"..i18n("flow_details.as_src_dst").."
"..i18n("flow_details.as_prev_next").."
" .. i18n("details.flow_verdict") .. "" .. flow_verdict_badge .. "
"..i18n("details.observation_point_id")..""..custom_name.."
"..i18n("details.flow_exporter")..""..snmpdevice.."
"..i18n("flow_details.additional_flow_elements").."
" .. getFlowKey(key) .. "" .. handleCustomFlowField(key, value, snmpdevice) .. "
Payload" .. payload .. "
\n") end local traffic_peity_width = "64" print [[
]] local cliLabel = '' local cliValue = '' local srvLabel = '' local srvValue = '' local vlan = 0 local infoDomain = '' local infoIssuerDN = '' if flow ~= nil then cliLabel = flowinfo2hostname(flow, "cli"); if cliLabel ~= flow["cli.ip"] then cliValue = flow["cli.ip"] else cliValue = cliLabel end srvLabel = flowinfo2hostname(flow,"srv"); if srvLabel ~= flow["srv.ip"] then srvValue = flow["srv.ip"] else srvValue = srvLabel end if flow["vlan"] and flow["vlan"] > 0 then vlan = flow["vlan"] end if not isEmptyString(flow["suspicious_dga_domain"]) then infoDomain = flow["suspicious_dga_domain"] elseif not isEmptyString(flow["protos.tls.client_requested_server_name"]) then infoDomain = flow["protos.tls.client_requested_server_name"] elseif not isEmptyString(flow["host_server_name"]) and hostnameIsDomain(flow["host_server_name"]) then infoDomain = flow["host_server_name"] end if flow["protos.tls.issuerDN"] ~= nil then infoIssuerDN = flow["protos.tls.issuerDN"] end end print [[ ]] dofile(dirs.installdir .. "/scripts/lua/inc/footer.lua")