|
1 -- This contains the auxiliary functions for the Incidents Handling module. |
|
2 -- (C) 2012-2013, Marco Cirillo (LW.Org) |
|
3 |
|
4 local pairs, ipairs, os_date, string, table, tonumber = pairs, ipairs, os.date, string, table, tonumber |
|
5 |
|
6 local dataforms_new = require "util.dataforms".new |
|
7 local st = require "util.stanza" |
|
8 |
|
9 local xmlns_inc = "urn:xmpp:incident:2" |
|
10 local xmlns_iodef = "urn:ietf:params:xml:ns:iodef-1.0" |
|
11 local my_host = nil |
|
12 |
|
13 -- // Util and Functions // |
|
14 |
|
15 local function ft_str() |
|
16 local d = os_date("%FT%T%z"):gsub("^(.*)(%+%d+)", function(dt, z) |
|
17 if z == "+0000" then return dt.."Z" else return dt..z end |
|
18 end) |
|
19 return d |
|
20 end |
|
21 |
|
22 local function get_incident_layout(i_type) |
|
23 local layout = { |
|
24 title = (i_type == "report" and "Incident report form") or (i_type == "request" and "Request for assistance with incident form"), |
|
25 instructions = "Started/Ended Time, Contacts, Sources and Targets of the attack are mandatory. See RFC 5070 for further format instructions.", |
|
26 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" }, |
|
27 |
|
28 { name = "name", type = "hidden", value = my_host }, |
|
29 { name = "entity", type ="text-single", label = "Remote entity to query" }, |
|
30 { name = "started", type = "text-single", label = "Incident Start Time" }, |
|
31 { name = "ended", type = "text-single", label = "Incident Ended Time" }, |
|
32 { name = "reported", type = "hidden", value = ft_str() }, |
|
33 { name = "description", type = "text-single", label = "Description", |
|
34 desc = "Description syntax is: <lang (in xml:lang format)> <short description>" }, |
|
35 { name = "contacts", type = "text-multi", label = "Contacts", |
|
36 desc = "Contacts entries format is: <address> <type> <role> - separated by new lines" }, |
|
37 { name = "related", type = "text-multi", label = "Related Incidents", |
|
38 desc = "Related incidents entries format is: <CSIRT's FQDN> <Incident ID> - separated by new lines" }, |
|
39 { name = "impact", type = "text-single", label = "Impact Assessment", |
|
40 desc = "Impact assessment format is: <severity> <completion> <type>" }, |
|
41 { name = "sources", type = "text-multi", label = "Attack Sources", |
|
42 desc = "Attack sources format is: <address> <category> <count> <count-type>" }, |
|
43 { name = "targets", type = "text-multi", label = "Attack Targets", |
|
44 desc = "Attack target format is: <address> <category> <noderole>" } |
|
45 } |
|
46 |
|
47 if i_type == "request" then |
|
48 table.insert(layout, { |
|
49 name = "expectation", |
|
50 type = "list-single", |
|
51 label = "Expected action from remote entity", |
|
52 value = { |
|
53 { value = "nothing", label = "No action" }, |
|
54 { value = "contact-sender", label = "Contact us, regarding the incident" }, |
|
55 { value = "investigate", label = "Investigate the entities listed into the incident" }, |
|
56 { value = "block-host", label = "Block the involved accounts" }, |
|
57 { value = "other", label = "Other action, filling the description field is required" } |
|
58 }}) |
|
59 table.insert(layout, { name = "description", type = "text-single", label = "Description" }) |
|
60 end |
|
61 |
|
62 return dataforms_new(layout) |
|
63 end |
|
64 |
|
65 local function render_list(incidents) |
|
66 local layout = { |
|
67 title = "Stored Incidents List", |
|
68 instructions = "You can select and view incident reports here, if a followup/response is possible it'll be noted in the step after selection.", |
|
69 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" }, |
|
70 { |
|
71 name = "ids", |
|
72 type = "list-single", |
|
73 label = "Stored Incidents", |
|
74 value = {} |
|
75 } |
|
76 } |
|
77 |
|
78 -- Render stored incidents list |
|
79 |
|
80 for id in pairs(incidents) do |
|
81 table.insert(layout[2].value, { value = id, label = id }) |
|
82 end |
|
83 |
|
84 return dataforms_new(layout) |
|
85 end |
|
86 |
|
87 local function insert_fixed(t, item) table.insert(t, { type = "fixed", value = item }) end |
|
88 |
|
89 local function render_single(incident) |
|
90 local layout = { |
|
91 title = string.format("Incident ID: %s - Friendly Name: %s", incident.data.id.text, incident.data.id.name), |
|
92 instructions = incident.data.desc.text, |
|
93 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" } |
|
94 } |
|
95 |
|
96 insert_fixed(layout, "Start Time: "..incident.data.start_time) |
|
97 insert_fixed(layout, "End Time: "..incident.data.end_time) |
|
98 insert_fixed(layout, "Report Time: "..incident.data.report_time) |
|
99 |
|
100 insert_fixed(layout, "Contacts --") |
|
101 for _, contact in ipairs(incident.data.contacts) do |
|
102 insert_fixed(layout, string.format("Role: %s Type: %s", contact.role, contact.type)) |
|
103 if contact.jid then insert_fixed(layout, "--> JID: "..contact.jid..(contact.xmlns and ", XMLNS: "..contact.xmlns or "")) end |
|
104 if contact.email then insert_fixed(layout, "--> E-Mail: "..contact.email) end |
|
105 if contact.telephone then insert_fixed(layout, "--> Telephone: "..contact.telephone) end |
|
106 if contact.postaladdr then insert_fixed(layout, "--> Postal Address: "..contact.postaladdr) end |
|
107 end |
|
108 |
|
109 insert_fixed(layout, "Related Activity --") |
|
110 for _, related in ipairs(incident.data.related) do |
|
111 insert_fixed(layout, string.format("Name: %s ID: %s", related.name, related.text)) |
|
112 end |
|
113 |
|
114 insert_fixed(layout, "Assessment --") |
|
115 insert_fixed(layout, string.format("Language: %s Severity: %s Completion: %s Type: %s", |
|
116 incident.data.assessment.lang, incident.data.assessment.severity, incident.data.assessment.completion, incident.data.assessment.type)) |
|
117 |
|
118 insert_fixed(layout, "Sources --") |
|
119 for _, source in ipairs(incident.data.event_data.sources) do |
|
120 insert_fixed(layout, string.format("Address: %s Counter: %s", source.address.text, source.counter.value)) |
|
121 end |
|
122 |
|
123 insert_fixed(layout, "Targets --") |
|
124 for _, target in ipairs(incident.data.event_data.targets) do |
|
125 insert_fixed(layout, string.format("For NodeRole: %s", (target.noderole.cat == "ext-category" and target.noderole.ext) or targets.noderole.cat)) |
|
126 for _, address in ipairs(target.addresses) do |
|
127 insert_fixed(layout, string.format("---> Address: %s Type: %s", address.text, (address.cat == "ext-category" and address.ext) or address.cat)) |
|
128 end |
|
129 end |
|
130 |
|
131 if incident.data.expectation then |
|
132 insert_fixed(layout, "Expected Action: "..incident.data.expectation.action) |
|
133 if incident.data.expectation.desc then |
|
134 insert_fixed(layout, "Expected Action Description: "..incident.data.expectation.desc) |
|
135 end |
|
136 end |
|
137 |
|
138 if incident.type == "request" and incident.status == "open" then |
|
139 table.insert(layout, { name = "response-datetime", type = "hidden", value = ft_str() }) |
|
140 table.insert(layout, { name = "response", type = "text-single", label = "Respond to the request" }) |
|
141 end |
|
142 |
|
143 return dataforms_new(layout) |
|
144 end |
|
145 |
|
146 local function get_type(var, typ) |
|
147 if typ == "counter" then |
|
148 local count_type, count_ext = var, nil |
|
149 if count_type ~= "byte" or count_type ~= "packet" or count_type ~= "flow" or count_type ~= "session" or |
|
150 count_type ~= "alert" or count_type ~= "message" or count_type ~= "event" or count_type ~= "host" or |
|
151 count_type ~= "site" or count_type ~= "organization" then |
|
152 count_ext = count_type |
|
153 count_type = "ext-type" |
|
154 end |
|
155 return count_type, count_ext |
|
156 elseif typ == "category" then |
|
157 local cat, cat_ext = var, nil |
|
158 if cat ~= "asn" or cat ~= "atm" or cat ~= "e-mail" or cat ~= "ipv4-addr" or |
|
159 cat ~= "ipv4-net" or cat ~= "ipv4-net-mask" or cat ~= "ipv6-addr" or cat ~= "ipv6-net" or |
|
160 cat ~= "ipv6-net-mask" or cat ~= "mac" then |
|
161 cat_ext = cat |
|
162 cat = "ext-category" |
|
163 end |
|
164 return cat, cat_ext |
|
165 elseif type == "noderole" then |
|
166 local noderole_ext = nil |
|
167 if cat ~= "client" or cat ~= "server-internal" or cat ~= "server-public" or cat ~= "www" or |
|
168 cat ~= "mail" or cat ~= "messaging" or cat ~= "streaming" or cat ~= "voice" or |
|
169 cat ~= "file" or cat ~= "ftp" or cat ~= "p2p" or cat ~= "name" or |
|
170 cat ~= "directory" or cat ~= "credential" or cat ~= "print" or cat ~= "application" or |
|
171 cat ~= "database" or cat ~= "infra" or cat ~= "log" then |
|
172 noderole_ext = true |
|
173 end |
|
174 return noderole_ext |
|
175 end |
|
176 end |
|
177 |
|
178 local function do_tag_mapping(tag, object) |
|
179 if tag.name == "IncidentID" then |
|
180 object.id = { text = tag:get_text(), name = tag.attr.name } |
|
181 elseif tag.name == "StartTime" then |
|
182 object.start_time = tag:get_text() |
|
183 elseif tag.name == "EndTime" then |
|
184 object.end_time = tag:get_text() |
|
185 elseif tag.name == "ReportTime" then |
|
186 object.report_time = tag:get_text() |
|
187 elseif tag.name == "Description" then |
|
188 object.desc = { text = tag:get_text(), lang = tag.attr["xml:lang"] } |
|
189 elseif tag.name == "Contact" then |
|
190 local jid = tag:get_child("AdditionalData").tags[1] |
|
191 local email = tag:get_child("Email") |
|
192 local telephone = tag:get_child("Telephone") |
|
193 local postaladdr = tag:get_child("PostalAddress") |
|
194 if not object.contacts then |
|
195 object.contacts = {} |
|
196 object.contacts[1] = { |
|
197 role = tag.attr.role, |
|
198 ext_role = (tag.attr["ext-role"] and true) or nil, |
|
199 type = tag.attr.type, |
|
200 ext_type = (tag.attr["ext-type"] and true) or nil, |
|
201 xmlns = jid.attr.xmlns, |
|
202 jid = jid:get_text(), |
|
203 email = email, |
|
204 telephone = telephone, |
|
205 postaladdr = postaladdr |
|
206 } |
|
207 else |
|
208 object.contacts[#object.contacts + 1] = { |
|
209 role = tag.attr.role, |
|
210 ext_role = (tag.attr["ext-role"] and true) or nil, |
|
211 type = tag.attr.type, |
|
212 ext_type = (tag.attr["ext-type"] and true) or nil, |
|
213 xmlns = jid.attr.xmlns, |
|
214 jid = jid:get_text(), |
|
215 email = email, |
|
216 telephone = telephone, |
|
217 postaladdr = postaladdr |
|
218 } |
|
219 end |
|
220 elseif tag.name == "RelatedActivity" then |
|
221 object.related = {} |
|
222 for _, t in ipairs(tag.tags) do |
|
223 if tag.name == "IncidentID" then |
|
224 object.related[#object.related + 1] = { text = t:get_text(), name = tag.attr.name } |
|
225 end |
|
226 end |
|
227 elseif tag.name == "Assessment" then |
|
228 local impact = tag:get_child("Impact") |
|
229 object.assessment = { lang = impact.attr.lang, severity = impact.attr.severity, completion = impact.attr.completion, type = impact.attr.type } |
|
230 elseif tag.name == "EventData" then |
|
231 local source = tag:get_child("Flow").tags[1] |
|
232 local target = tag:get_child("Flow").tags[2] |
|
233 local expectation = tag:get_child("Flow").tags[3] |
|
234 object.event_data = { sources = {}, targets = {} } |
|
235 for _, t in ipairs(source.tags) do |
|
236 local addr = t:get_child("Address") |
|
237 local cntr = t:get_child("Counter") |
|
238 object.event_data.sources[#object.event_data.sources + 1] = { |
|
239 address = { cat = addr.attr.category, ext = addr.attr["ext-category"], text = addr:get_text() }, |
|
240 counter = { type = cntr.attr.type, ext_type = cntr.attr["ext-type"], value = cntr:get_text() } |
|
241 } |
|
242 end |
|
243 for _, entry in ipairs(target.tags) do |
|
244 local noderole = { cat = entry:get_child("NodeRole").attr.category, ext = entry:get_child("NodeRole").attr["ext-category"] } |
|
245 local current = #object.event_data.targets + 1 |
|
246 object.event_data.targets[current] = { addresses = {}, noderole = noderole } |
|
247 for _, tag in ipairs(entry.tags) do |
|
248 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"] } |
|
249 end |
|
250 end |
|
251 if expectation then |
|
252 object.event_data.expectation = { |
|
253 action = expectation.attr.action, |
|
254 desc = expectation:get_child("Description") and expectation:get_child("Description"):get_text() |
|
255 } |
|
256 end |
|
257 elseif tag.name == "History" then |
|
258 object.history = {} |
|
259 for _, t in ipairs(tag.tags) do |
|
260 object.history[#object.history + 1] = { |
|
261 action = t.attr.action, |
|
262 date = t:get_child("DateTime"):get_text(), |
|
263 desc = t:get_chilld("Description"):get_text() |
|
264 } |
|
265 end |
|
266 end |
|
267 end |
|
268 |
|
269 local function stanza_parser(stanza) |
|
270 local object = {} |
|
271 |
|
272 if stanza:get_child("report", xmlns_inc) then |
|
273 local report = st.clone(stanza):get_child("report", xmlns_inc):get_child("Incident", xmlns_iodef) |
|
274 for _, tag in ipairs(report.tags) do do_tag_mapping(tag, object) end |
|
275 elseif stanza:get_child("request", xmlns_inc) then |
|
276 local request = st.clone(stanza):get_child("request", xmlns_inc):get_child("Incident", xmlns_iodef) |
|
277 for _, tag in ipairs(request.tags) do do_tag_mapping(tag, object) end |
|
278 elseif stanza:get_child("response", xmlns_inc) then |
|
279 local response = st.clone(stanza):get_child("response", xmlns_inc):get_child("Incident", xmlns_iodef) |
|
280 for _, tag in ipairs(response.tags) do do_tag_mapping(tag, object) end |
|
281 end |
|
282 |
|
283 return object |
|
284 end |
|
285 |
|
286 local function stanza_construct(id) |
|
287 if not id then return nil |
|
288 else |
|
289 local object = incidents[id].data |
|
290 local s_type = incidents[id].type |
|
291 local stanza = st.iq():tag(s_type or "report", { xmlns = xmlns_inc }) |
|
292 stanza:tag("Incident", { xmlns = xmlns_iodef, purpose = incidents[id].purpose }) |
|
293 :tag("IncidentID", { name = object.id.name }):text(object.id.text):up() |
|
294 :tag("StartTime"):text(object.start_time):up() |
|
295 :tag("EndTime"):text(object.end_time):up() |
|
296 :tag("ReportTime"):text(object.report_time):up() |
|
297 :tag("Description", { ["xml:lang"] = object.desc.lang }):text(object.desc.text):up():up(); |
|
298 |
|
299 local incident = stanza:get_child(s_type, xmlns_inc):get_child("Incident", xmlns_iodef) |
|
300 |
|
301 for _, contact in ipairs(object.contacts) do |
|
302 incident:tag("Contact", { role = (contact.ext_role and "ext-role") or contact.role, |
|
303 ["ext-role"] = (contact.ext_role and contact.role) or nil, |
|
304 type = (contact.ext_type and "ext-type") or contact.type, |
|
305 ["ext-type"] = (contact.ext_type and contact.type) or nil }) |
|
306 :tag("Email"):text(contact.email):up() |
|
307 :tag("Telephone"):text(contact.telephone):up() |
|
308 :tag("PostalAddress"):text(contact.postaladdr):up() |
|
309 :tag("AdditionalData") |
|
310 :tag("jid", { xmlns = contact.xmlns }):text(contact.jid):up():up():up() |
|
311 |
|
312 end |
|
313 |
|
314 incident:tag("RelatedActivity"):up(); |
|
315 |
|
316 for _, related in ipairs(object.related) do |
|
317 incident:get_child("RelatedActivity") |
|
318 :tag("IncidentID", { name = related.name }):text(related.text):up(); |
|
319 end |
|
320 |
|
321 incident:tag("Assessment") |
|
322 :tag("Impact", { |
|
323 lang = object.assessment.lang, |
|
324 severity = object.assessment.severity, |
|
325 completion = object.assessment.completion, |
|
326 type = object.assessment.type |
|
327 }):up():up(); |
|
328 |
|
329 incident:tag("EventData") |
|
330 :tag("Flow") |
|
331 :tag("System", { category = "source" }):up() |
|
332 :tag("System", { category = "target" }):up():up():up(); |
|
333 |
|
334 local e_data = incident:get_child("EventData") |
|
335 |
|
336 local sources = e_data:get_child("Flow").tags[1] |
|
337 local targets = e_data:get_child("Flow").tags[2] |
|
338 |
|
339 for _, source in ipairs(object.event_data.sources) do |
|
340 sources:tag("Node") |
|
341 :tag("Address", { category = source.address.cat, ["ext-category"] = source.address.ext }) |
|
342 :text(source.address.text):up() |
|
343 :tag("Counter", { type = source.counter.type, ["ext-type"] = source.counter.ext_type }) |
|
344 :text(source.counter.value):up():up(); |
|
345 end |
|
346 |
|
347 for _, target in ipairs(object.event_data.targets) do |
|
348 targets:tag("Node"):up() ; local node = targets.tags[#targets.tags] |
|
349 for _, address in ipairs(target.addresses) do |
|
350 node:tag("Address", { category = address.cat, ["ext-category"] = address.ext }):text(address.text):up(); |
|
351 end |
|
352 node:tag("NodeRole", { category = target.noderole.cat, ["ext-category"] = target.noderole.ext }):up(); |
|
353 end |
|
354 |
|
355 if object.event_data.expectation then |
|
356 e_data:tag("Expectation", { action = object.event_data.expectation.action }):up(); |
|
357 if object.event_data.expectation.desc then |
|
358 local expectation = e_data:get_child("Expectation") |
|
359 expectation:tag("Description"):text(object.event_data.expectation.desc):up(); |
|
360 end |
|
361 end |
|
362 |
|
363 if object.history then |
|
364 local history = incident:tag("History"):up(); |
|
365 |
|
366 for _, item in ipairs(object.history) do |
|
367 history:tag("HistoryItem", { action = item.action }) |
|
368 :tag("DateTime"):text(item.date):up() |
|
369 :tag("Description"):text(item.desc):up():up(); |
|
370 end |
|
371 end |
|
372 |
|
373 -- Sanitize contact empty tags |
|
374 for _, tag in ipairs(incident) do |
|
375 if tag.name == "Contact" then |
|
376 for i, check in ipairs(tag) do |
|
377 if (check.name == "Email" or check.name == "PostalAddress" or check.name == "Telephone") and |
|
378 not check:get_text() then |
|
379 table.remove(tag, i) |
|
380 end |
|
381 end |
|
382 end |
|
383 end |
|
384 |
|
385 if s_type == "request" then stanza.attr.type = "get" |
|
386 elseif s_type == "response" then stanza.attr.type = "set" |
|
387 else stanza.attr.type = "set" end |
|
388 |
|
389 return stanza |
|
390 end |
|
391 end |
|
392 |
|
393 |
|
394 _M = {} -- wraps methods into the library. |
|
395 _M.ft_str = ft_str |
|
396 _M.get_incident_layout = get_incident_layout |
|
397 _M.render_list = render_list |
|
398 _M.render_single = render_single |
|
399 _M.get_type = get_type |
|
400 _M.stanza_parser = stanza_parser |
|
401 _M.stanza_construct = stanza_construct |
|
402 _M.set_my_host = function(host) my_host = host end |
|
403 |
|
404 return _M |
|
405 |