├── .gitmodules ├── README.md ├── doc └── pictures │ ├── misp-wireshark.png │ ├── misp.png │ ├── options.png │ └── output.png ├── lib └── empty ├── misp-wireshark.lua ├── mispHelper.lua └── mispWiresharkUtils.lua /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/LuaMISP"] 2 | path = lib/LuaMISP 3 | url = https://github.com/MISP/LuaMISP.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # misp-wireshark 2 | 3 | `misp-wireshark` is a Lua plugin intended to help analysts extract data from Wireshark and convert it into the [MISP Core format](https://www.misp-project.org/datamodels/) 4 | 5 | [![](https://raw.githubusercontent.com/MISP/misp-wireshark/main/doc/pictures/misp-wireshark.png)](https://youtu.be/B7xs5SwhlTA) 6 | 7 | ## Usage 8 | 9 | ### Wireshark 10 | 11 | 1. Go to `Tools` located in Wireshark's top bar and click on `MISP: Export to MISP format` 12 | 2. Enter the export options to configure the behavior of the exporter 13 | - ![Plugin options](doc/pictures/options.png) 14 | - `Main filter`: Fill this field to filter the exported data. Essentially, it will just be a copy/paste from the global filter in the interface. (This cannot be done automatically because of [this](https://github.com/MISP/misp-wireshark/blob/89578d5c0eac9a23dc6f60afe223996ee0e50e32/misp-wireshark.lua#L70)) 15 | - `Include HTTP payload`: Should the payloads sent via HTTP be included as a file in the output 16 | - `Export path`: The location where the exported file should be saved when clicking on `Save to file` 17 | - `Tags`: Optional tags can be attached to some MISP attributes. 18 | 3. Copy or save in a file the data to be imported in MISP 19 | - ![Plugin output](doc/pictures/output.png) 20 | 4. Import in MISP 21 | - ![MISP result](doc/pictures/misp.png) 22 | 23 | ### Tshark 24 | Command-line options are the same parameters as in the user interface: 25 | - `filters`: The filter expression to be applied 26 | - `include_payload`: Should potentials payload be also exported. Accept `y` or `n` 27 | - `export_path`: The folder under which the json should be saved. If not supplied, default to stdout 28 | - `tags`: Optional tags to be attached to some MISP attributes 29 | 30 | 31 | **Example** 32 | 33 | ```bash 34 | tshark \ 35 | -r ~/Downloads/capture.cap \ 36 | -X lua_script:/home/john/.local/lib/wireshark/plugins/misp-wireshark/misp-wireshark.lua \ 37 | -X lua_script1:filters="ip.addr == 127.0.0.1" \ 38 | -X lua_script1:include_payload=n \ 39 | -X lua_script1:tags="tlp1,tlp2" \ 40 | frame.number == 0 41 | ``` 42 | *Note: As we did not supply an export path, the result is printed on stdout. However, to avoid mixing both the plugin output and tshark output, we provide a filter to tshark that will filter out every packets. However, this filter is not used by the plugin. Only the filter provided via `-X lua_script1:filters` is used.* 43 | 44 | *Based on the example above, `frame.number == 0` is only used to prevent the output of tshark while `ip.addr == 127.0.0.1` is actually used by the plugin* 45 | 46 | ## Installation 47 | 48 | On linux, clone the repository in wireshark's plugin location folder 49 | 50 | ```bash 51 | mkdir -p ~/.local/lib/wireshark/plugins 52 | cd ~/.local/lib/wireshark/plugins 53 | git clone https://github.com/MISP/misp-wireshark 54 | cd misp-wireshark/ 55 | git submodule update --init --recursive 56 | ``` 57 | 58 | ## Updates 59 | 60 | ```bash 61 | git pull 62 | git submodule update 63 | ``` 64 | 65 | 66 | ## Notes about `community-id` 67 | :warning: In order for this plugin to use [community-id](https://github.com/corelight/community-id-spec), wireshark must be at version 3.3.1 or higher. 68 | 69 | By default, `community-id` is disabled. To enable it, you have to perform these steps: 70 | 1. On the top bar go to `Analyze/Enabled Protocols...` 71 | 2. Search for `CommunityID` in the list 72 | 3. Check the checkbox 73 | 74 | 75 | ## Exports supported in MISP object format 76 | 77 | - [`network-connection`](https://www.misp-project.org/objects.html#_network_connection) from tcp 78 | - [`http-request`](https://www.misp-project.org/objects.html#_http_request) from tcp.http, including HTTP payloads 79 | - [`dns-record`](https://www.misp-project.org/objects.html#_dns_record) from udp.dns 80 | -------------------------------------------------------------------------------- /doc/pictures/misp-wireshark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MISP/misp-wireshark/be6d89df62503465d9b92e6839612111ea8fc068/doc/pictures/misp-wireshark.png -------------------------------------------------------------------------------- /doc/pictures/misp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MISP/misp-wireshark/be6d89df62503465d9b92e6839612111ea8fc068/doc/pictures/misp.png -------------------------------------------------------------------------------- /doc/pictures/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MISP/misp-wireshark/be6d89df62503465d9b92e6839612111ea8fc068/doc/pictures/options.png -------------------------------------------------------------------------------- /doc/pictures/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MISP/misp-wireshark/be6d89df62503465d9b92e6839612111ea8fc068/doc/pictures/output.png -------------------------------------------------------------------------------- /lib/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MISP/misp-wireshark/be6d89df62503465d9b92e6839612111ea8fc068/lib/empty -------------------------------------------------------------------------------- /misp-wireshark.lua: -------------------------------------------------------------------------------- 1 | local my_info = { 2 | version = "1.0", 3 | author = "Sami Mokaddem, CIRCL - Computer Incident Response Center Luxembourg", 4 | email = "sami.mokaddem@circl.lu", 5 | license = "AGPLv2", 6 | details = [[ 7 | This is a plugin for Wireshark, to output data in the MISP Format. 8 | Currently support the following: 9 | - `network-connection` from tcp 10 | - `http-request` from tcp.http, including HTTP payloads 11 | - `dns-record` from udp.dns 12 | ]], 13 | repository = "https://github.com/MISP/misp-wireshark" 14 | } 15 | set_plugin_info(my_info) 16 | 17 | local LuaMISP = require 'lib.LuaMISP.LuaMISP' 18 | local mispHelper = require 'mispHelper' 19 | local wiresharkUtils = require 'mispWiresharkUtils' 20 | 21 | local INCLUDE_HTTP_PAYLOAD = true 22 | local EXPORT_FILEPATH = '' 23 | local FILTERS = '' 24 | local TAGS = {} 25 | local summary = {} 26 | local final_output 27 | 28 | 29 | local args = { ... } 30 | local parsedArgs = {} 31 | if not gui_enabled() then 32 | if #args == 0 then 33 | return -- Plugin most probably loaded automatically by tshark 34 | end 35 | end 36 | 37 | local SUPPORT_COMMUNITY_ID 38 | if wiresharkUtils.check_wireshark_version() then 39 | SUPPORT_COMMUNITY_ID = true 40 | else 41 | SUPPORT_COMMUNITY_ID = false 42 | end 43 | 44 | 45 | local get_http = Field.new("http") 46 | local get_http_useragent = Field.new("http.user_agent") 47 | local get_http_host = Field.new("http.host") 48 | local get_http_file_data = Field.new("http.file_data") 49 | local get_http_request_method = Field.new("http.request.method") 50 | local get_http_request_uri = Field.new("http.request.uri") 51 | local get_http_response_in_frame = Field.new("http.response_in") 52 | local get_http_cookie = Field.new("http.cookie") 53 | local get_http_referer = Field.new("http.referer") 54 | local get_http_content_type = Field.new("http.content_type") 55 | local get_http_content_length = Field.new("http.content_length") 56 | local get_http_server = Field.new("http.server") 57 | local get_http_text = Field.new("text") 58 | 59 | local get_frame_number = Field.new("frame.number") 60 | local get_tcp = Field.new("tcp") 61 | local get_stream_index = Field.new("tcp.stream") 62 | local get_community_id 63 | if SUPPORT_COMMUNITY_ID then 64 | get_community_id = Field.new("communityid") 65 | end 66 | 67 | local get_dns = Field.new("dns") 68 | local get_dns_query_name = Field.new("dns.qry.name") 69 | local get_dns_query_type = Field.new("dns.qry.type") 70 | local get_dns_resp_class = Field.new("dns.resp.class") 71 | local get_dns_mx = Field.new("dns.mx.mail_exchange") 72 | local get_dns_ptr = Field.new("dns.ptr.domain_name") 73 | local get_dns_a = Field.new("dns.a") 74 | local get_dns_aaaa = Field.new("dns.aaaa") 75 | local get_dns_ns = Field.new("dns.ns") 76 | local get_dns_cname = Field.new("dns.cname") 77 | local get_dns_srv = Field.new("dns.srv.name") 78 | local get_dns_soa = Field.new("dns.soa.rname") 79 | local get_dns_spf = Field.new("dns.spf") 80 | 81 | 82 | local tcp_streams = {} 83 | local http_payloads = {} 84 | local http_packets = {} 85 | local dns_queries = {} 86 | 87 | local main_tw 88 | local tap 89 | local function menuable_tap(main_filter) 90 | main_tw = TextWindow.new("MISP format export result") 91 | 92 | -- local filters = get_filter() or '' -- get_filter() function is not working anymore. We rely on user provided filter instead 93 | FILTERS = main_filter 94 | tap = Listener.new(nil, FILTERS) 95 | register_tap() 96 | local function remove() 97 | -- this way we remove the listener that otherwise will remain running indefinitely 98 | tap:remove(); 99 | end 100 | 101 | -- we tell the window to call the remove() function when closed 102 | main_tw:set_atclose(remove) 103 | -- add buttons to the window 104 | main_tw:add_button("Save to file", function () wiresharkUtils.save_to_file(final_output, EXPORT_FILEPATH, tw) end) 105 | 106 | -- Ensure that all existing packets are processed. 107 | retap_packets() 108 | end 109 | 110 | function register_tap() 111 | -- this function will be called once for each packet 112 | function tap.packet(pinfo,tvb) 113 | local frame_number = tonumber(tostring(get_frame_number())) 114 | local community_id = nil 115 | if SUPPORT_COMMUNITY_ID and get_community_id() ~= nil then 116 | community_id = tostring(get_community_id()) 117 | end 118 | 119 | local tcp = get_tcp() 120 | if tcp then 121 | local stream_index = tonumber(tostring(get_stream_index())) 122 | local index = stream_index + 1 123 | local contextualData = { 124 | pinfo = pinfo, 125 | community_id = community_id, 126 | frame_number = frame_number, 127 | stream_index = stream_index, 128 | index = index, 129 | } 130 | handleTCP(tcp_streams, contextualData) 131 | 132 | local http = get_http() 133 | if http then 134 | handleHTTP(http_payloads, http_packets, contextualData) 135 | end 136 | end 137 | 138 | local dns = get_dns() 139 | if dns then 140 | local contextualData = { 141 | pinfo = pinfo, 142 | } 143 | handleDNS(dns_queries, contextualData) 144 | end 145 | end 146 | 147 | -- this function will be called once every few seconds to update our window 148 | function tap.draw() 149 | if main_tw ~= nil then 150 | main_tw:clear() 151 | end 152 | local collected_data = { 153 | tcp_streams = tcp_streams, 154 | http_payloads = http_payloads, 155 | http_packets = http_packets, 156 | dns_queries = dns_queries, 157 | } 158 | local misp_format = generate_misp_format(collected_data) 159 | if main_tw ~= nil then 160 | final_output = misp_format 161 | local output_too_large = #misp_format / 1024 > 500 -- Output larger than ~500k 162 | if not output_too_large then 163 | main_tw:set(misp_format) 164 | else 165 | local summary = generate_summary() 166 | local text = '' 167 | if (FILTERS == '') then 168 | text = text .. '[warning] No filters have been set. The whole capture has been processed.\n' 169 | end 170 | text = text .. '[info] Output is too large to be displayed.\n\nOutput content:\n' 171 | text = text .. summary 172 | main_tw:set(text) 173 | end 174 | else 175 | -- We are in a command-line context. 176 | saveToFileIfRequested(misp_format) 177 | end 178 | end 179 | 180 | 181 | -- this function will be called whenever a reset is needed 182 | -- e.g. when reloading the capture file 183 | function tap.reset() 184 | if main_tw ~= nil then 185 | main_tw:clear() 186 | end 187 | tcp_streams = {} 188 | http_payloads = {} 189 | end 190 | end 191 | 192 | 193 | local function dialog_options() 194 | local working_dir = get_working_directory() 195 | local function dialog_export_func(main_filter, include_http_payload, export_filepath, tags_text) 196 | INCLUDE_HTTP_PAYLOAD = getBoolFromString(include_http_payload, true) 197 | if export_filepath ~= '' then 198 | EXPORT_FILEPATH = export_filepath 199 | else 200 | EXPORT_FILEPATH = working_dir 201 | end 202 | TAGS = getTableFromString(tags_text) 203 | menuable_tap(main_filter) 204 | end 205 | new_dialog( 206 | "MISP format export options", 207 | dialog_export_func, 208 | "Main filter", 209 | "Include HTTP payload? (Y/n)", 210 | string.format("Export path (%s)", working_dir), 211 | "Tags (comma-separated)" 212 | -- {name="Main filter", value=get_filter()}, -- feature is not working according to the doc. Keep it in case it gets fixed. 213 | -- {name="Include HTTP payload? (Y/n)", value="Y"}, 214 | -- {name="Export file path", value=working_dir}, 215 | -- {name="Tags (comma-separated)", value="tlp:white,extraction-origin:wireshark"} 216 | ) 217 | if not SUPPORT_COMMUNITY_ID then 218 | wiresharkUtils.make_splash("Wireshark version is too old to export the community-id!\nThis script needs Wireshark version 3.3.1 or higher to include the community-id.\n") 219 | end 220 | end 221 | 222 | register_menu("MISP: Export to MISP format", dialog_options, MENU_TOOLS_UNSORTED) 223 | 224 | function getBoolFromString(include_http_payload, default) 225 | if include_http_payload == '' then 226 | return default 227 | else 228 | if include_http_payload == 'y' or include_http_payload == 'Y' or include_http_payload == '1' then 229 | return true 230 | else 231 | return false 232 | end 233 | end 234 | end 235 | 236 | function getTableFromString(string) 237 | local tag_table = {} 238 | for tag in string.gmatch(string, "[^,]+") do 239 | table.insert(tag_table, tag) 240 | end 241 | return tag_table 242 | end 243 | 244 | function get_working_directory() 245 | return os.getenv("PWD") or io.popen("echo $PWD"):read("*all") 246 | end 247 | 248 | 249 | function generate_misp_format(collected_data) 250 | local tcp_streams = collected_data.tcp_streams 251 | local http_payloads = collected_data.http_payloads 252 | local http_packets = collected_data.http_packets 253 | local dns_queries = collected_data.dns_queries 254 | summary = { 255 | network_connection = 0, 256 | http_request = 0, 257 | http_payload = 0, 258 | http_payload_total_size = 0, 259 | dns_record = 0, 260 | } 261 | 262 | local event = Event:new({title='Wireshark test event'}) 263 | 264 | local all_network_objects = {} 265 | for stream_id, tcp_stream in pairs(tcp_streams) do 266 | local network_object = mispHelper.generate_misp_network_connection_object_for_stream(tcp_stream) 267 | all_network_objects[stream_id] = network_object 268 | event:addObject(network_object) 269 | summary['network_connection'] = summary['network_connection'] + 1 270 | end 271 | 272 | for frame_number, http_packet in pairs(http_packets) do 273 | local stream_id = http_packet['stream_id'] 274 | local http_request_object = mispHelper.generate_misp_http_request_object(http_packet) 275 | event:addObject(http_request_object) 276 | summary['http_request'] = summary['http_request'] + 1 277 | all_network_objects[stream_id]:addReference(http_request_object, 'contains') 278 | if INCLUDE_HTTP_PAYLOAD then 279 | if http_payloads[stream_id] then 280 | if http_payloads[stream_id][frame_number] then 281 | local payload_object = mispHelper.generate_misp_file_object_for_payload(stream_id, frame_number, http_payloads[stream_id][frame_number]) 282 | all_network_objects[stream_id]:addReference(payload_object, 'contains') 283 | http_request_object:addReference(payload_object, 'contains') 284 | event:addObject(payload_object) 285 | summary['http_payload'] = summary['http_payload'] + 1 286 | a_attachment = payload_object:getAttributeByName('size-in-bytes') 287 | if a_attachment ~= nil then 288 | summary['http_payload_total_size'] = summary['http_payload_total_size'] + a_attachment.value 289 | end 290 | end 291 | end 292 | end 293 | end 294 | 295 | for query_name, dns_query in pairs(dns_queries) do 296 | local o_dns = mispHelper.generate_misp_dns_record_object(query_name, dns_query) 297 | event:addObject(o_dns) 298 | summary['dns_record'] = summary['dns_record'] + 1 299 | end 300 | local output = event:toJson() 301 | return output 302 | end 303 | 304 | function generate_summary() 305 | local text = '' 306 | for key, amount in pairs(summary) do 307 | if key == 'http_payload_total_size' then 308 | text = text .. string.format('- %s: %s\n', key, wiresharkUtils.humanizeFilesize(tonumber(amount))) 309 | else 310 | text = text .. string.format('- %s: %s\n', key, amount) 311 | end 312 | end 313 | return text 314 | end 315 | 316 | -- Tap handler 317 | --------------- 318 | 319 | function handleTCP(tcp_streams, contextualData) 320 | local pinfo = contextualData.pinfo 321 | local stream_index = contextualData.stream_index 322 | local index = contextualData.index 323 | local tcp_srcport = tonumber(pinfo.src_port) 324 | local tcp_dstport = tonumber(pinfo.dst_port) 325 | local tcp_srcip = tostring(pinfo.src) 326 | local tcp_dstip = tostring(pinfo.dst) 327 | 328 | if tcp_streams[index] == nil then 329 | tcp_streams[index] = { 330 | tcp_stream = tcp_stream, 331 | start_time = start_time, 332 | stop_time = start_time, 333 | tcp_src_port = tcp_srcport, 334 | tcp_dst_port = tcp_dstport, 335 | tcp_src_ip = tcp_srcip, 336 | tcp_dst_ip = tcp_dstip, 337 | community_id = contextualData.community_id, 338 | flow_duration = 0, 339 | packet_count = 1, 340 | } 341 | else 342 | local stream = tcp_streams[index] 343 | if start_time ~= nil then 344 | stream.stop_time = start_time 345 | stream.flow_duration = stream.stop_time - stream.start_time 346 | stream.packet_count = stream.packet_count + 1 347 | end 348 | end 349 | end 350 | 351 | function handleHTTP(http_payloads, http_packets, contextualData) 352 | local index = contextualData.index 353 | local frame_number = contextualData.frame_number 354 | local tcp_srcip = tostring(contextualData.pinfo.src) 355 | local tcp_dstip = tostring(contextualData.pinfo.dst) 356 | 357 | local http_file_data = get_http_file_data() 358 | local http_response_in_frame = get_http_response_in_frame() 359 | if http_response_in_frame then 360 | http_response_in_frame = tonumber(http_response_in_frame) 361 | end 362 | local method = get_http_request_method() 363 | if method then 364 | method = tostring(method) 365 | end 366 | local host = get_http_host() 367 | if host then 368 | host = tostring(host) 369 | end 370 | local uri = get_http_request_uri() 371 | if uri then 372 | uri = tostring(uri) 373 | end 374 | local user_agent = get_http_useragent() 375 | if user_agent then 376 | user_agent = tostring(user_agent) 377 | end 378 | local refere = get_http_referer() 379 | if refere then 380 | refere = tostring(refere) 381 | end 382 | local content_type = get_http_content_type() 383 | if content_type then 384 | content_type = tostring(content_type) 385 | end 386 | local content_length = get_http_content_length() 387 | if content_length then 388 | content_length = tostring(content_length) 389 | end 390 | local cookie = get_http_cookie() 391 | if cookie then 392 | cookie = tostring(cookie) 393 | end 394 | local server = get_http_server() 395 | if server then 396 | server = tostring(server) 397 | end 398 | local text = get_http_text() 399 | if text then 400 | text = tostring(text) 401 | end 402 | 403 | if http_file_data then 404 | if http_payloads[index] == nil then 405 | http_payloads[index] = {} 406 | end 407 | local raw_data = { 408 | len = http_file_data.range:bytes():len(), 409 | rawData = http_file_data.range:bytes():raw(), 410 | name = http_file_data.name, 411 | } 412 | http_payloads[index][frame_number] = raw_data 413 | end 414 | 415 | if http_packets[frame_number] == nil then 416 | http_packets[frame_number] = { 417 | stream_id = index, 418 | http_response_in_frame = http_response_in_frame, 419 | http_file_data = http_file_data, 420 | host = host, 421 | method = method, 422 | uri = uri, 423 | user_agent = user_agent, 424 | refere = refere, 425 | content_type = content_type, 426 | content_length = content_length, 427 | cookie = cookie, 428 | server = server, 429 | http_text = text, 430 | tcp_src_ip = tcp_srcip, 431 | tcp_dst_ip = tcp_dstip, 432 | } 433 | end 434 | end 435 | 436 | function handleDNS(dns_queries, contextualData) 437 | local query_name = get_dns_query_name() 438 | if query_name then 439 | query_name = tostring(query_name) 440 | else 441 | return 442 | end 443 | 444 | if dns_queries[query_name] == nil then 445 | dns_queries[query_name] = {} 446 | end 447 | 448 | dns_queries[query_name]['first_seen'] = first_seen 449 | 450 | local function setFieldValue(var, is_string) 451 | local var_value = var() 452 | if var_value then 453 | if is_string then 454 | return tostring(var_value) 455 | else 456 | return getAllFieldValues(var) 457 | end 458 | end 459 | end 460 | 461 | dns_queries[query_name]['dns_query_type'] = setFieldValue(get_dns_query_type, true) 462 | dns_queries[query_name]['dns_resp_class'] = setFieldValue(get_dns_resp_class, true) 463 | 464 | dns_queries[query_name]['dns_mx'] = setFieldValue(get_dns_mx, false) 465 | dns_queries[query_name]['dns_ptr'] = setFieldValue(get_dns_ptr, false) 466 | dns_queries[query_name]['dns_a'] = setFieldValue(get_dns_a, false) 467 | dns_queries[query_name]['dns_aaaa'] = setFieldValue(get_dns_aaaa, false) 468 | dns_queries[query_name]['dns_ns'] = setFieldValue(get_dns_ns, false) 469 | dns_queries[query_name]['dns_cname'] = setFieldValue(get_dns_cname, false) 470 | dns_queries[query_name]['dns_srv'] = setFieldValue(get_dns_srv, false) 471 | dns_queries[query_name]['dns_soa'] = setFieldValue(get_dns_soa, false) 472 | dns_queries[query_name]['dns_spf'] = setFieldValue(get_dns_spf, false) 473 | end 474 | 475 | function getAllFieldValues(field) 476 | local tmp = { field() } 477 | local values = {} 478 | for i=1,#tmp do 479 | values[i] = tostring(tmp[i]) 480 | end 481 | return values 482 | end 483 | 484 | function saveToFileIfRequested(misp_format) 485 | if parsedArgs['export_path'] ~= nil then 486 | wiresharkUtils.save_to_file(misp_format, EXPORT_FILEPATH, nil) 487 | else 488 | print(misp_format) 489 | end 490 | end 491 | 492 | -- Main 493 | ----------------------- 494 | if not gui_enabled() then 495 | if #args == 0 then 496 | return -- Plugin most probably loaded automatically by tshark 497 | else 498 | if #args == 1 and (args[1] == 'help') then 499 | print('Usage:') 500 | print('\t`export_path`:\t-X lua_script1:export_path=/home/john/Document') 501 | print('\t`include_payload`:\t-X lua_script1:include_payload=y') 502 | print('\t`tags`:\t-X lua_script1:tags="tag1,tag2"') 503 | print('\t`help`:\t-X lua_script1:help"\n') 504 | end 505 | parsedArgs = wiresharkUtils.parse_args(args) 506 | if parsedArgs['export_path'] == nil then 507 | local working_dir = get_working_directory() 508 | EXPORT_FILEPATH = working_dir 509 | else 510 | EXPORT_FILEPATH = parsedArgs['export_path'] 511 | end 512 | INCLUDE_HTTP_PAYLOAD = getBoolFromString(parsedArgs['include_payload'], true) 513 | TAGS = getTableFromString(parsedArgs['tags']) 514 | FILTERS = parsedArgs['filters'] or '' 515 | end 516 | 517 | tap = Listener.new(nil, FILTERS) 518 | register_tap() 519 | end 520 | -------------------------------------------------------------------------------- /mispHelper.lua: -------------------------------------------------------------------------------- 1 | 2 | local utils = require 'mispWiresharkUtils' 3 | 4 | local mispHelper = {} 5 | 6 | -- Registed used MISP object templates 7 | -------------------------------------- 8 | 9 | local NETWORK_CONNECTION_TEMPLATE = { 10 | name = "network-connection", 11 | description = "A local or remote network connection.", 12 | template_uuid = "af16764b-f8e5-4603-9de1-de34d272f80b", 13 | template_version = "3" 14 | } 15 | NETWORK_CONNECTION_TEMPLATE['meta-category'] = "network" 16 | function get_network_connection_template() 17 | return utils.deepcopy(NETWORK_CONNECTION_TEMPLATE) 18 | end 19 | 20 | local FILE_TEMPLATE = { 21 | name = "file", 22 | description = "File object describing a file with meta-information", 23 | template_uuid = "688c46fb-5edb-40a3-8273-1af7923e2215", 24 | template_version = "24" 25 | } 26 | FILE_TEMPLATE['meta-category'] = "file" 27 | function get_file_template() 28 | return utils.deepcopy(FILE_TEMPLATE) 29 | end 30 | 31 | local HTTP_REQUEST_TEMPLATE = { 32 | name = "http-request", 33 | description = "A single HTTP request header", 34 | template_uuid = "b4a8d163-8110-4239-bfcf-e08f3a9fdf7b", 35 | template_version = "4" 36 | } 37 | HTTP_REQUEST_TEMPLATE['meta-category'] = "network" 38 | function get_http_request_template() 39 | return utils.deepcopy(HTTP_REQUEST_TEMPLATE) 40 | end 41 | 42 | local DNS_RECORD_TEMPLATE = { 43 | name = "dns-record", 44 | description = "A set of DNS records observed for a specific domain.", 45 | template_uuid = "f023c8f0-81ab-41f3-9f5d-fa597a34a9b9", 46 | template_version = "2" 47 | } 48 | DNS_RECORD_TEMPLATE['meta-category'] = "network" 49 | function get_dns_record_template() 50 | return utils.deepcopy(DNS_RECORD_TEMPLATE) 51 | end 52 | 53 | local PASSIVE_DNS_TEMPLATE = { 54 | name = "passive-dns", 55 | description = "Passive DNS records as expressed in draft-dulaunoy-dnsop-passive-dns-cof-07. See https://tools.ietf.org/id/draft-dulaunoy-dnsop-passive-dns-cof-07.html", 56 | template_uuid = "b77b7b1c-66ab-4a41-8da4-83810f6d2d6c", 57 | template_version = "5" 58 | } 59 | PASSIVE_DNS_TEMPLATE['meta-category'] = "network" 60 | function get_passive_dns_template() 61 | return utils.deepcopy(PASSIVE_DNS_TEMPLATE) 62 | end 63 | 64 | -- Actual functions converting data collected from the tap into LuaMISP entities 65 | --------------------------------------------------------------------------------- 66 | 67 | function mispHelper.generate_misp_network_connection_object_for_stream(tcp_stream) 68 | local a_src_port = Attribute:new({object_relation='src-port', type='port', value=tcp_stream['tcp_src_port']}) 69 | local a_dst_port = Attribute:new({object_relation='dst-port', type='port', value=tcp_stream['tcp_dst_port']}) 70 | local a_ip_src = Attribute:new({object_relation='ip-src', type='ip-src', value=tcp_stream['tcp_src_ip']}) 71 | local a_ip_dst = Attribute:new({object_relation='ip-dst', type='ip-dst', value=tcp_stream['tcp_dst_ip']}) 72 | local a_layer3 = Attribute:new({object_relation='layer3-protocol', type='text', value='IP'}) 73 | local a_layer4 = Attribute:new({object_relation='layer4-protocol', type='text', value='TCP'}) 74 | local a_layer7 = Attribute:new({object_relation='layer7-protocol', type='text', value='HTTP'}) 75 | local a_community_id = Attribute:new({object_relation='community-id', type='community-id', value=tcp_stream['community_id']}) 76 | local first_seen = os.date("%Y-%m-%d %X", tcp_stream['start_time']) 77 | local last_seen = os.date("%Y-%m-%d %X", tcp_stream['stop_time']) 78 | 79 | if TAGS then 80 | a_src_port:addTags(TAGS) 81 | a_dst_port:addTags(TAGS) 82 | a_ip_src:addTags(TAGS) 83 | a_ip_dst:addTags(TAGS) 84 | end 85 | 86 | local o_network_connection = Object:new(get_network_connection_template()) 87 | o_network_connection.first_seen = first_seen 88 | o_network_connection.last_seen = last_seen 89 | o_network_connection:addAttributes({ 90 | a_src_port, 91 | a_dst_port, 92 | a_ip_src, 93 | a_ip_dst, 94 | a_layer3, 95 | a_layer4, 96 | a_layer7, 97 | a_community_id 98 | }) 99 | return o_network_connection 100 | end 101 | 102 | function mispHelper.generate_misp_file_object_for_payload(stream_id, frame_number, http_payload) 103 | local payload_name = string.format("payload-%s-%s", stream_id, frame_number) 104 | local a_attachment = Attribute:new({type='attachment', object_relation='attachment', value=payload_name, data=http_payload.rawData}) 105 | local a_filename = Attribute:new({type='filename', object_relation='filename', value=payload_name, disable_correlation=1}) 106 | local a_fsize = Attribute:new({type='size-in-bytes', object_relation='size-in-bytes', value=http_payload.len}) 107 | local o_payload = Object:new(get_file_template()) 108 | o_payload.comment = http_payload.name 109 | if TAGS then 110 | a_attachment:addTags(TAGS) 111 | a_filename:addTags(TAGS) 112 | end 113 | o_payload:addAttributes({ 114 | a_attachment, 115 | a_filename, 116 | a_fsize, 117 | }) 118 | return o_payload 119 | end 120 | 121 | function mispHelper.generate_misp_http_request_object(http_request) 122 | local a_content_type = Attribute:new({object_relation='content-type', type='other', value=http_request['content_type']}) 123 | local a_cookie = Attribute:new({object_relation='cookie', type='port', value=http_request['cookie']}) 124 | local a_ip_dst = Attribute:new({object_relation='ip-dst', type='ip-dst', value=http_request['tcp_dst_ip']}) 125 | local a_ip_src = Attribute:new({object_relation='ip-src', type='ip-src', value=http_request['tcp_src_ip']}) 126 | local a_method = Attribute:new({object_relation='method', type='http-method', value=http_request['method']}) 127 | local a_refere = Attribute:new({object_relation='referer', type='other', value=http_request['refere']}) 128 | local a_uri = Attribute:new({object_relation='uri', type='uri', value=http_request['uri']}) 129 | local a_user_agent = Attribute:new({object_relation='user-agent', type='user-agent', value=http_request['user_agent']}) 130 | local a_content_length = Attribute:new({object_relation='content-length', type='other', value=http_request['content_length']}) -- unused 131 | local a_server = Attribute:new({object_relation='server', type='text', value=http_request['server']}) -- unused 132 | local a_text = Attribute:new({object_relation='text', type='text', value=http_request['text']}) -- unused 133 | local first_seen = os.date("%Y-%m-%d %X", http_request['start_time']) 134 | local last_seen = os.date("%Y-%m-%d %X", http_request['stop_time']) 135 | 136 | if TAGS and a_uri ~= nil then 137 | a_uri:addTags(TAGS) 138 | end 139 | if TAGS and a_text ~= nil then 140 | a_text:addTags(TAGS) 141 | end 142 | if TAGS and a_ip_dst ~= nil then 143 | a_ip_dst:addTags(TAGS) 144 | end 145 | 146 | local o_http_request = Object:new(get_http_request_template()) 147 | o_http_request.first_seen = first_seen 148 | o_http_request.last_seen = last_seen 149 | o_http_request:addAttribute(a_content_type) 150 | o_http_request:addAttribute(a_cookie) 151 | o_http_request:addAttribute(a_ip_dst) 152 | o_http_request:addAttribute(a_ip_src) 153 | o_http_request:addAttribute(a_method) 154 | o_http_request:addAttribute(a_refere) 155 | o_http_request:addAttribute(a_uri) 156 | o_http_request:addAttribute(a_user_agent) 157 | return o_http_request 158 | end 159 | 160 | function mispHelper.generate_misp_dns_record_object(query_name, dns_query) 161 | local o_dns_record = Object:new(get_dns_record_template()) 162 | o_dns_record.first_seen = os.date("%Y-%m-%d %X", dns_query['first_seen']) 163 | 164 | local function addAttributeFor(object, dnsEntry, object_relation, type) 165 | if dnsEntry ~= nil then 166 | for i, recordValue in pairs(dnsEntry) do 167 | local attribute = Attribute:new({object_relation=object_relation, type=type, value=recordValue, comment=string.format('%s-#%d', object_relation, i)}) 168 | object:addAttribute(attribute) 169 | end 170 | end 171 | end 172 | 173 | o_dns_record:addAttribute(Attribute:new({object_relation='queried-domain', type='domain', value=query_name})) 174 | addAttributeFor(o_dns_record, dns_query['dns_a'], 'a-record', 'ip-dst') 175 | addAttributeFor(o_dns_record, dns_query['dns_aaaa'], 'aaaa-record', 'ip-dst') 176 | addAttributeFor(o_dns_record, dns_query['dns_mx'], 'mx-record', 'domain') 177 | addAttributeFor(o_dns_record, dns_query['dns_ns'], 'ns-record', 'domain') 178 | addAttributeFor(o_dns_record, dns_query['dns_ptr'], 'ptr-record', 'domain') 179 | addAttributeFor(o_dns_record, dns_query['dns_cname'], 'cname-record', 'domain') 180 | addAttributeFor(o_dns_record, dns_query['dns_srv'], 'srv-record', 'domain') 181 | addAttributeFor(o_dns_record, dns_query['dns_soa'], 'soa-record', 'domain') 182 | addAttributeFor(o_dns_record, dns_query['dns_spf'], 'spf-record', 'ip-dst') 183 | 184 | return o_dns_record 185 | end 186 | 187 | function mispHelper.generate_misp_passive_dns_object() 188 | end 189 | 190 | return mispHelper -------------------------------------------------------------------------------- /mispWiresharkUtils.lua: -------------------------------------------------------------------------------- 1 | 2 | local utils = {} 3 | -- https://gist.github.com/tylerneylon/81333721109155b2d244 4 | function utils.deepcopy(obj) 5 | if type(obj) ~= 'table' then return obj end 6 | local res = setmetatable({}, getmetatable(obj)) 7 | for k, v in pairs(obj) do res[utils.deepcopy(k)] = utils.deepcopy(v) end 8 | return res 9 | end 10 | 11 | function utils.save_to_file(content, export_filepath, tw) 12 | local now = os.time(os.date("!*t")) 13 | local filename = string.format("wireshark-misp-%s.json", now) 14 | local full_path 15 | if export_filepath ~= '' then 16 | full_path = string.format('%s/%s', export_filepath, filename) 17 | else 18 | full_path = string.format('%s', export_filepath, filename) 19 | end 20 | local file = assert(io.open(full_path, "w")) 21 | file:write(content) 22 | file:close() 23 | utils.make_splash(string.format("Saved %s at %s", filename, export_filepath)) 24 | if tw then 25 | tw:close() 26 | end 27 | end 28 | 29 | function utils.make_splash(text) 30 | if gui_enabled() then 31 | local splash = TextWindow.new("MISP Export error"); 32 | splash:set(text) 33 | return splash 34 | else 35 | print(text) 36 | end 37 | end 38 | 39 | -- verify tshark/wireshark version is new enough - needs to be 3.3.1+ as community was introduced in this version 40 | function utils.check_wireshark_version() 41 | local version_ok = true 42 | local major, minor, micro = 0, 0, 0 43 | major, minor, micro = get_version():match("(%d+)%.(%d+)%.(%d+)") 44 | if ( 45 | tonumber(major) < 3) or 46 | ((tonumber(major) <= 3) and (tonumber(minor) < 3)) or 47 | ((tonumber(major) <= 3) and (tonumber(minor) <= 3) and (tonumber(micro) < 1) 48 | ) then 49 | version_ok = false 50 | end 51 | return version_ok 52 | end 53 | 54 | function utils.humanizeFilesize(size) 55 | if (size == 0) then 56 | return "0.00 B" 57 | end 58 | 59 | local sizes = {'B', 'kB', 'MB', 'GB', 'TB', 'PB'} 60 | local e = math.floor(math.log(size, 1024)) 61 | local significant = math.floor(size/math.pow(1024, e), 2) 62 | local remaining = math.floor(size/math.pow(1024, e-1), 2) % 1024 63 | local text = string.format("%s.%s%s", significant, remaining, sizes[e]) 64 | return text 65 | end 66 | 67 | function utils.parse_args(args) 68 | local t = {} 69 | for i, arg in ipairs(args) do 70 | local matches = string.gmatch(arg, "([^=]+)=(.+)") 71 | local k, v = matches() 72 | if k ~= '' and v ~= '' then 73 | t[k] = v 74 | end 75 | end 76 | return t 77 | end 78 | 79 | return utils --------------------------------------------------------------------------------