view mod_incidents_handling/incidents_handling/incidents_handling.lib.lua @ 5954:e5b5a74feb91

mod_rest: Workaround lack of number coercion in util.datamapper for HTTP upload util.datamapper will not coerce a string into an integer like the XEP-0363 'size' slot, which becomes a problem when using the HTTP GET method, passing fields as ?query parameters which are always strings.
author Kim Alvefur <zash@zash.se>
date Mon, 19 Aug 2024 20:08:41 +0200
parents 7dbde05b48a9
children
line wrap: on
line source

-- This contains the auxiliary functions for the Incidents Handling module.
-- (C) 2012-2013, Marco Cirillo (LW.Org)

local pairs, ipairs, os_date, string, table, tonumber = pairs, ipairs, os.date, string, table, tonumber

local dataforms_new = require "util.dataforms".new
local st = require "util.stanza"

local xmlns_inc = "urn:xmpp:incident:2"
local xmlns_iodef = "urn:ietf:params:xml:ns:iodef-1.0"
local my_host = nil

-- // Util and Functions //

local function ft_str()
	local d = os_date("%FT%T%z"):gsub("^(.*)(%+%d+)", function(dt, z)
		if z == "+0000" then return dt.."Z" else return dt..z end
	end)
	return d
end

local function get_incident_layout(i_type)
	local layout = {
		title = (i_type == "report" and "Incident report form") or (i_type == "request" and "Request for assistance with incident form"),
		instructions = "Started/Ended Time, Contacts, Sources and Targets of the attack are mandatory. See RFC 5070 for further format instructions.",
		{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" },

		{ name = "name", type = "hidden", value = my_host },
		{ name = "entity", type ="text-single", label = "Remote entity to query" },
		{ name = "started", type = "text-single", label = "Incident Start Time" },
		{ name = "ended", type = "text-single", label = "Incident Ended Time" },
		{ name = "reported", type = "hidden", value = ft_str() },
		{ name = "description", type = "text-single", label = "Description",
		  desc = "Description syntax is: <lang (in xml:lang format)> <short description>" },
		{ name = "contacts", type = "text-multi", label = "Contacts",
		  desc = "Contacts entries format is: <address> <type> <role> - separated by new lines" },
		{ name = "related", type = "text-multi", label = "Related Incidents",
		  desc = "Related incidents entries format is: <CSIRT's FQDN> <Incident ID> - separated by new lines" },
		{ name = "impact", type = "text-single", label = "Impact Assessment",
		  desc = "Impact assessment format is: <severity> <completion> <type>" },
		{ name = "sources", type = "text-multi", label = "Attack Sources",
		  desc = "Attack sources format is: <address> <category> <count> <count-type>" },
		{ name = "targets", type = "text-multi", label = "Attack Targets",
		  desc = "Attack target format is: <address> <category> <noderole>" }
	}

	if i_type == "request" then
		table.insert(layout, {
			name = "expectation",
			type = "list-single",
			label = "Expected action from remote entity",
			value = {
				{ value = "nothing", label = "No action" },
				{ value = "contact-sender", label = "Contact us, regarding the incident" },
				{ value = "investigate", label = "Investigate the entities listed into the incident" },
				{ value = "block-host", label = "Block the involved accounts" },
				{ value = "other", label = "Other action, filling the description field is required" }
			}})
		table.insert(layout, { name = "description", type = "text-single", label = "Description" })
	end

	return dataforms_new(layout)
end

local function render_list(incidents)
	local layout = {
		title = "Stored Incidents List",
		instructions = "You can select and view incident reports here, if a followup/response is possible it'll be noted in the step after selection.",
		{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" },
		{
			name = "ids",
			type = "list-single",
			label = "Stored Incidents",
			value = {}
		}
	}

	-- Render stored incidents list

	for id in pairs(incidents) do
		table.insert(layout[2].value, { value = id, label = id })
	end

	return dataforms_new(layout)
end

local function insert_fixed(t, item) table.insert(t, { type = "fixed", value = item }) end

local function render_single(incident)
	local layout = {
		title = string.format("Incident ID: %s - Friendly Name: %s", incident.data.id.text, incident.data.id.name),
		instructions = incident.data.desc.text,
		{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" }
	}

	insert_fixed(layout, "Start Time: "..incident.data.start_time)
	insert_fixed(layout, "End Time: "..incident.data.end_time)
	insert_fixed(layout, "Report Time: "..incident.data.report_time)

	insert_fixed(layout, "Contacts --")
	for _, contact in ipairs(incident.data.contacts) do
		insert_fixed(layout, string.format("Role: %s Type: %s", contact.role, contact.type))
		if contact.jid then insert_fixed(layout, "--> JID: "..contact.jid..(contact.xmlns and ", XMLNS: "..contact.xmlns or "")) end
		if contact.email then insert_fixed(layout, "--> E-Mail: "..contact.email) end
		if contact.telephone then insert_fixed(layout, "--> Telephone: "..contact.telephone) end
		if contact.postaladdr then insert_fixed(layout, "--> Postal Address: "..contact.postaladdr) end
	end

	insert_fixed(layout, "Related Activity --")
	for _, related in ipairs(incident.data.related) do
		insert_fixed(layout, string.format("Name: %s ID: %s", related.name, related.text))
	end

	insert_fixed(layout, "Assessment --")
	insert_fixed(layout, string.format("Language: %s Severity: %s Completion: %s Type: %s",
		incident.data.assessment.lang, incident.data.assessment.severity, incident.data.assessment.completion, incident.data.assessment.type))

	insert_fixed(layout, "Sources --")
	for _, source in ipairs(incident.data.event_data.sources) do
		insert_fixed(layout, string.format("Address: %s Counter: %s", source.address.text, source.counter.value))
	end

	insert_fixed(layout, "Targets --")
	for _, target in ipairs(incident.data.event_data.targets) do
		insert_fixed(layout, string.format("For NodeRole: %s", (target.noderole.cat == "ext-category" and target.noderole.ext) or targets.noderole.cat))
		for _, address in ipairs(target.addresses) do
			insert_fixed(layout, string.format("---> Address: %s Type: %s", address.text, (address.cat == "ext-category" and address.ext) or address.cat))
		end
	end

	if incident.data.expectation then
		insert_fixed(layout, "Expected Action: "..incident.data.expectation.action)
		if incident.data.expectation.desc then
			insert_fixed(layout, "Expected Action Description: "..incident.data.expectation.desc)
		end
	end

	if incident.type == "request" and incident.status == "open" then
		table.insert(layout, { name = "response-datetime", type = "hidden", value = ft_str() })
		table.insert(layout, { name = "response", type = "text-single", label = "Respond to the request" })
	end

	return dataforms_new(layout)
end

local function get_type(var, typ)
	if typ == "counter" then
		local count_type, count_ext = var, nil
		if count_type ~= "byte" or count_type ~= "packet" or count_type ~= "flow" or count_type ~= "session" or
		   count_type ~= "alert" or count_type ~= "message" or count_type ~= "event" or count_type ~= "host" or
		   count_type ~= "site" or count_type ~= "organization" then
			count_ext = count_type
			count_type = "ext-type"
		end
		return count_type, count_ext
	elseif typ == "category" then
		local cat, cat_ext = var, nil
		if cat ~= "asn" or cat ~= "atm" or cat ~= "e-mail" or cat ~= "ipv4-addr" or
		   cat ~= "ipv4-net" or cat ~= "ipv4-net-mask" or cat ~= "ipv6-addr" or cat ~= "ipv6-net" or
		   cat ~= "ipv6-net-mask" or cat ~= "mac" then
			cat_ext = cat
			cat = "ext-category"
		end
		return cat, cat_ext
	elseif type == "noderole" then
		local noderole_ext = nil
		if cat ~= "client" or cat ~= "server-internal" or cat ~= "server-public" or cat ~= "www" or
		   cat ~= "mail" or cat ~= "messaging" or cat ~= "streaming" or cat ~= "voice" or
		   cat ~= "file" or cat ~= "ftp" or cat ~= "p2p" or cat ~= "name" or
		   cat ~= "directory" or cat ~= "credential" or cat ~= "print" or cat ~= "application" or
		   cat ~= "database" or cat ~= "infra" or cat ~= "log" then
			noderole_ext = true
		end
		return noderole_ext
	end
end

local function do_tag_mapping(tag, object)
	if tag.name == "IncidentID" then
		object.id = { text = tag:get_text(), name = tag.attr.name }
	elseif tag.name == "StartTime" then
		object.start_time = tag:get_text()
	elseif tag.name == "EndTime" then
		object.end_time = tag:get_text()
	elseif tag.name == "ReportTime" then
		object.report_time = tag:get_text()
	elseif tag.name == "Description" then
		object.desc = { text = tag:get_text(), lang = tag.attr["xml:lang"] }
	elseif tag.name == "Contact" then
		local jid = tag:get_child("AdditionalData").tags[1]
		local email = tag:get_child("Email")
		local telephone = tag:get_child("Telephone")
		local postaladdr = tag:get_child("PostalAddress")
		if not object.contacts then
			object.contacts = {}
			object.contacts[1] = {
				role = tag.attr.role,
				ext_role = (tag.attr["ext-role"] and true) or nil,
				type = tag.attr.type,
				ext_type = (tag.attr["ext-type"] and true) or nil,
				xmlns = jid.attr.xmlns,
				jid = jid:get_text(),
				email = email,
				telephone = telephone,
				postaladdr = postaladdr
			}
		else
			object.contacts[#object.contacts + 1] = {
				role = tag.attr.role,
				ext_role = (tag.attr["ext-role"] and true) or nil,
				type = tag.attr.type,
				ext_type = (tag.attr["ext-type"] and true) or nil,
				xmlns = jid.attr.xmlns,
				jid = jid:get_text(),
				email = email,
				telephone = telephone,
				postaladdr = postaladdr
			}
		end
	elseif tag.name == "RelatedActivity" then
		object.related = {}
		for _, t in ipairs(tag.tags) do
			if tag.name == "IncidentID" then
				object.related[#object.related + 1] = { text = t:get_text(), name = tag.attr.name }
			end
		end
	elseif tag.name == "Assessment" then
		local impact = tag:get_child("Impact")
		object.assessment = { lang = impact.attr.lang, severity = impact.attr.severity, completion = impact.attr.completion, type = impact.attr.type }
	elseif tag.name == "EventData" then
		local source = tag:get_child("Flow").tags[1]
		local target = tag:get_child("Flow").tags[2]
		local expectation = tag:get_child("Flow").tags[3]
		object.event_data = { sources = {}, targets = {} }
		for _, t in ipairs(source.tags) do
			local addr = t:get_child("Address")
			local cntr = t:get_child("Counter")
			object.event_data.sources[#object.event_data.sources + 1] = {
				address = { cat = addr.attr.category, ext = addr.attr["ext-category"], text = addr:get_text() },
				counter = { type = cntr.attr.type, ext_type = cntr.attr["ext-type"], value = cntr:get_text() }
			}
		end
		for _, entry in ipairs(target.tags) do
			local noderole = { cat = entry:get_child("NodeRole").attr.category, ext = entry:get_child("NodeRole").attr["ext-category"] }
			local current = #object.event_data.targets + 1
			object.event_data.targets[current] = { addresses = {}, noderole = noderole }
			for _, tag in ipairs(entry.tags) do
				object.event_data.targets[current].addresses[#object.event_data.targets[current].addresses + 1] = { text = tag:get_text(), cat = tag.attr.category, ext = tag.attr["ext-category"] }
			end
		end
		if expectation then
			object.event_data.expectation = {
				action = expectation.attr.action,
				desc = expectation:get_child("Description") and expectation:get_child("Description"):get_text()
			}
		end
	elseif tag.name == "History" then
		object.history = {}
		for _, t in ipairs(tag.tags) do
			object.history[#object.history + 1] = {
				action = t.attr.action,
				date = t:get_child("DateTime"):get_text(),
				desc = t:get_chilld("Description"):get_text()
			}
		end
	end
end

local function stanza_parser(stanza)
	local object = {}

	if stanza:get_child("report", xmlns_inc) then
		local report = st.clone(stanza):get_child("report", xmlns_inc):get_child("Incident", xmlns_iodef)
		for _, tag in ipairs(report.tags) do do_tag_mapping(tag, object) end
	elseif stanza:get_child("request", xmlns_inc) then
		local request = st.clone(stanza):get_child("request", xmlns_inc):get_child("Incident", xmlns_iodef)
		for _, tag in ipairs(request.tags) do do_tag_mapping(tag, object) end
	elseif stanza:get_child("response", xmlns_inc) then
		local response = st.clone(stanza):get_child("response", xmlns_inc):get_child("Incident", xmlns_iodef)
		for _, tag in ipairs(response.tags) do do_tag_mapping(tag, object) end
	end

	return object
end

local function stanza_construct(id)
	if not id then return nil
	else
		local object = incidents[id].data
		local s_type = incidents[id].type
		local stanza = st.iq():tag(s_type or "report", { xmlns = xmlns_inc })
		stanza:tag("Incident", { xmlns = xmlns_iodef, purpose = incidents[id].purpose })
			:tag("IncidentID", { name = object.id.name }):text(object.id.text):up()
			:tag("StartTime"):text(object.start_time):up()
			:tag("EndTime"):text(object.end_time):up()
			:tag("ReportTime"):text(object.report_time):up()
			:tag("Description", { ["xml:lang"] = object.desc.lang }):text(object.desc.text):up():up();

		local incident = stanza:get_child(s_type, xmlns_inc):get_child("Incident", xmlns_iodef)

		for _, contact in ipairs(object.contacts) do
			incident:tag("Contact", { role = (contact.ext_role and "ext-role") or contact.role,
						  ["ext-role"] = (contact.ext_role and contact.role) or nil,
						  type = (contact.ext_type and "ext-type") or contact.type,
						  ["ext-type"] = (contact.ext_type and contact.type) or nil })
				:tag("Email"):text(contact.email):up()
				:tag("Telephone"):text(contact.telephone):up()
				:tag("PostalAddress"):text(contact.postaladdr):up()
				:tag("AdditionalData")
					:tag("jid", { xmlns = contact.xmlns }):text(contact.jid):up():up():up()

		end

		incident:tag("RelatedActivity"):up();

		for _, related in ipairs(object.related) do
			incident:get_child("RelatedActivity")
				:tag("IncidentID", { name = related.name }):text(related.text):up();
		end

		incident:tag("Assessment")
			:tag("Impact", {
				lang = object.assessment.lang,
				severity = object.assessment.severity,
				completion = object.assessment.completion,
				type = object.assessment.type
			}):up():up();

		incident:tag("EventData")
			:tag("Flow")
				:tag("System", { category = "source" }):up()
				:tag("System", { category = "target" }):up():up():up();

		local e_data = incident:get_child("EventData")

		local sources = e_data:get_child("Flow").tags[1]
		local targets = e_data:get_child("Flow").tags[2]

		for _, source in ipairs(object.event_data.sources) do
			sources:tag("Node")
				:tag("Address", { category = source.address.cat, ["ext-category"] = source.address.ext })
					:text(source.address.text):up()
				:tag("Counter", { type = source.counter.type, ["ext-type"] = source.counter.ext_type })
					:text(source.counter.value):up():up();
		end

		for _, target in ipairs(object.event_data.targets) do
			targets:tag("Node"):up() ; local node = targets.tags[#targets.tags]
			for _, address in ipairs(target.addresses) do
				node:tag("Address", { category = address.cat, ["ext-category"] = address.ext }):text(address.text):up();
			end
			node:tag("NodeRole", { category = target.noderole.cat, ["ext-category"] = target.noderole.ext }):up();
		end

		if object.event_data.expectation then
			e_data:tag("Expectation", { action = object.event_data.expectation.action }):up();
			if object.event_data.expectation.desc then
				local expectation = e_data:get_child("Expectation")
				expectation:tag("Description"):text(object.event_data.expectation.desc):up();
			end
		end

		if object.history then
			local history = incident:tag("History"):up();

			for _, item in ipairs(object.history) do
				history:tag("HistoryItem", { action = item.action })
					:tag("DateTime"):text(item.date):up()
					:tag("Description"):text(item.desc):up():up();
			end
		end

		-- Sanitize contact empty tags
		for _, tag in ipairs(incident) do
			if tag.name == "Contact" then
				for i, check in ipairs(tag) do
					if (check.name == "Email" or check.name == "PostalAddress" or check.name == "Telephone") and
					   not check:get_text() then
						table.remove(tag, i)
					end
				end
			end
		end

		if s_type == "request" then stanza.attr.type = "get"
		elseif s_type == "response" then stanza.attr.type = "set"
		else stanza.attr.type = "set" end

		return stanza
	end
end


_M = {} -- wraps methods into the library.
_M.ft_str = ft_str
_M.get_incident_layout = get_incident_layout
_M.render_list = render_list
_M.render_single = render_single
_M.get_type = get_type
_M.stanza_parser = stanza_parser
_M.stanza_construct = stanza_construct
_M.set_my_host = function(host) my_host = host end

return _M